ElIauk

MySQL 分库分表入门与 ShardingSphere

什么时候才轮到分库分表 单表数据量涨到几千万,查询开始变慢,很多人的第一反应是“上分库分表”。但在动手之前,先确认几件事:索引是否合理、有没有大字段拖累、历史数据能不能归档、读写分离能不能扛住。分库分表是最后手段,不是性能优化的起点。 一个粗略的判断是:单表行数超过千万、并且以每年千万级增长;单库连

评论
MySQL 分库分表入门与 ShardingSphere

什么时候才轮到分库分表

单表数据量涨到几千万,查询开始变慢,很多人的第一反应是“上分库分表”。但在动手之前,先确认几件事:索引是否合理、有没有大字段拖累、历史数据能不能归档、读写分离能不能扛住。分库分表是最后手段,不是性能优化的起点。

一个粗略的判断是:单表行数超过千万、并且以每年千万级增长;单库连接数、磁盘 IO 或写入 TPS 已经接近上限;业务查询天然带分片键。三个条件同时成立,才值得引入 ShardingSphere。否则,加索引、归档、缓存、读写分离的收益更高,复杂度却低得多。

垂直拆还是水平拆

垂直拆分两种:按业务把不同表拆到不同库,比如订单库、用户库;或者把宽表里的大字段、低频字段拆到扩展表。它解决的是“表太宽、库太杂”,不解决单表行数问题。

水平拆是把同一张表按规则拆成多个结构相同的分片,比如 t_order_0 到 t_order_3。核心是分片键的选择:基数要高、分布要均匀、大多数查询要能带上它。订单表常用 user_id 或 order_id,前者方便查“我的订单”,后者方便按订单号查详情。如果两种查询都很多,可以用基因法,把用户 ID 的尾部编进订单 ID,让两个维度都能路由到同一个分片。

分片键一旦选定,后续扩容会很痛苦。提前规划分片数量,比如先分 16 个库、每个库 16 张表,而不是一开始只分 2 个库,等数据涨了再迁移。

ShardingSphere 5.5 配置

Spring Boot 3.2 项目引入 org.apache.shardingsphere:shardingsphere-jdbc:5.5.0。从 5.3 开始,官方已经移除 spring.shardingsphere.* 的 Starter 配置方式,网上很多旧教程还在用,照抄会直接启动失败。现在推荐让 ShardingSphere 接管 DataSource,通过 JDBC URL 加载 YAML:

spring:
  datasource:
    driver-class-name: org.apache.shardingsphere.driver.ShardingSphereDriver
    url: jdbc:shardingsphere:classpath:sharding.yaml

sharding.yaml 放在 src/main/resources 下,数据源和分片规则写在同一个文件里:

dataSources:
  ds_0:
    dataSourceClassName: com.zaxxer.hikari.HikariDataSource
    driverClassName: com.mysql.cj.jdbc.Driver
    jdbcUrl: jdbc:mysql://127.0.0.1:3306/order_db_0?useSSL=false
    username: root
    password: root
  ds_1:
    dataSourceClassName: com.zaxxer.hikari.HikariDataSource
    driverClassName: com.mysql.cj.jdbc.Driver
    jdbcUrl: jdbc:mysql://127.0.0.1:3306/order_db_1?useSSL=false
    username: root
    password: root

rules:
  - !SHARDING
    tables:
      t_order:
        actualDataNodes: ds_${0..1}.t_order_${0..1}
        databaseStrategy:
          standard:
            shardingColumn: user_id
            shardingAlgorithmName: order-db-inline
        tableStrategy:
          standard:
            shardingColumn: user_id
            shardingAlgorithmName: order-table-inline
    keyGenerateStrategies:
      t_order_order_id:
        keyGenerateType: column
        keyGeneratorName: snowflake
        logicTable: t_order
        keyGenerateColumn: order_id
    shardingAlgorithms:
      order-db-inline:
        type: INLINE
        props:
          algorithm-expression: ds_${user_id % 2}
      order-table-inline:
        type: INLINE
        props:
          algorithm-expression: t_order_${user_id % 2}
    keyGenerators:
      snowflake:
        type: SNOWFLAKE

props:
  sql-show: true

sql-show 只建议在开发和预发打开,它会打印逻辑 SQL 和路由后的真实 SQL,排查分片规则时非常有用。生产环境打开会刷大量日志,还可能带出参数敏感信息。

分片之后的新问题

跨分片查询是最先遇到的。SELECT * FROM t_order WHERE status = 'PAID' 没有分片键,ShardingSphere 只能广播到所有分片,再在内存里归并排序;分片越多,放大越明显。COUNT(*)、GROUP BY、ORDER BY 加 LIMIT 都有类似问题,深分页尤其严重。

分布式事务是第二个坑。跨库写要么用 XA,强一致但性能差;要么用本地消息表、Seata AT 这类最终一致方案。最好的办法还是设计上避免跨分片事务:把同一业务聚合根的数据放在同一个分片,支付和订单的联动通过消息而不是跨库事务完成。

全局唯一 ID 也要重新设计。AUTO_INCREMENT 在多个分片里会重复,ShardingSphere 内置雪花算法可以生成 order_id,但要注意时钟回拨和机器号分配。业务上如果有“按订单号查”的需求,还要保证订单号能反解出分片键。

常见坑

1. 分片键和查询条件不匹配

按 order_id 分片,但 90% 的查询是按 user_id 查列表,结果每次都是全路由。分片键要按主要查询模式选,不能只看主键。

2. 直接上分库分表,没做归档

历史数据占了大半,分片后每个分片仍然很大。先归档冷数据,再分片,能少走很多弯路。

3. 忽略 SQL 兼容性

ShardingSphere 对子查询、UNION、部分函数和跨分片 JOIN 的支持有限。上线前把核心 SQL 跑一遍,看日志里的真实执行计划,不要假设“单库能跑,分片也能跑”。

4. 分布式事务滥用

每个写操作都包 XA,连接持有时间变长,吞吐骤降。能本地事务解决的就本地解决,跨分片的部分用消息补偿。

5. 扩容没有方案

从 2 个库扩到 4 个库,取模规则变化,历史数据要重新分布。提前用一致性哈希或成倍扩容,并准备好双写、校验、切流的数据迁移脚本。

排查步骤

分片路由不对时,先在 sharding.yaml 的 props 里把 sql-show 设为 true,确认逻辑 SQL 有没有带分片键。

日志里会看到 Logic SQL 和 Actual SQL,如果 Actual SQL 是多条、命中了所有分片,说明发生了广播。再看分片算法表达式,用具体参数手算一遍路由结果。需要强制走某个分片时,可以用 HintManager:

try (HintManager hintManager = HintManager.getInstance()) {
    hintManager.addDatabaseShardingValue("t_order", 1L);
    hintManager.addTableShardingValue("t_order", 1L);
    return orderMapper.selectById(orderId);
}

最后到真实分片库上执行 EXPLAIN,确认索引没有被分片改写破坏。

小结

分库分表解决的是“单库单表容量和写入上限”,代价是查询、事务、ID、运维复杂度全面上升。先用尽索引、归档、缓存、读写分离这些低成本手段,再考虑 ShardingSphere。分片键设计得好,后面的坑少一半。

参考

评论

推荐阅读

订阅邮件

订阅更新,及时收到最新文章。

作者