单表数千万行之后
统一支付平台的订单表和统一认证中心的登录日志表,都长到了单表数千万行。订单查询开始吃紧,日志写入也开始吃紧。
这两张表气质完全不同。订单是按商户维度的高并发写入,登录日志是按用户和时间的海量追加写入。访问模式不同,分片策略就不能套同一个模板。
按商户切订单,按用户切日志
订单表按 merchant_id 哈希,16 库 × 8 表,一共 128 张。商户维度的查询(订单列表、对账)天然落在一个分片内。跨商户的后台统计走 MySQL 到 StarRocks 的离线同步,不做强一致跨片 JOIN。
登录日志按 user_id 分库、再按月份分表。「查某用户最近的登录」落在单片;按月归档清理直接处理整月表,两头都方便。
主键统一用 Snowflake。订单号里嵌入了商户分片位,解析订单号就能直接路由,不用二次查路由表。
订单号里藏着路由
真实项目里我们用 GORM 的 sharding 插件加自研 Resolver,下面是核心路由逻辑的简化版:
| |
业务侧解析订单号即可路由:
| |
登录日志按月分表:
| |
坑一个一个说
第一个坑是分片键。一旦选错,代价极大。订单按 merchant_id 分片之后,C 端「查我的订单」会变成广播查询。我们的做法是订单再冗余一份按 user_id 分片到查询库(通过 Canal 同步),复杂检索直接走 ES。别指望一个分片键满足所有查询。
第二,跨片事务尽量避免。订单和账户余额落在同一商户分片内,本地事务就够;跨片的清结算用本地消息表保证最终一致,不上 XA。
第三,分库数量提前规划,但别过度。16 库是按未来几年容量估的,扩容用翻倍法(16→32),配合双写加数据校对平滑迁移。
第四,Snowflake 要防时钟回拨。NTP 同步打底,小幅回拨直接拒绝请求,宁可让调用方重试,也不能吐出重复 ID。
第五,跨片分页是噩梦。LIMIT 100000,20 会在每个分片都执行再归并,我们强制查询带时间范围和分片键,并限制深翻页。
第六,数据迁移必须能回滚。新流量双写新旧库,对账任务比对两边,一致后切读,最后停旧写。每一步都要退得回来。
后来
分库分表是「先苦后甜」,苦的不是中间件配置,是想清楚四件事:按什么维度分片、哪些查询必须落在单片、跨片查询去哪查、未来怎么扩容。统一支付平台和统一认证中心两个场景,这四件事的答案就完全不一样。
封面图:mikecogh / Flickr · CC BY-SA 2.0
