MySQL 分库分表:MyCat 水平分片踩坑实录
简历库主表增长到 2 亿行之后,单表已经跑不动了:写入锁竞争、索引膨胀、备份要几小时。我们选了 MyCat 做水平分库分表,中间踩了不少坑,这篇是完整记录。
一、为什么是 MyCat
当时(2019 年)的选项:
- ShardingSphere:功能全,但要改应用(或上 proxy),团队是 PHP 为主,改造成本高
- MyCat:proxy 模式,对应用透明——应用连 MyCat 就像连一个 MySQL,SQL 基本不用改
- TiDB:分布式数据库,但整体替换成本更高,团队没有 NewSQL 经验
我们的诉求是「存量系统平滑迁」,MyCat 的透明代理最合适。现在回头看,如果从零开始,我会直接选 TiDB/ShardingSphere-Proxy 这类演进性更好的方案——MyCat 对复杂的 SQL 支持有限,这是后话。
二、分片设计
分片键:用户 ID
简历场景所有核心查询都是「按用户/按候选人」维度,所以分片键用简历所属用户 ID,按 ID 取模 16 分片(4 库 × 4 表):
核心原则
- 所有查询必须带分片键:不带分片键的查询 MyCat 会广播到所有分片再聚合,慢到爆炸
- 全局唯一 ID 独立于分片键:用雪花算法(snowflake)生成业务 ID,不依赖自增主键
- 跨分片事务尽量规避:单条简历的所有操作都走同一用户 ID,天然落在同一分片
三、踩坑清单
坑 1:跨分片 JOIN 的假象
MyCat 支持跨分片 join(catlet/全局表),但性能是灾难。简历表和技能表都是分片表,跨分片 join 触发广播查询。
解法:冗余 + 反规范化。技能标签冗余进简历主表(JSON 列),业务侧保证一致性,查询零 join。
坑 2:分布式主键踩了自增的坑
早期图省事用了 MyCat 的全局自增(autoIncrement),结果重启后出现重复 ID。
解法:换雪花算法,在应用层生成 ID 写入。ID 里带时间戳,还能顺便做按时间范围排序。
坑 3:扩容 = 全量重分布
取模分片(% 16)的硬伤:加一个分片变成 % 20,所有数据都要重新分布。
解法(两阶段):
- 先按
% 16跑稳定,预留扩容位(比如 16 → 32 翻倍,取模基数乘 2 可平滑扩容) - 扩容时用一致性哈希思路重映射,旧数据按新映射迁移,双写过渡期后切换
坑 4:热点用户
大客户公司会一次性批量导入几千份简历,取模分片会砸进同一个分片。
解法:热点感知 + 分片键后缀。批量导入时给用户 ID 加随机后缀再取模,查询时按规则还原。
坑 5:count(*) 和分页不准
跨分片 count(*) 是各分片之和没问题,但 ORDER BY ... LIMIT 跨分片是归并排序,深分页(offset 大)性能极差。
解法:业务上禁止深分页(简历搜索本来就走 ES),管理后台分页限定前 100 页内。
四、结果
- 单表 2 亿行 → 16 分片,单分片约 1300 万行,写入锁竞争消失
- 峰值写入从「写不进」到稳定承接,配合后面的 ES 检索体系,主库压力大减
- 踩坑成本:核心是把「跨分片」从 SQL 层面消灭掉——设计上就避免,比事后优化便宜一百倍
总结
分库分表是最后的手段,不是第一手段。如果你还在单库,先做:索引优化 → 读写分离 → 冷热分离(归档)→ 才轮到分片。
真到了分片这一步,记住三条:分片键决定一切、跨分片要设计上避免、扩容要提前规划。这三条想清楚,MyCat 还是别的中间件,都只是工具差异。