分片键必须与高频等值查询条件对齐,优先选user_id等写入时已知字段;路由逻辑需独立封装并显式传参;元数据应外置管理;严禁跨分片事务与JOIN,需通过补偿机制实现最终一致性。

分片键设计必须与查询模式对齐
动态分片不是“按ID取模”就完事。如果业务里 SELECT * FROM orders WHERE user_id = ? 是主查询路径,但分片键用了 order_id,那每次查询都得打到所有分片——动态分片立刻退化成广播查询。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 分片键字段必须是高频 WHERE 条件中等值匹配的列,且该列在写入时已知(不能是 INSERT 后由数据库生成的值)
- 避免用时间戳、UUID 等高散列但低查询局部性的字段作分片键;
user_id、tenant_id更稳妥 - 若需多条件路由(如
WHERE user_id = ? AND status = ?),优先让user_id决定分片,status仅用于分片内过滤 - 分片键值尽量做归一化:统一转小写、去空格、截断超长字符串,否则
"U123"和"u123"可能被路由到不同分片
分片路由逻辑要独立于 SQL 构建过程
很多团队把分片逻辑硬塞进 ORM 的 Query 方法里,结果发现 db.Where("id > ?", 100).Find(&orders) 这类链式调用根本没法提前提取分片键——ORM 尚未生成 SQL,你却需要决定连哪个库。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 强制要求所有分片查询必须显式传入分片键值,例如定义
OrderRepo.FindByUserID(ctx, userID, opts),而非泛化的Find(ctx, conditions) - 路由函数单独封装,如
ShardForUser(userID string) (*sql.DB, error),内部查缓存或配置中心获取目标实例,不碰 SQL 解析 - 禁止在
database/sql的Driver层做分片(如自定义Open),这会让连接池、事务、上下文传播全乱套 - 如果必须支持“模糊”查询(如按手机号前缀查),预留
ScanAllShards通道,但默认关闭,调用方需显式启用并承担性能代价
分片元数据不能写死在代码里
把分片数、DB 地址、权重写进 const 或 init() 函数,上线后扩容就得发版重启——分片数从 8 扩到 16?停服半小时?
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 用外部存储管理分片映射,比如 etcd 中存
/shard/config/orders/v1,内容为 JSON:{"shards": [{"id": "shard-0", "dsn": "host=a...", "weight": 1}, ...]} - 客户端启动时拉取一次,并监听变更(etcd watch / nacos listener);变更后平滑 reload 路由表,旧连接继续处理完再关闭
- 每个分片配
weight字段,便于灰度切流或故障隔离;路由时用加权一致性哈希,而非简单取模 - 避免用数据库自身存分片配置(如
shard_meta表)——一旦这个库挂了,整个分片层就不可用
事务和跨分片 JOIN 必须明确拒绝或降级
想在 user 和 user_profile 两个按 user_id 分片的表上执行 JOIN?或者跨 3 个分片更新订单、库存、物流单?Golang 层无法 magically 实现分布式事务语义。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 在路由层拦截含多表名的
SELECT或UPDATE ... FROM,直接返回ErrCrossShardJoinNotSupported - 对跨分片写操作,拆成单分片事务 + 最终一致性补偿(如发 MQ、写 DTS 日志),不要尝试用
XID或两阶段提交 - 提供
MustInSameShard(userA, userB)辅助函数,校验两个分片键是否落在同一分片——用于前置防御“本以为同库结果跨了”这类低级错误 - 监控里单独埋点
cross_shard_query_count,数值突增说明业务方绕过了规范,得立刻找人
分片不是银弹,最常被忽略的是“数据迁移期间的双写一致性”和“分片键变更的回滚路径”。这两块没想清楚就动分片,后面八成要重做。


















