MySQL
分库分表
一、解决的问题
- 用户请求量太大,导致 操作变慢 - 分库、分表
- 单库数据量太大,导致 硬盘空间不足 - 分库
- 单表数据量太大,导致 操作、增加索引、增加字段 变慢 - 分表
二、拆分类型
1. 垂直拆分
1. 垂直分库
-
按照 业务切分
-
【目的】保证 数据隔离
2. 垂直分表
-
将 常用字段 与 不常用字段或大字段 进行拆分
-
【目的】避免 binlog 文档过大,导致同步延迟
2. 水平拆分
1. 水平分表
-
将 一张表的数据 切分到 多张表 中,但多张表仍在同个库
-
【目的】避免 单表级别的 IO 瓶颈 - 库级别仍有 IO 瓶颈
-
规则
- Range
- 时间 : 按照 年、月、日 切分 - order_2020、order_202001、order_20200101
- 地点 : 按照 省、市 切分 - order_beijing、order_shanghai、order_chengdu
- 大小 : 按照 数据量 切分 - order_1000000、order_2000000
- Hash
- 根据 特定字段 取模
- 用户 ID
- 站内信 : 用户只能看到自己的站内信,按照 收件人 ID 分库,保证查找请求都在同一个库内
- 用户表 : 直接将 用户 ID 哈希取模分库,保证用户数据均衡分散在库中
- 用户 ID
- 根据 特定字段 取模
- Range
2. 水平分库
-
将 一张表的数据 切分到 多个数据库 上,每个数据库都有相应的表
-
【目的】避免 单表、单库 级别的 IO 瓶颈
-
原则
- 不跨库、不跨表,保证 同一类数据 都在 同一个数据库 上
- 需考虑如何高效获取数据,如果查找需横跨多个节点,则应谨慎使用
3. 复杂案例
-
关系表
- 场景 : 用户表依照 ID 进行分库,但允许使用 手机号 登录,需避免全库扫描
- 方案 : 创建 手机号 与 用户ID 的 关系表,关系表 按照 手机号 切分
-
冗于表
-
场景 : 招聘网站,求职者(C端用户)、企业(B端用户)。
C端用户需查看自己的投递记录与状态,B端用户需查看自己收到的简历。
需满足 C端用户 与 B端用户 的查找,且不需跨库。
一份投递纪录 两种用户 都希望依照自己的面向来查找
-
方案 : 利用空间换时间,创建冗于表,将一份投递纪录存为两份。C端用户以 用户ID 为分片键,B端用户 以简历投递的 企业ID 为分片键
-
三、Sharding JDBC
1. 表概念
-
真实表 : 在数据库中真实存在 - b_order0、b_order1
-
逻辑表 : 分片后,同一类表结构的总称 - b_order
-
数据节点 : 分片后,数据源 和 数据表 的组合 - ds0.b_order1
-
绑定表 : 分片规则一致的关系表(主表、子表) - b_order、b_order_item
绑定表 之间的 关联查找 不会出现笛卡尔积(所有可能结果组合而成的集合)
-
广播表
- 没必要分片,但需要与海量数据的表进行关联查找 - 字典表、省份表
- 会在不同数据节点存储,但 表结构 与 数据 完全相同
2. 分片算法
- 精确 : 使用 单一键 作为分片键,并使用 =、IN 进行分片
- 范围 : 使用 单一键 作为分片键,并使用 BETWEEN、>、<、>=、<= 进行分片
- 复合 : 使用 多键 作为分片键,开发者需根据业务自行撰写分片逻辑
- Hint : 分片键不在 SQL 语句、数据库 中,而是由其他外置条件决定 - 登录地区
3. 分片策略
分片策略 由 分片算法 构成
-
标准 StandardShardingStrategy
- 只支持 单分片键,由 精确分片算法(必选) 与 范围分片算法(可选) 构成
- 如果在 SQL 中使用范围操作,但未配置 范围分片算法,将导致 全库路由扫描,效率低
-
复合 ComplexShardingStrategy
- 支持 多分片键,由 精确分片算法 与 范围分片算法 构成
- 具体策略需由开发者自行实现
-
【常用】行表达式 InlineShardingStrategy
- 只支持 单分片键,使用 Groovy 表达式,提供 精确分片算法 支持
-
Hint HintShardingStrategy
- 透过 Hint 指定分片值,而非从 SQL 中提取分片值
- 读写分离时,可以强制从主库读取
-
不分片 NoneShardingStrategy
4. 使用规范
-
对 单数据节点,MySQL 数据库,100% 兼容
-
对 多数据节点
- 不支持 CASE WHEN、HAVING、UNION
- 不支持 一层以上的 SQL 子查找
- 不支持 子查找中使用 聚合函数
- 不支持 VALUES 语句使用 运算表达式
- 不支持 INSERT .. SELECT 语句
- 不支持 SQL 语句包含 schema,应该只使用 逻辑表 作代称
- 不支持 聚合函数中使用 DISTINCT
- 当 分片键 处于 表达式 或 函数中 时,将采用全库路由方式获取结果
/** 不支持的 SQL 范例 **/ /* 不支持 一层以上的 SQL 子查找 */ SELECT COUNT(*) FROM (SELECT * FROM b_order o WHERE o.id IN (SELECT id FROM b_order WHERE status = ?)) /* 不支持 VALUES 语句使用 运算表达式 */ INSERT INTO tbl_name (col1, col2, …) VALUES(1+2, ?, …) /* 不支持 INSERT .. SELECT 语句 */ INSERT INTO tbl_name (col1, col2, …) SELECT col1, col2, … FROM tbl_name WHERE col3 = ? /* 不支持 使用 HAVING */ SELECT COUNT(col1) as count_alias FROM tbl_name GROUP BY col1 HAVING count_alias > ? /* 不支持 使用 UNION */ SELECT * FROM tbl_name1 UNION SELECT * FROM tbl_name2 /* 不支持 使用 UNION ALL */ SELECT * FROM tbl_name1 UNION ALL SELECT * FROM tbl_name2 /* 不支持 SQL 语句包含 schema 应该只使用 逻辑表作代称 */ SELECT * FROM ds.tbl_name1 /* 不支持 聚合函数中使用 DISTINCT */ SELECT SUM(DISTINCT col1) FROM tbl_name /* create_time 为分片键,将导致全路由查找*/ SELECT * FROM tbl_name WHERE to_date(create_time, ‘yyyy-mm-dd’) = ?
5. 行表达式
-
$->{begin..end} : 区间范围
-
$->{[unit1, unit2, unit_x]} : 枚举值
/* 解析前*/ $->{['online', 'offline']}_table$->{1..3} /* 解析后 */ online_table1, online_table2, online_table3, offline_table1, offline_table2, offline_table3 /* 解析前 */ db$->{0..1}.b_order$->{1..2} /* 解析后 */ db0 ├── b_order2 └── b_order1 db1 ├── b_order2 └── b_order1
-
使用范例
# Sharding JDBC sharding: jdbc: # 数据源 datasource: names: master0,master1,slave0,slave1,slave2,slave3 master0: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://104.155.227.89:3306/lagou?useUnicode=true&characterEncoding=utf-8&useSSL=false username: waiting password: myPassword type: com.alibaba.druid.pool.DruidDataSource master1: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://104.155.221.108:3306/lagou?useUnicode=true&characterEncoding=utf-8&useSSL=false username: waiting password: myPassword type: com.alibaba.druid.pool.DruidDataSource slave0: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://35.201.232.138:3306/lagou?useUnicode=true&characterEncoding=utf-8&useSSL=false username: waiting password: myPassword type: com.alibaba.druid.pool.DruidDataSource slave1: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://34.80.141.108:3306/lagou?useUnicode=true&characterEncoding=utf-8&useSSL=false username: waiting password: myPassword type: com.alibaba.druid.pool.DruidDataSource slave2: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://35.241.76.18:3306/lagou?useUnicode=true&characterEncoding=utf-8&useSSL=false username: waiting password: myPassword type: com.alibaba.druid.pool.DruidDataSource slave3: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://34.92.0.0:3306/lagou?useUnicode=true&characterEncoding=utf-8&useSSL=false username: waiting password: myPassword type: com.alibaba.druid.pool.DruidDataSource config: sharding: # 读写分离规则 master-slave-rules: ms0: master-data-source-name: master0 slave-data-source-names: slave0,slave1 ms1: master-data-source-name: master1 slave-data-source-names: slave2,slave3 # 表策略 tables: c_order: # 数据节点 actual-data-nodes: ms$->{0..1}.c_order # 分库策略 database-strategy: inline: algorithm-expression: ms$->{user_id % 2} sharding-column: user_id # 自增主键字段 key-generator-column-name: id # 单库读写分离 # masterslave: # name: ms # master-data-source-name: master1 # slave-data-source-names: slave0,slave1 # load-balance-algorithm-type: round_robin
四、分布式事务规范
1. XA 强一致性
两阶段提交协议(2PC) 的实现
1. 角色
- 应用 AP : 应用进程 - Java
- 事务管理器 TM : 负责 协调、管理 事务
- 资源管理器 RM : 数据库 - MySQL
2. 流程
-
AP 向 TM 请求 开启全局事务
-
AP 向 TM 注册 资源管理器A
-
AP 将 SQL 语句提交给 资源管理器A
-
AP 向 TM 注册 资源管理器B
-
AP 将 SQL 语句提交给 资源管理器B
-
AP 向 TM 告知需要提交事务
-
TM 向 资源管理器A 发送 Prepare 消息,运行本地事务,但不提交 - 第一阶段
-
TM 向 资源管理器B 发送 Prepare 消息,运行本地事务,但不提交 - 第一阶段
-
TM 向 资源管理器A 发送 Commit 消息,提交事务 - 第二阶段
-
TM 向 资源管理器B 发送 Commit 消息,提交事务 - 第二阶段
3. 缺点
-
同步阻塞 - 在一阶段未达二阶段时,RM 事务都处于阻塞状态
-
单点问题 - 如果 TM 在一阶段后,运行二阶段前崩溃,参与者事务将处于锁定状态
-
数据不一致 - TM 在尚未发送完全部的 Commit 消息就崩溃,将导致数据不一致
-
过于保守 - 任一节点失败,将导致整个事务失败
2. TCC 最终一致性
1. 概念
- 两阶段提交协议(2PC) 的实现,2PC 是面向单一功能,而非整体
- TCC 可以视为是多个 2PC 的集合,故虽然 2PC 是强一致性,但整体 TCC 却是最终一致性
2. 阶段
-
Try : 检查、预留 业务资源
-
Confirm : 确定运行 业务操作
-
Cancel : 取消运行 业务操作
○ 运行 反向业务逻辑
○ 反向业务逻辑 : 库存 -2 → 库存 +2,订单状态 UPDATING → 订单状态 CANCLED
3. 流程
-
应用进程 调用 服务A 的 Try 接口,运行本地事务但不提交
-
如果 服务A 的 Try 接口运行成功,则由 事务协调器 运行 服务A 的 Confirm 接口,提交事务
-
应用进程 调用 服务B 的 Try 接口,运行本地事务但不提交
-
如果 服务B 的 Try 接口运行成功,则由 事务协调器 运行 服务B 的 Confirm 接口,提交事务
○ 如果 服务B 的 Try 接口运行失败,则调用 服务A 的 Cancel 接口,回复成运行前的状态
○ 如果 Confirm 接口运行失败,不会调用 Cancel 接口,而是会不断重试,故接口需保证幂等性
4. 优缺点
- 优点
- 阻塞范围变小 - 只阻塞单一功能的范围,不像 XA 那样阻塞整体事务
- 缺点
- 需由业务层控制 补偿机制(Cancel),较为麻烦
3. 消息队列 最终一致性
1. 概念
- 将分布式事务拆分成多个本地事务,并由消息队列进行协调
2. 流程
-
订单服务 添加订单纪录
-
订单服务 向 MQ 发送 减少库存的消息
-
库存服务 收到减少库存的消息
-
库存服务 减少库存
-
库存服务 向 MQ 发送 减少库存成功的消息
-
订单服务 收到库存减少成功的消息
-
下单事务 完成
如果 订单服务 一直没有收到 库存减少成功 的消息,则会不断向 MQ 发送消息,直到成功扣减库存
4. Saga 最终一致性
Saga 的 向后恢复模式 与 TCC 类似,可以视为是没有 Try 阶段的 TCC,但也因此在高并发情况下,可能导致脏读问题
1. 补偿模式
- 向前恢复 : 如果发生错误则进行重试 - 适用 必须要成功 的场景
- 向后恢复 : 如果发生错误则补偿之前所有的操作