分库主要是解决单库读写能力不足的问题,分表主要是解决单表数据量过大的问题,读写分离则是在读多写少场景下的一种优化方案。Sharding-JDBC 作为数据库中间件,提供了这三个功能(Sharding-JDBC 的功能不仅限于此)。这篇文章则主要讲述在 Springboot 下如何使用 Sharding-JDBC 来解决分库/分表及读写分离的问题。
概述
ShardingSphere 是一套开源的分布式数据库中间件解决方案组成的生态圈,它由 Sharding-JDBC、Sharding-Proxy 和 Sharding-Sidecar(计划中)这3款相互独立的产品组成。 他们均提供标准化的数据分片、分布式事务和数据库治理功能,可适用于如 Java 同构、异构语言、云原生等各种多样化的应用场景。
- Sharding-JDBC: 定位为轻量级 Java 框架,在 Java 的 JDBC 层提供的额外服务。 它使用客户端直连数据库,以 jar 包形式提供服务,无需额外部署和依赖,可理解为增强版的 JDBC 驱动,完全兼容 JDBC 和各种 ORM 框架;
- Sharding-Proxy: 定位为透明化的数据库代理端,提供封装了数据库二进制协议的服务端版本,用于完成对异构语言的支持。 目前先提供 MySQL/PostgreSQL 版本,它可以使用任何兼容 MySQL/PostgreSQL 协议的访问客户端(如:MySQL Command Client, MySQL Workbench, Navicat等 )操作数据,对 DBA 更加友好;
- Sharding-Sidecar: 定位为 Kubernetes 的云原生数据库代理,以 Sidecar 的形式代理所有对数据库的访问。 通过无中心、零侵入的方案提供与数据库交互的的啮合层,即 Database Mesh,又可称数据网格。
核心概念
数据分表
数据分片指按照某个维度将存放在单一数据库中的数据分散地存放至多个数据库或表中以达到提升性能瓶颈以及可用性的效果。
读写分离
对于同一时刻有大量并发读操作和较少写操作类型的应用系统来说,将数据库拆分为主库和从库,主库负责处理事务性的增删改操作,从库负责处理查询操作,能够有效的避免由数据更新导致的行锁,使得整个系统的查询性能得到极大的改善。
与将数据根据分片键打散至各个数据节点的水平分片不同,读写分离则是根据 SQL 语义的分析,将读操作和写操作分别路由至主库与从库。
逻辑表
水平拆分的数据库(表)的相同逻辑和数据结构表的总称。例:订单数据根据主键尾数拆分为 10 张表,分别是 t_order_0 到 t_order_9,他们的逻辑表名为 t_order.
真实表
在分片的数据库中真实存在的物理表。即上个示例中的 t_order_0 到 t_order_9.
数据节点
数据分片的最小单元。由数据源名称和数据表组成,例:ds_0.t_order_0.
绑定表
指分片规则一致的主表和子表。例如:t_order 表和 t_order_item 表,均按照 order_id 分片,则此两张表互为绑定表关系。绑定表之间的多表关联查询不会出现笛卡尔积关联,关联查询效率将大大提升。
广播表
指所有的分片数据源中都存在的表,表结构和表中的数据在每个数据库中均完全一致。适用于数据量不大且需要与海量数据的表进行关联查询的场景,例如:字典表。
配置数据库
以 t_order
例,如下图所示:
t_order
数据库有两个分片,以user_id
为分片键,同一个用户的所有订单在一个库里。每一个分片有一个 slave 结点,实现主从复制;t_order
以order_id
为分片键,分为两个表;t_order_item
与t_order
是绑定关系,有同样的分片键;t_config
为广播表,每个库都会 copy 一份数据。
主从配置
需要建立 4 个库,分别是 master0, master1, slave0, slave1, 其中 master0, master1 是主库,slave1 是 master0 的从库,slave1 是 master1 的从库。在这里,我们使用 docker 来安装 4 个库,以 master0, slave0 为例。
安装数据库
1 | # 创建 master0 |
Mysql 配置参数:
- 配置文件目录:/etc/mysql
- 日志文件目录:/var/log/mysql
- 数据文件目录:/var/lib/mysql
配置 my.cnf
master0 my.cnf
配置:1
2
3
4[mysqld]
mysql-bin # 开启 binlog =
ROW # 选择 ROW 模式 =
server_id=1 # 配置 MySQL replaction 需要定义。
slave0 my.cnf
配置:1
2
3
4
5
6
7
8
9
10
11[mysqld]
mysql-bin # 开启 binlog =
ROW # 选择 ROW 模式 =
server_id=2 # 配置 MySQL replaction 需要定义,不要和 master 的 slaveId 重复
## relay_log 配置中继日志
relay_log=mysql-relay-bin
## 跳过主从复制中遇到的所有错误或指定类型的错误,避免slave端复制中断
## 如:1062错误是指一些主键重复,1032错误是因为主从数据库数据不一致
slave_skip_errors=1062
获取 master position
进入主结点 master0, 获取 master 状态。
1 | # 进入容器 |
记下 File
和 Position
信息,后面会用到。
获取 master0 ip 信息
1 | $ docker inspect --format='{{.NetworkSettings.IPAddress}}' mysql-master |
开启主从功能
进入从结点,开启主从功能;1
2
3
4
5
6
7
8
9
10
11
12
13
14# 进入容器
$ docker exec -it mysql-master0 /bin/bash
# 容器内,访问 Mysql 服务
$ mysql -uroot -p123456
# 根据 master0 ip, position 及 slave 用户设置主从
mysql> change master to master_host='172.17.0.4',master_user='slave',master_password='slave',master_log_file='mysql-bin.000003',master_log_pos=777;
# 启动主从功能
mysql> start slave;
# 查看从结点信息
mysql> show slave status\G
初始化 Sql 脚本
1 | CREATE DATABASE IF NOT EXISTS ds0 DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_bin; |
Java 实例
引入依赖
1 | <!-- 依赖包版本管理 --> |
说明:
使用 druid
连接池不需要引入 druid-spring-boot-starter
包,只要引入 druid
即可,否则加载数据源不成功。
定义数据库对象
以 t_order
为例。
1 | public class Order { |
定义 Mapper 对象
1 | public interface OrderMapper { |
说明:
- 若数据表主键由 Sharding-JDBC 生成,则在插入操作中不应包含主键,该主键由 Shardings-JDBC 生成,并改写 Sql 自动插入到数据库中,如此处的
t_order
; - 可通过
Options(useGeneratedKeys = true, keyProperty = "orderId")
选项,返回插入的主键。
Shardings-JDBC 配置
1 | # 定义数据源 |
测试代码
插入 Order 表
1 |
|
根据 userId
进行的分库,orderId
进行分表,order
表数据均衡分布到两个主库中。
关联查询
根据 orderId
列表 对 order
, order_item
进行关联查询。
1 |
|
关联查询因为没有分库的信息,所以会查询所有的分库(如果设置了读写分离,则从主库的从查询),再将所有分库的结果进行聚合。在每一个分库中查询,又分为两种情况:1) 绑定表;2) 非绑定表;
绑定表
1 | 2022-05-21 15:38:24.602 [main] INFO ShardingSphere-SQL - Logic SQL: select o.order_id as orderId, o.order_no as orderNo, o.user_id as userId, o.name as name,o.price as price,oi.item_id as itemId from t_order o left join t_order_item oi on o.order_id = oi.order_id where o.order_id in ( ? , ? ) |
在两个分库两个分表的情况下,这个关联查询转化为两个查询,一个分库一个查询。由于 orderId
是分表键,可以定位到数据位于 t_order_0
和 t_order_item_0
表中,直接查询即可。
非绑定表
1 | 2022-05-21 15:39:34.135 [main] INFO ShardingSphere-SQL - Logic SQL: select o.order_id as orderId, o.order_no as orderNo, o.user_id as userId, o.name as name,o.price as price,oi.item_id as itemId from t_order o left join t_order_item oi on o.order_id = oi.order_id where o.order_id in ( ? , ? ) |
在两个分库两个分表的情况下,这个关联查询转化为四个查询。在一个分库中,根据分表键 orderId
, 可以定位到这两个 orderId
位于 t_order_0
中,由于 t_order_0
和 t_order_item_0
表没有绑定关系,所以 t_order_0
表需要与所有 t_order_item
表进行关联查询,最终需要执行四个查询。
插入广播表
1 |
|
插入广播表会在所有分库中执行插入操作。
1 | 2022-05-21 15:40:27.425 [main] INFO ShardingSphere-SQL - Logic SQL: INSERT INTO t_config(code,name,create_date) VALUES(?, ?, ?) |
如上所述,向 t_config
中插入一条数据,会中所有分库的主库中执行操作。
查询所有数据
1 |
|
查询语句中如果没有分库分表健,会默认查询所有表。
1 | 2022-05-21 15:41:16.781 [main] INFO ShardingSphere-SQL - Logic SQL: SELECT * FROM t_order |
工程代码:https://github.com/noahsarkzhang-ts/springboot-lab/tree/main/springcloud-sharding-jdbc
参考:
2. Sharding-JDBC > 配置手册 > Spring Boot配置