改不动的决策
分库分表的所有决策里,扩容可以重新设计、路由可以换中间件、表数可以从 64 加到 128,唯独分片键一旦选定,几乎是终身制。原因在于一条铁律:所有查询都必须能推导出数据在哪个分片。而查询是活着长出来的——今天按用户查订单,明天运营要按商家查,后天客服要按订单号查。分片键选了 user_id,所有不带 user_id 的查询,都会退化成扫全部分片的「全路由」,分片越多,扫得越惨。
所以选分片键的第一原则不是技术,是业务:把系统里频次最高、最重要的查询维度找出来,用数据说话——把慢查询日志和访问日志拉出来按 WHERE 条件聚类,哪个维度的查询占了八成,它就是候选。
经典难题:订单的三个主人
订单表是这个难题的教科书案例。一张订单至少有三个查询维度:买家(我的订单)、卖家(我的生意)、订单号(客服与支付回调)。三个维度互相独立,而分片键只能选一个——选 buyer_id,卖家的「店铺订单列表」就要扫全分片;选 seller_id,买家的「我的订单」同样遭殃。三种经典解法,按代价从小到大排:
解法一:冗余双写——订单数据写两份,一份按 buyer_id 分片、一份按 seller_id 分片,两个维度各自精准路由,代价是存储翻倍、写入要保证两份一致、任何字段变更都要双份同步。适合两个维度都高频的场景。解法二:异构索引——主数据按 buyer_id 分片,另建一套按 seller_id 组织的订单索引表(或直接喂给搜索引擎),索引只存定位信息,回主库取详情。ES 这类检索引擎天然适合干这个,代价是多维护一条数据同步链路。解法三:基因法——在生成订单号时,把 buyer_id 的哈希「基因」埋进去,订单号本身就能算出分片,按订单号查不再需要额外路由;再配合解法一或二覆盖卖家维度。基因法的订单号形如:
订单号 = 时间戳段 | buyer基因(hash(buyer_id) 低 10 位) | 序列段
路由: slot = 订单号基因段 % 1024 // 与按 buyer_id 路由结果一致选键的其余纪律
除了维度覆盖,分片键还有几条纪律。要分散:键的取值分布要均匀,用省份、状态这类低基数字段做分片键,等于给自己造热点;自增 ID 单调递增会让 Range 刀法出现写入热点,配 Hash 刀法则没问题。要常在:分片键必须在绝大多数查询和写入请求里天然可用——用户下单必带 user_id,天然贴合;如果分片键只在少数场景出现,数据写入就没了归属。要稳定:分片键的值不能变——数据一旦落片,改分片键等于删除加重新插入,业务上往往是灾难(换手机号都别想让用户数据搬家)。
把纪律合起来就是一句口诀:高频维度做键、基因法补洞、冗余或异构兜底。分片键定了,下一个问题是路由逻辑放在哪儿执行——写在代码里、坐在代理上,还是长在 SDK 里,下一篇比较三种落法。
评论 (0)