【数据库】MySQL 实战精通系列 · 第10篇:分库分表与分布式事务实战

MySQL 实战精通系列 · 第10篇:分库分表与分布式事务实战

本篇目标:单机扛不住、数据量太大、跨库怎么保证一致------从分片策略到分布式事务,系统化掌握 MySQL 水平扩展方案。


文章目录

  • [MySQL 实战精通系列 · 第10篇:分库分表与分布式事务实战](#MySQL 实战精通系列 · 第10篇:分库分表与分布式事务实战)
    • 一、什么时候需要分库分表
      • [1.1 单机瓶颈的三个信号](#1.1 单机瓶颈的三个信号)
      • [1.2 一张图看懂决策顺序](#1.2 一张图看懂决策顺序)
    • [二、分片策略:垂直 vs 水平](#二、分片策略:垂直 vs 水平)
      • [2.1 两种拆分方式](#2.1 两种拆分方式)
      • [2.2 垂直拆分:按业务分库](#2.2 垂直拆分:按业务分库)
      • [2.3 水平拆分:按数据行分表](#2.3 水平拆分:按数据行分表)
    • 三、分片算法
      • [3.1 常用分片算法对比](#3.1 常用分片算法对比)
      • [3.2 Range 分片](#3.2 Range 分片)
      • [3.3 Hash 分片](#3.3 Hash 分片)
      • [3.4 雪花算法:全局唯一 ID](#3.4 雪花算法:全局唯一 ID)
    • 四、分库分表中间件
      • [4.1 两种技术路线](#4.1 两种技术路线)
      • [4.2 ShardingSphere-JDBC 配置示例](#4.2 ShardingSphere-JDBC 配置示例)
      • [4.3 分片后的 SQL 限制](#4.3 分片后的 SQL 限制)
    • 五、分库分表实战:订单表拆分
      • [5.1 需求分析](#5.1 需求分析)
      • [5.2 分片键选择](#5.2 分片键选择)
      • [5.3 建表 SQL](#5.3 建表 SQL)
      • [5.4 路由计算](#5.4 路由计算)
      • [5.5 跨分片分页查询](#5.5 跨分片分页查询)
    • 六、分布式事务
      • [6.1 为什么需要分布式事务](#6.1 为什么需要分布式事务)
      • [6.2 分布式事务方案对比](#6.2 分布式事务方案对比)
      • [6.3 Seata AT 模式实战](#6.3 Seata AT 模式实战)
      • [6.4 本地消息表:最终一致方案](#6.4 本地消息表:最终一致方案)
    • 七、扩容方案
      • [7.1 为什么要考虑扩容](#7.1 为什么要考虑扩容)
      • [7.2 扩容方案对比](#7.2 扩容方案对比)
      • [7.3 双写迁移实战](#7.3 双写迁移实战)
    • 八、实战任务
    • 九、本篇小结

一、什么时候需要分库分表

1.1 单机瓶颈的三个信号

复制代码
单机 MySQL 扛不住的信号
│
├── ① 数据量太大        ← 单表 > 2000万行,B+树层高增加,查询变慢
├── ② 写入压力太大      ← 单机 TPS 到瓶颈,主库 CPU 打满
├── ③ 磁盘空间不够      ← 单机磁盘容量到上限

原则:能不拆就不拆。分库分表是最后手段,优先考虑读写分离、索引优化、归档冷数据。

1.2 一张图看懂决策顺序

复制代码
数据库扛不住了
   │
   ↓
① SQL 和索引优化了吗?
   │  没有 → 先优化 SQL 和索引
   │
   ↓ 优化了
② 读写分离做了吗?
   │  没有 → 先做读写分离,读压力分摊
   │
   ↓ 做了
③ 冷数据归档了吗?
   │  没有 → 先归档历史数据,减少单表体积
   │
   ↓ 归档了
④ 还是扛不住?
   │  是 → 分库分表

二、分片策略:垂直 vs 水平

2.1 两种拆分方式

维度 垂直拆分 水平拆分
拆分对象 按业务/字段拆 按数据行拆
典型场景 用户库、订单库、商品库分离 订单表拆成 orders_0~orders_9
优点 业务解耦、专库专用 单表数据量可控
缺点 跨库 JOIN 困难 分片键选择困难
复杂度 低 高

2.2 垂直拆分:按业务分库

复制代码
拆分前:
  shop 库(所有表在一起)

拆分后:
  shop_user 库    → user, address
  shop_order 库   → orders, order_items, payment
  shop_product 库 → product, category

垂直拆分原则:

复制代码
① 按业务关联度拆:强关联的表放一起
② 按访问频率拆:高频表和低频表分离
③ 按数据量拆:大表独立
④ 避免跨库 JOIN:能冗余就冗余

2.3 水平拆分:按数据行分表

复制代码
拆分前:
  orders 表(5000万行)

拆分后:
  orders_0 表(500万行)
  orders_1 表(500万行)
  ...
  orders_9 表(500万行)

分片键选择:

分片键 优点 缺点
user_id 同一用户订单集中 商家维度查询跨片
order_id 分布均匀 用户维度查询跨片
create_time 便于归档 热点数据集中
基因法 兼顾多维度 实现复杂

基因法示例:订单 ID 中嵌入 user_id 的后几位,使 order_id 和 user_id 都能定位到同一分片。


三、分片算法

3.1 常用分片算法对比

算法 原理 优点 缺点
Range 按范围分(id 1-100万 → 分片0) 扩容方便 热点集中
Hash hash(key) % N 分布均匀 扩容需 rehash
一致性 Hash 环上取模 扩容影响小 实现复杂
雪花算法 生成全局唯一 ID 趋势递增 依赖时钟

3.2 Range 分片

sql 复制代码
-- 按 order_id 范围分片
orders_0: order_id 1 ~ 10000000
orders_1: order_id 10000001 ~ 20000000
orders_2: order_id 20000001 ~ 30000000

适用 :订单、日志等有时间顺序的数据。

问题:新数据集中写入最后一个分片,造成热点。

3.3 Hash 分片

sql 复制代码
-- 按 user_id 取模分片
分片号 = user_id % 10

适用 :用户维度查询为主的场景。

问题:扩容时需要数据迁移(10 片扩到 20 片,几乎所有数据都要动)。

3.4 雪花算法:全局唯一 ID

复制代码
Snowflake ID 结构(64 位)
│
├── 1 位符号位(固定 0)
├── 41 位时间戳(毫秒级,可用 69 年)
├── 10 位机器 ID(1024 台机器)
└── 12 位序列号(每毫秒 4096 个 ID)

特点:
  ① 全局唯一
  ② 趋势递增(利于 B+ 树索引)
  ③ 本地生成,无网络开销
  ④ 每秒可生成 400 万+ ID

为什么不用自增 ID:

复制代码
单库自增:分库后 ID 会冲突
UUID:无序,作为主键会导致 B+ 树频繁分裂
雪花算法:有序 + 唯一 + 高性能

四、分库分表中间件

4.1 两种技术路线

路线 代表 原理 优点 缺点
客户端分片 ShardingSphere-JDBC 应用层计算路由 性能好、无额外部署 应用侵入、多语言支持差
代理分片 ShardingSphere-Proxy、MyCat 独立代理层 应用透明、多语言 多一跳、代理需高可用

4.2 ShardingSphere-JDBC 配置示例

yaml 复制代码
spring:
  shardingsphere:
    datasource:
      names: ds0,ds1
      ds0:
        type: com.zaxxer.hikari.HikariDataSource
        jdbc-url: jdbc:mysql://127.0.0.1:3306/shop_0
        username: root
        password: root123
      ds1:
        type: com.zaxxer.hikari.HikariDataSource
        jdbc-url: jdbc:mysql://127.0.0.1:3306/shop_1
        username: root
        password: root123
    rules:
      sharding:
        tables:
          orders:
            actual-data-nodes: ds$->{0..1}.orders_$->{0..1}
            table-strategy:
              standard:
                sharding-column: user_id
                sharding-algorithm-name: orders-inline
            key-generate-strategy:
              column: order_id
              key-generator-name: snowflake
        sharding-algorithms:
          orders-inline:
            type: INLINE
            props:
              algorithm-expression: orders_$->{user_id % 2}

4.3 分片后的 SQL 限制

复制代码
分库分表后不能用的 SQL:
  ✗ 跨库 JOIN
  ✗ 跨库事务
  ✗ 不包含分片键的查询(会广播到所有分片)
  ✗ 跨库 ORDER BY + LIMIT
  ✗ 跨库 COUNT(*)、GROUP BY
  ✗ 跨库 UPDATE/DELETE

解决方案:

问题 方案
跨库 JOIN 冗余字段、宽表、ES 检索
跨库分页 各分片查 N 条,内存归并
跨库 COUNT 各分片求和、维护计数表
跨库事务 分布式事务(见第六节)

五、分库分表实战:订单表拆分

5.1 需求分析

复制代码
订单表 orders
├── 当前数据量:5000 万行
├── 日增:50 万行
├── 查询模式:
│   ├── 用户查自己的订单(user_id 维度)80%
│   ├── 商家查店铺订单(shop_id 维度)15%
│   └── 运营查全量订单(跨维度)5%
└── 目标:拆成 4 库 × 4 表 = 16 片

5.2 分片键选择

复制代码
主分片键:user_id(覆盖 80% 查询)
基因法:将 shop_id 后 4 位嵌入 order_id
  → 商家查询时,通过 shop_id 反推分片

5.3 建表 SQL

sql 复制代码
-- 在每个分片库中执行
CREATE TABLE orders_0 (
    order_id BIGINT NOT NULL,
    user_id BIGINT NOT NULL,
    shop_id BIGINT NOT NULL,
    total_amount DECIMAL(10,2) NOT NULL,
    status TINYINT NOT NULL,
    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (order_id),
    KEY idx_user_created (user_id, created_at),
    KEY idx_shop_created (shop_id, created_at)
) ENGINE=InnoDB;

-- orders_1 ~ orders_15 结构相同

5.4 路由计算

java 复制代码
// 用户维度查询
int dbIndex = userId % 4;
int tableIndex = userId % 4;

// 商家维度查询(基因法)
long gene = shopId & 0xF;  // 取后4位
int shopDbIndex = (int) (gene % 4);
int shopTableIndex = (int) (gene % 4);

5.5 跨分片分页查询

sql 复制代码
-- 需求:查询第 100 页,每页 20 条
-- 错误做法:每个分片查 LIMIT 2000, 20 → 结果不对
-- 正确做法:每个分片查 LIMIT 0, 2020,内存归并后取 2000-2020

-- 第1步:各分片查询
SELECT * FROM orders_0 ORDER BY created_at DESC LIMIT 2020;
SELECT * FROM orders_1 ORDER BY created_at DESC LIMIT 2020;
-- ... 所有分片

-- 第2步:内存归并排序,取第 2000-2020 条

优化:使用游标分页(记录上一页最后一条的 created_at + order_id),避免深度分页。


六、分布式事务

6.1 为什么需要分布式事务

复制代码
场景:用户下单
  ① 订单库:创建订单
  ② 库存库:扣减库存
  ③ 账户库:扣减余额

问题:步骤 ② 失败,步骤 ① 已提交,数据不一致

6.2 分布式事务方案对比

方案 一致性 性能 复杂度 适用场景
2PC/XA 强一致 差 中 传统金融
TCC 强一致 中 高 支付、交易
本地消息表 最终一致 好 中 订单、通知
事务消息 最终一致 好 中 异步解耦
Seata AT 强一致 中 低 通用场景
Saga 最终一致 好 中 长流程

6.3 Seata AT 模式实战

核心角色:

复制代码
Seata 三大角色
│
├── TC (Transaction Coordinator)   ← 事务协调器,独立部署
├── TM (Transaction Manager)       ← 事务管理器,发起方
└── RM (Resource Manager)          ← 资源管理器,各数据库

AT 模式原理:

复制代码
① 业务 SQL 执行前,记录 before image
② 执行业务 SQL
③ 执行后,记录 after image
④ 注册分支事务到 TC
⑤ 全局提交 → 删除 undo log
⑥ 全局回滚 → 用 undo log 反向补偿

代码示例:

java 复制代码
@GlobalTransactional
public void createOrder(OrderDTO order) {
    // ① 订单库:创建订单
    orderMapper.insert(order);
    
    // ② 库存库:扣减库存
    inventoryService.deduct(order.getProductId(), order.getQuantity());
    
    // ③ 账户库:扣减余额
    accountService.deduct(order.getUserId(), order.getTotalAmount());
}

6.4 本地消息表:最终一致方案

复制代码
本地消息表方案
│
├── ① 业务库:订单创建 + 消息写入(同一本地事务)
├── ② 定时任务:扫描未发送消息
├── ③ 发送消息到 MQ
├── ④ 下游消费消息,执行库存扣减
└── ⑤ 消费成功,更新消息状态

建表 SQL:

sql 复制代码
CREATE TABLE local_message (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    biz_type VARCHAR(50) NOT NULL,
    biz_id BIGINT NOT NULL,
    payload JSON NOT NULL,
    status TINYINT NOT NULL DEFAULT 0,  -- 0待发送 1已发送 2已消费
    retry_count INT NOT NULL DEFAULT 0,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    KEY idx_status_created (status, created_at)
);

七、扩容方案

7.1 为什么要考虑扩容

复制代码
初始:4 库 × 4 表 = 16 片
增长:数据量翻倍,需要扩到 8 库 × 8 表 = 64 片

问题:hash(user_id) % 4 变成 % 8,几乎所有数据都要迁移

7.2 扩容方案对比

方案 原理 优点 缺点
停机扩容 停服迁移 简单 业务中断
双写迁移 新旧库同时写 平滑 实现复杂
一致性 Hash 环上迁移 影响小 实现复杂
倍增扩容 4→8,只迁一半 迁移量小 需规划

7.3 双写迁移实战

复制代码
双写迁移步骤
│
├── ① 新库建好,结构一致
├── ② 应用双写:同时写旧库和新库
├── ③ 数据同步:将旧库存量数据迁移到新库
├── ④ 数据校验:对比新旧库数据一致性
├── ⑤ 读切换:读流量逐步切到新库
├── ⑥ 停写旧库:确认无误后,停止双写
└── ⑦ 下线旧库

八、实战任务

任务清单

  • 设计订单表的分片方案(分片键 + 算法)
  • 搭建 ShardingSphere-JDBC 环境
  • 实现订单表的水平分片
  • 实现跨分片分页查询
  • 用 Seata 实现下单分布式事务
  • 用本地消息表实现最终一致
  • 模拟扩容:4 库扩到 8 库

自检问题

  1. 什么时候需要分库分表?决策顺序是什么?
  2. 垂直拆分和水平拆分的区别?
  3. 常用分片算法有哪些?各自的优缺点?
  4. 雪花算法的结构是什么?为什么不用自增 ID?
  5. 分库分表后哪些 SQL 不能用?怎么解决?
  6. 分布式事务有哪些方案?各自适用什么场景?
  7. Seata AT 模式的原理是什么?
  8. 扩容有哪些方案?双写迁移的步骤?

九、本篇小结

复制代码
第10篇 核心收获
│
├── 分片决策
│   ├── 先优化 SQL 和索引
│   ├── 再读写分离
│   ├── 再冷数据归档
│   └── 最后才分库分表
│
├── 分片方式
│   ├── 垂直拆分:按业务分库
│   └── 水平拆分:按数据行分表
│
├── 分片算法
│   ├── Range:范围分片,扩容方便,热点集中
│   ├── Hash:分布均匀,扩容需 rehash
│   └── 雪花算法:全局唯一 ID,趋势递增
│
├── 中间件
│   ├── ShardingSphere-JDBC:客户端分片
│   └── ShardingSphere-Proxy:代理分片
│
├── 分片后限制
│   ├── 跨库 JOIN → 冗余字段/宽表
│   ├── 跨库分页 → 内存归并
│   ├── 跨库 COUNT → 分片求和
│   └── 跨库事务 → 分布式事务
│
├── 分布式事务
│   ├── 2PC/XA:强一致,性能差
│   ├── TCC:强一致,复杂度高
│   ├── Seata AT:强一致,通用
│   ├── 本地消息表:最终一致
│   └── 事务消息:最终一致
│
└── 扩容方案
    ├── 双写迁移:平滑扩容
    ├── 一致性 Hash:影响小
    └── 倍增扩容:迁移量小

下一篇:第11篇《Redis 缓存与 MySQL 一致性实战》

相关推荐
我叫洋洋2 小时前
Cadence CIS 元器件库合并实战:3000+ 焊盘、156 个符号零冲突并入自有库
数据库·单片机·嵌入式硬件·oracle·电路
AI 编程助手GPT2 小时前
GPT-6 Luna (Batch) 批量处理性能与质量深度评测
数据库·gpt·batch
quweiie2 小时前
Neo4j Community 安全访问配置总结
数据库·centos·neo4j
꯭自꯭闭꯭3 小时前
DM7主备升级方案
linux·服务器·数据库
蜗牛互联网3 小时前
MongoDB Atlas Agent Engine之后,如何用版本门禁防止陈旧写入
java·数据库·人工智能·后端·mongodb
IvorySQL4 小时前
PostgreSQL 日报 |PostgreSQL 19 Beta 4 发布(9 月 25 日)
数据库·postgresql
梦帮科技4 小时前
vLLM / TensorRT-LLM 极限推理:PagedAttention 细粒度物理页表管理与连续批处理(Continuous Batching)实战
数据结构·人工智能·分布式·python·深度学习·算法·vllm
科技研学社4 小时前
绿色合规时代,碳足迹与ESG倒逼服装工厂工序升级
数据库·人工智能
赵渝强老师5 小时前
【赵渝强老师】崖山数据库的数据库文件
数据库·国产数据库·yashandb·崖山数据库