上周和一个做电商的朋友吃饭,他愁眉苦脸地说,公司业务涨得太快,MySQL 单库扛不住了,每天凌晨的报表查询能把主库拖死。他问我,要不要搞分库分表?我说,你先别急着动手,数据迁移这事儿,搞不好就是一场灾难。他听完更焦虑了,筷子都放下来了。

分库分表听起来挺专业,说白了就是数据库装不下那么多数据了,得拆开。最常见的场景是订单表,一天几百万条记录,一年下来就是上亿。这时候查个上个月的订单,索引再优化,也得扫几千万条数据。更别说那些复杂的统计查询,直接把 CPU 跑满。我见过一个真实案例,某公司把用户表分了 128 个库,查询时得同时打开 128 个连接,结果网络 I/O 成了瓶颈,反而比单库还慢。所以分库分表不是万能的,你得先想清楚,是为了解决写入瓶颈还是查询瓶颈。
数据迁移的第一步不是写代码,而是搞清楚业务场景。你得知道哪些字段是查询条件,哪些字段是排序依据。比如订单表,用户 ID 肯定是核心查询条件,那按用户 ID 哈希分片最合理。但如果是按时间查询多,比如查最近 7 天的订单,就得考虑按时间范围分表。我见过一个奇葩案例,有人把用户表按手机号后四位分片,结果手机号前三位固定,后四位分布极不均匀,有的库塞了 3000 万数据,有的库只有 200 万。这就是没想清楚业务逻辑,直接拍脑袋定的分片规则。
确定好分片规则后,就要开始设计迁移方案。最稳妥的方式是双写双读,也就是新旧库同时写入,查询优先走旧库,数据校验通过后再切到新库。具体怎么做呢?第一步,在旧库上搭建数据同步工具,比如用 Canal 监听 binlog,把增量数据实时同步到新库。第二步,写全量迁移脚本,把历史数据分批导入新库,每批 1000 条,加上事务控制,失败了能回滚。第三步,全量迁移完成后,开启双写,应用同时写旧库和新库,但查询仍走旧库。这一步最关键的是数据校验,需要写脚本每天比对旧库和新库的数据,确保一致性。
双写双读听起来简单,但坑特别多。最大的坑是数据一致性保障不了。比如用户下单时,旧库写成功了,新库写失败,这时查询旧库没问题,但数据不同步。解决办法是写补偿任务,定期扫描新库缺失的数据,重新同步。另一个坑是性能问题,双写意味着每次写入都要操作两个数据库,延迟会增加。我见过一个团队,双写后接口响应时间从 20 毫秒涨到 80 毫秒,用户直接投诉。他们把新库的写入改成异步队列,优化了连接池配置,才把延迟降下来。
数据校验这一步很多人会忽略,却是最不能省的环节。你得写全量校验脚本,把旧库和新库的数据按主键排序,逐条比对。比对内容包括字段值是否一致、记录数是否一致、唯一索引是否冲突。我见过一个案例,某公司迁移用户表时,旧库有 3000 万用户,新库只有 2998 万,少了 2 万条。排查后发现是分片规则没处理好,有的用户 ID 哈希后落到了不存在的分片上。所以校验脚本必须跑满至少三轮,每轮间隔 24 小时,确保增量数据也正确。
数据校验通过后,就可以切查询了。但切查询不是一次性全切,必须灰度发布。先把 1% 的流量切到新库,跑 24 小时,观察错误日志和慢查询。没问题再切到 5%,然后是 10%、30%、50%、100%。每一步都要有回滚预案。比如切到 10% 时,发现新库某个分片查询特别慢,就得马上把该分片的流量切回旧库,排查是索引问题还是数据倾斜。灰度过程快的一周,慢的一个月,取决于业务的重要程度。
最后一步是清理旧库。很多人以为迁移完了就万事大吉,直接把旧库删了,结果第二天新库出现 bug,回滚来不及。正确做法是,切完所有流量后,旧库保留至少 30 天,只读不写。30 天内如果新库出问题,还能切回去。30 天后,再逐步下线旧库的只读节点,删除数据。这个过程中别忘了备份旧库的数据文件,万一有法律合规需求(比如用户数据要保留 3 年),还能从备份里提取。
朋友听完我的建议,沉默了半分钟,然后说,那我还是先优化一下 SQL 吧。我笑了笑,没说话。其实分库分表不是单纯的技术问题,而是业务判断和风险控制的问题。如果业务增长没有那么快,单库优化一下索引、加个缓存,可能还能撑两年。但如果真的到了不得不拆的那一天,记住一句话:迁移不是目的,稳定才是。每一步都要有回滚方案,每一次切流量都要灰度,每一行数据都要校验。只有这样,才能在数据库“爆炸”时优雅地拆掉它,而不是被它炸得遍体鳞伤。


