SQL 分库分表策略与实践案例

1次阅读

分库分表前必须确认三件事:单库单表是否真成瓶颈、写入qps超3000且主从延迟>5s才考虑分库、单表行数超5000万且查询无法稳定走索引才考虑分表;无全局唯一id不得分表;sharding-key须高覆盖率+高区分度,忌低基数或时间字段滥用;中间件配置须严谨,跨分片聚合需预计算或业务妥协。

SQL 分库分表策略与实践案例

分库分表前必须确认的三件事

不加判断直接分库分表,90% 会把问题变得更糟。先问清楚:单库单表是否真成了瓶颈?很多所谓“慢”,其实是缺少索引未走覆盖索引、或应用层 N+1 查询导致的,和数据量关系不大。

确认真实瓶颈的方法只有两个:EXPLAIN 看执行计划 + slow_query_log 抓真实慢 sql;同时观察数据库Threads_runningInnodb_row_lock_waits 是否持续偏高。如果锁等待多、连接数满、但磁盘 I/O 和 CPU 都没打满,大概率是设计或查询问题,不是分片问题。

  • 业务写入 QPS 超过 3000 且持续增长,单实例主从同步延迟 > 5s —— 才值得考虑分库
  • 单表行数 > 5000 万,且 WHERE 条件无法稳定命中索引(比如总带 OR 或函数)—— 才值得考虑分表
  • 没有全局唯一 ID 生成机制(如 UUID_SHORT()Twitter Snowflake 或自建号段服务),别碰分表

sharding-key 选错等于白干

分片键(sharding key)不是“哪个字段用得最多就选哪个”,而是“哪个字段能保证 90% 以上查询都带它、且分布足够离散”。常见错误是选 user_id 却忽略“大 V 用户”导致热点;或者选 create_time 导致新数据全挤在少数节点上。

真正靠谱的策略是:先统计高频查询的 WHERE 条件组合,再看哪些字段具备高区分度 + 高覆盖率。比如电商订单表,order_id 全局唯一但业务查询极少用它查;而 buyer_id 查询频次高,但存在头部用户占 20% 订单的情况 —— 这时就得加一层哈希(如 MOD(buyer_id, 1024))再分片,避免倾斜。

  • 绝对不要用 statustype 这类低基数字段做分片键
  • 时间类字段慎用范围分片(range sharding),除非你明确只查最近 N 天,且能接受跨分片归并
  • 如果业务强依赖 JOIN 或跨用户查(如后台报表),说明分片键设计失败,得退回去重想

MyCat / ShardingSphere 的配置陷阱

这两个中间件看着配置简单,但几个关键参数一设错,上线就出事。sql.show 必须开,否则连路由日志都看不到;props.sql-show 在 ShardingSphere 5.x 后改名了,设错就等于盲操作。

最常踩的坑是 default-database-strategydefault-table-strategy 没配一致,导致有的 SQL 路由到 A 库 A 表,有的却去了 A 库 B 表;还有就是 master-slave 配置里漏掉 load-balance-algorithm-type: round_robin,结果所有读请求全压在第一个从库上。

  • sharding-column 名称必须和 SQL 中写的完全一致(大小写敏感),别写成 user_id 却在 SQL 里用 userId
  • ShardingSphere 的 spring.shardingsphere.props.sql-show=true 只在开发环境开,生产务必关,否则日志刷爆磁盘
  • MyCat 的 scheme.xmldataNodedataHost 必须一一对应,IP 写错一个,整个节点不可用

跨分片 count / ORDER BY 的现实解法

别信文档里“自动合并”的说法。真实场景下,select COUNT(*) FROM order WHERE status = 1 会发到所有分片,再在中间件里累加 —— 数据量大时,网络传输和内存聚合本身就成了瓶颈。更麻烦的是 ORDER BY create_time LIMIT 20,10:每个分片返回 30 条,中间件合并后取第 20–30 条,结果错得离谱。

可行方案只有两个:一是用异步任务定期预计算(比如每小时跑一次 INSERT INTO stat_order_count SELECT status, COUNT(*) ... GROUP BY status 到汇总库);二是业务妥协,比如把“最新 100 条订单”改成“最近 1 小时内最新 100 条”,用时间分片兜底。

  • 禁止在分片环境下用 DISTINCT + 多字段组合,去重逻辑中间件扛不住
  • union ALL 可以用,但必须确保每个子查询都带上分片键条件,否则变成广播查询
  • 如果业务真要精确分页,老老实实加 create_time + id 复合索引,用游标分页(WHERE create_time )

分库分表不是加个中间件就完事。最难的部分永远不在怎么切,而在怎么让业务接受“不能随便 JOIN、不能随便 COUNT、不能随便跨天查”。这些约束得在拆之前就和产品、前端对齐清楚,不然上线后改需求比重构还疼。

text=ZqhQzanResources