【Java劝退师】MySQL 知识脑图 - 分布式事务、分库分表 | 🏆 技术专题第五期征文

816 阅读9分钟

MySQL

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 哈希取模分库,保证用户数据均衡分散在库中

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. 流程

  1. AP 向 TM 请求 开启全局事务

  2. AP 向 TM 注册 资源管理器A

  3. AP 将 SQL 语句提交给 资源管理器A

  4. AP 向 TM 注册 资源管理器B

  5. AP 将 SQL 语句提交给 资源管理器B

  6. AP 向 TM 告知需要提交事务

  7. TM 向 资源管理器A 发送 Prepare 消息,运行本地事务,但不提交 - 第一阶段

  8. TM 向 资源管理器B 发送 Prepare 消息,运行本地事务,但不提交 - 第一阶段

  9. TM 向 资源管理器A 发送 Commit 消息,提交事务 - 第二阶段

  10. 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. 流程

  1. 应用进程 调用 服务A 的 Try 接口,运行本地事务但不提交

  2. 如果 服务A 的 Try 接口运行成功,则由 事务协调器 运行 服务A 的 Confirm 接口,提交事务

  3. 应用进程 调用 服务B 的 Try 接口,运行本地事务但不提交

  4. 如果 服务B 的 Try 接口运行成功,则由 事务协调器 运行 服务B 的 Confirm 接口,提交事务

    ○ 如果 服务B 的 Try 接口运行失败,则调用 服务A 的 Cancel 接口,回复成运行前的状态

    ○ 如果 Confirm 接口运行失败,不会调用 Cancel 接口,而是会不断重试,故接口需保证幂等性

4. 优缺点

  • 优点
    • 阻塞范围变小 - 只阻塞单一功能的范围,不像 XA 那样阻塞整体事务
  • 缺点
    • 需由业务层控制 补偿机制(Cancel),较为麻烦

3. 消息队列 最终一致性

1. 概念

  • 将分布式事务拆分成多个本地事务,并由消息队列进行协调

2. 流程

  1. 订单服务 添加订单纪录

  2. 订单服务 向 MQ 发送 减少库存的消息

  3. 库存服务 收到减少库存的消息

  4. 库存服务 减少库存

  5. 库存服务 向 MQ 发送 减少库存成功的消息

  6. 订单服务 收到库存减少成功的消息

  7. 下单事务 完成

    如果 订单服务 一直没有收到 库存减少成功 的消息,则会不断向 MQ 发送消息,直到成功扣减库存

4. Saga 最终一致性

Saga 的 向后恢复模式 与 TCC 类似,可以视为是没有 Try 阶段的 TCC,但也因此在高并发情况下,可能导致脏读问题

1. 补偿模式

  • 向前恢复 : 如果发生错误则进行重试 - 适用 必须要成功 的场景
  • 向后恢复 : 如果发生错误则补偿之前所有的操作

🏆 技术专题第五期 | 聊聊分布式的那些事......