取模的账,迟早要还
第 4 篇选 Hash 刀法时埋了个伏笔:取模路由均匀,但扩容是它的死穴。数据按 user_id % 32 分了 32 张表,两年后每张表又长到一亿行,扩到 64 张——模数一改,新的归属和旧的归属对不上号:原本落在表 3 的数据,新规则下大部分要去别的表。「扩容」实际等于「把几乎全部数据重新分配一遍」,生产系统上这就是一次大迁移工程。怎么还这笔账,两条主流路线:翻倍扩容法和预分片法。
路线一:翻倍扩容——数学的温柔
翻倍扩容利用了取模的一个数学性质:模数从 32 翻到 64 时,x % 64 的结果要么等于 x % 32,要么等于它加 32——每条数据只有两个去处:留在原地,或搬进「原表号加 32」的新表。也就是搬家率恰好 50%,而且目标位置确定,无需任何计算协商:
旧:slot = x % 32 → 表 3 (x%32 == 3)
新:slot = x % 64 → 表 3 (x%64 == 3,原地不动)
→ 表 35 (x%64 == 35,即 3+32,迁移)
判定:x % 64 >= 32 ? 搬到 表(x%32)+32 : 原地不动操作节奏配双写:新表上线后进入双写期(老表为主、新表同步),后台任务按判定规则分批迁移存量(第 14-15 篇的迁移框架原样复用),迁完校验、切读、切写、下线老表。翻倍法的约束是必须成对扩——32 到 64 到 128,不能 32 跳 96,规划容量时按 2 的幂走。
路线二:预分片——把映射固定住
更一劳永逸的思路是把「数据到槽」的分配和「槽到库」的部署彻底分开:第一天就按一个足够大的模数(如 1024)算槽,1024 个槽的归属永不改变;每个槽映射到当前部署的某个库某张表,这张「槽位映射表」是唯一可变的东西:
slot = hash(user_id) % 1024 // 永不变:数据到槽
slot → (db, table) // 可变:槽到物理库的映射
扩容 8 库 → 16 库:把映射表里 4 个库的槽划给 4 个新库,批量搬扩容从「改路由规则」降级成「改映射配置加搬部分槽」——应用代码与路由算法全程不动。这个思想的同款你在别处见过:Redis Cluster 的 16384 个哈希槽、一致性哈希的虚拟节点,都是用一层固定的大粒度中间层,把数据归属和物理部署解耦。预分片的工程代价:槽位映射本身要高可用存储、要缓存进路由层,代码里多一层间接——买的是未来每一次扩容的从容。
扩容日的执行清单
无论哪条路线,扩容日的动作都收敛成同一套:限速搬迁(迁移任务的 QPS 压在源库安全水位以下)、双写保一致(迁移期间新写双发,杜绝边界数据丢失)、校验后切流(行数加 checksum 通过才放流量)、可回滚(切流后老库保留一个观察期)。这套流程正是接下来两篇「数据迁移」的完整展开——扩容只是迁移最常见的一种触发场景,把它吃透,换机房、换架构、换存储都只是换个搬家的理由。
评论 (0)