首选user_id;因create_time易致写入热点和跨分片查询,而user_id支持高频等值查询、哈希后数据均匀且路由精准。

分片键选 user_id 还是 create_time?先看你的查询和写入模式
绝大多数业务该选 user_id,而不是 create_time。不是因为时间字段“不好”,而是它天然导致写入热点+范围查询跨片——新订单、新消息、新日志全往最新分片扎堆,单片 CPU 和 I/O 很快打满;而查“最近7天数据”这种需求,又得扫多个分片,延迟翻倍。
真正适合用 create_time 的场景极少:比如归档表只读、按月导出报表、冷数据分离。即便如此,也建议搭配二级分片键(如 user_id % 4)做复合路由,避免纯时间分片的雪崩风险。
- 高频等值查询(如“查用户A所有订单”)→
user_id是首选,哈希后均匀,路由精准 - 高频范围查询(如“查某省所有门店昨日流水”)→ 可考虑
province_code+date联合范围分片,但需配元数据表管理边界 - 写多读少且 ID 自增 → 绝对不要直接用主键
id做分片键,否则所有 INSERT 都压在最后一片
哈希取模时为什么必须用 2 的幂次?不是数学洁癖
MySQL 和多数分库中间件(如 ShardingSphere、MyCat)的哈希路由默认走位运算优化:shard_id = hash(key) & (n-1),这只有在分片数 n 是 2 的幂次(如 4、8、16、32)时才等价于 hash(key) % n。如果不是,要么触发慢路径(真取模),要么路由错片——线上出现“数据写了却查不到”基本就栽在这儿。
- 扩容时别硬加到 12 片或 20 片,宁可先扩到 16,再平滑迁到 32
-
hash()函数必须确定性:禁止用UUID()、NOW()、RAND();推荐fnv1a_64(user_id)或MD5(user_id) % 18446744073709551615后转整型 - NULL 值必须提前拦截:
WHERE user_id IS NOT NULL要写进所有业务 SQL,否则所有 NULL 全落到第 0 片,秒变热点
状态码、性别、租户类型…这些字段为什么不能当分片键?
它们基数太低。比如 status 只有 0/1/2 三个值,哈希后最多分散到 3 个分片,剩下 5 片完全空转;gender 是 M/F,两片永远吃不饱,另 6 片干瞪眼。这不是“不够均匀”,是根本没发挥分片价值——你花 8 倍运维成本,只换来 2 倍物理资源利用率。
- 判断基数是否够高:执行
SELECT COUNT(DISTINCT user_id) / COUNT(*) FROM orders,结果 > 0.95 才算健康 - 如果只有低基数字段可用(比如 SaaS 系统只有
tenant_type),必须加扰动:改成CONCAT(tenant_type, '_', user_id)再哈希 - 严禁把多个字段拼一起当“万能分片键”,如
CONCAT(user_id, '_', shop_id)—— 一旦shop_id缺失,整个路由失效
上线后发现数据倾斜了,还能救吗?
能,但代价远高于前期设计。没有“在线改分片键”的银弹,所有方案都绕不开双写+迁移+校验三步。最轻量的是“冗余分片键”补救:在原表加一列 shard_key_v2,用新规则(如 user_id * 1000000 + order_id % 1000000)填充,应用层逐步切流量到新路由逻辑,老数据不动,新写入走新键。
- 绝对不要尝试 ALTER TABLE 直接改分片策略——ShardingSphere 会拒绝,MyCat 会路由错,MySQL 分区表会锁表数小时
- 监控必须前置:上线首周盯死各分片的
data_length和table_rows,用SELECT table_schema, table_name, data_length FROM information_schema.tables定期采样 - 真正的难点不在技术,而在业务协同:订单、支付、物流等系统必须同步改路由逻辑,漏一个,跨片 JOIN 就崩给你看
分片键定下的那一刻,你就已经给未来两年的数据流向画好了轨道。改轨道不是换轮胎,是拆铁轨重铺。









