周六凌晨三点,电话响了。老板在那头吼:“系统挂了!用户都登不上!” 我一个激灵,爬起来看监控——数据库CPU 100%。单库,单表,几千万的数据,撑不住了。那是我第一次真正动念头:得分库分表了。
网上搜了一圈,大部分文章都是概念性的,讲什么水平垂直分割,图倒是画得漂亮,可落地时全是坑。今天把这些年踩的坑和压箱底方案全抖出来,全是干货,不废话。
一致性哈希到底在解决什么鬼问题?
先说分片算法。很多人一上来就用取模,比如 user_id % 16。这简单。但扩容呢?从16变成32时,几乎所有数据都要重新打散迁移——生产环境的噩梦。我们第一次扩容,迁移了整整一周,中途还丢了几条记录,差点被开除。

后来用了一致性哈希。你想象一个环,0到2^32-1,把节点哈希到环上,数据也哈希到环上,顺时针找第一个节点。这样增减节点只影响邻近的一小部分数据——最多迁移 1/n 的数据量,n是节点数。完美?并不。你会发现某两个节点间距离特别大,数据全倾斜到下一个节点,它被打趴下。怎么办?上虚拟节点:每个物理节点映射多个虚拟节点散落在环上,数据分布就均匀了。我们线上部署后,数据倾斜从40%降到了5%以内。这才是工程之美——用虚拟化把随机性驯服。
压测数据教你做人:分库分表的性能魔咒
理论说分库分表能线性扩展?我们实测告诉你真相。环境:MySQL8.0,32C128G,NVMe SSD。单库单表,1000万用户数据。
点查询(主键ID查一条):单库QPS 50,000(P99 2ms)。16库16表后,配合Proxy,QPS飙到 720,000 ——近乎线性,因为每个分片独立回应,没啥耦合。爽!
范围查询(按时间查最近7天订单):单库跑出来 8,000 QPS(索引优化过)。分库后…你猜多少?15,000 QPS!只翻了不到一倍。why?因为SQL要发到所有分片,然后聚合排序。网络、合并排序开销,更重要的是,最慢的那个分片拖后腿——木桶效应。我们在Proxy层做了并行查询+快速失败,但天花板就在那里。所以分库后的查询模式必须改变,别指望沿用老的SQL。
差点让我删库跑路的三个坑
坑1:分片键选不对,热点直接爆炸
我们做社交App,自然用user_id分片。上线一个月,忽然某个分片告警不断,CPU 95%。查了半天:一位网红入驻,粉丝百万,他的动态写入全落到同一个分片!这就叫热点。取模或一致性哈希都解决不了业务热点。解决方案?两级分片:先按user_id哈希分库,再在库内按时间范围分表(比如按月),但写入热点还在。最终我们搞了热点动态迁移:监控检测到热点,自动将该用户的数据路由到一个独立的“热点库”,那个库我们给了更高配置。代价是这套路由系统维护成本不低。没办法,架构就是权衡。

坑2:跨分片join,慢得让你怀疑人生
一开始我们用中间件支持多表关联,压测时200并发,平均响应2秒——不可用。原因很简单:从16个节点拉回数据,在内存里join,瞬间爆炸。后来我们定了条铁律:禁止跨分片join。业务方必须接受。替代方案?数据冗余。比如用户表user,每个分片都存一份read副本,通过canal同步。或者订单表关联商品表,把商品名称直接冗余到订单分片里。查询时不再join。偶尔需要的复杂分析,扔到Elasticsearch里。这确实增加了开发成本,但换来了毫秒级响应。记住,在分布式里,冗余换性能是常态。
坑3:分布式事务——别碰,除非你懂得怎么死
下单减库存。以前在一个库,事务保证。分库后怎么办?脑袋一热上XA,2PC。测试环境一跑:单库下订单500 TPS,2PC后只剩80 TPS——锁等待和协调开销惊人。而且万一协调者挂了,资源阻塞,那是灾难。我们差点在生产用了,还好及时打住。
最终方案:本地事务 + 消息表。下单流程:先在订单库写订单(状态pending),同时写一条消息记录到本地消息表,然后提交事务。另一个“发消息”任务不断捞消息发送到MQ。库存服务消费消息扣减库存,如果扣减成功,回写消息状态;失败则重试或人工介入。这是最终一致性,有短暂的不一致窗口,但业务能接受。压测:这套东西上线后,下单达到 2000 TPS,远超2PC。当然,开发复杂度翻倍了,补偿、幂等都要处理好。但架构不就是在复杂度和性能间走钢丝吗?
这些坑,搞得我三年没睡好觉。但回过头看,每一个坑都踩得值——因为它们逼你理解系统的本质。分库分表永远是最后手段,能不拆就不拆,要拆就得做好被扒层皮的准备。就这样,散了吧。