分片键选user_id还是order_id?选错一次,80%查询跨库,加班三周全量回滚——分库分表血泪决策指南

21 阅读7分钟

分库分表实战:垂直/水平分片 + ShardingSphere 5.x

订单表破亿那一刻就知道——分表不是"拆成十张表"那么简单。最痛苦的决策不是选 ShardingSphere 还是 MyCat,而是选什么字段做分片键。选 user_id——按用户查订单飞快,但运营要"昨天的 Top 100 订单"——垮了,全库扫描。选 order_id——反过来。选错了键,80% 查询跨库,改回来全量数据迁移+停机窗口。这篇文章把我从选键→配置→扩容踩过的坑全记下来了,附一份分片键 4 原则决策清单。

阅读约 14 分钟 | 系列第 16/17 篇


一、为什么需要分库分表

1.1 单表的物理极限

MySQL 单表在 500 万行以下通常表现良好,超过 2000 万行后 B+Tree 层数增加、索引维护开销上升、DDL 操作(加字段/改索引)变得不可控。阿里的规范建议单表控制在 500 万以内,超过则考虑分表。

1.2 分库分表解决的核心问题

问题表现分库分表的解法
数据量过大索引层级增加,查询变慢水平分表——将一张表的数据分散到多张表
写入瓶颈单库连接数有限,写操作排队水平分库——写入分散到多个数据库实例
字段过多一张表上百字段,查询总带大量无用列垂直分片——按业务拆分为多张窄表

二、垂直分片 vs 水平分片

⚠️ 概念辨析:垂直分片与垂直拆分是两个容易混淆的概念。本文讨论的"垂直分片"指将不同表按业务模块拆分到不同库,不是指将一张表的列拆成多张表(那是"垂直拆分/分区")。

2.1 垂直分片

定义:将不同的表按照业务模块拆分到不同的数据库节点上。

垂直分片前(单库):
  order_db: orders | order_items | users | products

垂直分片后:
  order_db: orders | order_items     订单相关表
  user_db:  users                    用户相关表
  product_db: products               商品相关表

适用场景:不同业务表的访问频率和重要性差异大。交易表访问频繁需要高性能实例,日志表访问少可以用低成本实例——物理隔离让资源分配更精准。

2.2 水平分片

定义:将同一张表的数据行按照分片键拆分到多个数据库或多张表中。

水平分库分表:
  ds0: orders_0, orders_1    ← user_id % 2 = 0 的数据
  ds1: orders_0, orders_1    ← user_id % 2 = 1 的数据

分片键选择是水平分片最关键的设计决策——选错了整个架构都要推倒重来。

分片键优点缺点
user_id用户维度数据集中,单用户查询不跨库跨用户统计需要全库扫描
order_id(雪花算法)数据均匀分布按用户查订单需要路由到多个库
create_time按时间段查询效率高热点集中——所有写入压在最新日期对应的分片上

黄金法则:分片键 = 最高频查询的 WHERE 条件。如果 90% 的查询都带 user_id,用它做分片键就是正确的选择。


三、ShardingSphere 5.x 核心配置

3.1 ShardingSphere 是什么

Apache ShardingSphere 是一站式分布式数据库解决方案,核心组件:

组件定位原理
Sharding-JDBC轻量级 JDBC 增强拦截 SQL → 解析 → 路由 → 改写 → 执行 → 结果归并
Sharding-Proxy透明数据库代理作为 MySQL 协议的代理层,对客户端零侵入

选型建议:Java 技术栈项目直接用 Sharding-JDBC(零部署、性能最优);异构语言或需要对业务零侵入选 Sharding-Proxy。

3.2 5.x YAML 配置(关键变化)

ShardingSphere 5.x 的 YAML 语法相比 4.x 有重要变化。以下为水平分库分表的完整配置:

# ShardingSphere 5.x 水平分库分表配置
dataSources:
  ds0:
    url: jdbc:mysql://localhost:3306/order_db0
    username: root
    password: root
  ds1:
    url: jdbc:mysql://localhost:3306/order_db1
    username: root
    password: root

rules:
  - !SHARDING
    tables:
      t_order:
        actualDataNodes: ds$->{0..1}.t_order_$->{0..1}  # ds0.t_order_0, ds0.t_order_1, ds1.t_order_0, ds1.t_order_1
        databaseStrategy:
          standard:
            shardingColumn: user_id
            shardingAlgorithmName: database_inline
        tableStrategy:
          standard:
            shardingColumn: order_id
            shardingAlgorithmName: table_inline

    shardingAlgorithms:
      database_inline:
        type: INLINE
        props:
          algorithm-expression: ds$->{user_id % 2}
      table_inline:
        type: INLINE
        props:
          algorithm-expression: t_order_$->{order_id % 2}

3.3 读写分离配置

rules:
  - !READWRITE_SPLITTING
    dataSources:
      order_rw:
        writeDataSourceName: ds0
        readDataSourceNames:
          - ds0_read1
          - ds0_read2
        loadBalancerName: round_robin
    loadBalancers:
      round_robin:
        type: ROUND_ROBIN

强制走主库HintManager.getInstance().setWriteRouteOnly()——关键业务(如支付后立即查订单状态)必须从主库读,避免主从延迟导致"支付成功了但查询显示未支付"。


四、广播表与绑定表

4.1 广播表

定义:数据量小、变更少、但每个分片都需要的表。ShardingSphere 将这类表的数据复制到所有数据源中。

rules:
  - !SHARDING
    broadcastTables:
      - t_config      # 系统配置表
      - t_dict         # 字典表

适用:字典表、配置表、省市区表——不用 JOIN 跨库也能查到完整数据。

4.2 绑定表

定义:存在关联关系的主表和从表,ShardingSphere 确保它们的数据落在一个分片上,避免跨库 JOIN。

rules:
  - !SHARDING
    bindingTables:
      - t_order, t_order_item

工作原理t_ordert_order_item 都按 order_id 分片,ShardingSphere 保证 order_id=100 的订单和订单明细落在同一个数据库分片上。查询 SELECT * FROM t_order o JOIN t_order_item i ON o.id=i.order_id 时无需跨库。

⚠️ 5.x 语法变更:4.x 的 shardingTablesbindingTables 独立存在,5.x 改为 YAML tag 语法——!BROADCAST!BINDING。升级时注意适配。


五、分布式 ID 与分片算法

5.1 雪花算法(Snowflake)

// ShardingSphere 内置雪花算法
spring:
  shardingsphere:
    rules:
      sharding:
        keyGenerators:
          snowflake:
            type: SNOWFLAKE
            props:
              worker-id: 1

组成:1 位符号位 + 41 位时间戳 + 10 位工作机器 ID + 12 位序列号。每毫秒可生成 4096 个有序 ID,趋势递增不依赖数据库。

5.2 分片算法选择

算法原理适用优缺点
INLINEds$->{user_id % 2}简单取模配置简单,但扩容时所有数据需重分布
MOD取模分片均匀分布同上
HASH_MOD一致性 Hash动态扩缩容扩容时仅部分数据迁移
RANGE按范围分片按时间/ID范围可能导致数据倾斜

一致性 Hash 的核心价值:新增一个节点时,只有该节点"虚拟环"上相邻节点的部分数据需要迁移,而非全部数据重分布。这是动态扩容场景下的首选。


六、分布式事务

ShardingSphere 集成 Seata 实现分布式事务:

rules:
  - !SHARDING
    ...
  - !TRANSACTION
    defaultType: BASE
    providerType: Seata

Seata AT 模式:一阶段执行 SQL → 二阶段提交(删除 undo_log)/ 回滚(通过 undo_log 反向补偿)。核心理解:AT 模式的回滚不是"撤销 SQL",而是"执行反向 SQL 补偿"——INSERT 的回滚是 DELETEUPDATE 的回滚是反向 UPDATE

选择建议undo_log 表必须和业务表在同一个数据库,Seata Server 生产必须集群部署(否则单点故障导致全局事务不可用)。


七、分库分表的代价

代价说明缓解方案
跨分片查询不带分片键的查询需广播到所有分片强制查询带分片键,或用 ES 做搜索层
跨分片 JOINGROUP BY / ORDER BY 需在内存归并绑定表 + 应用层聚合
分布式事务跨库事务一致性复杂Seata AT/TCC + 事务消息
扩容困难新增节点需要数据迁移一致性 Hash + 平滑迁移工具
ID 全局唯一数据库自增 ID 不可用雪花算法 / 美团 Leaf

核心要点回顾

垂直分片与水平分片是两把不同尺寸的刀:垂直分片拆分不同的表到不同库(按业务模块),解决的是"不同业务隔离"的问题;水平分片拆分同一张表的数据行到多个库/表(按分片键),解决的是"单表数据量过大"的问题。真实项目通常是两者叠加——先按业务垂直分库,再对核心表水平分表。

ShardingSphere 5.x 在 4.x 基础上调整了 YAML 语法——广播表用 !BROADCAST tag、绑定表用 !BINDING tag,分片算法从 type: INLINE 配置。广播表将小表数据复制到所有分片,绑定表保证关联表数据落在同一分片避免跨库 JOIN。

分片键是水平分片最关键的决策——选择最高频查询的 WHERE 条件作为分片键,用时间做分片键是经典反模式(写入热点集中在一个分片上)。一致性 Hash 是支持动态扩容的最佳分片算法。


分库分表最难的从来不是技术实现——是分片键选型和扩容方案。收藏这份分片决策指南,表快到瓶颈时翻出来对照。

上一篇:《设计模式》 | 下一篇:《微服务治理六大支柱》 系列合集掘金Java合集