Go微服务分库分表必须由业务层显式控制,ORM和语言均不支持;每个物理库需独立sql.Open()初始化*sql.DB实例并单独配置连接池,分片键应选稳定整型字段如user_id,路由用取模或一致性哈希,表名动态拼接且查询必须带分片条件,全局ID须用Sonyflake等方案生成。

Go 微服务里做分库分表,没有银弹,必须自己扛路由、连接隔离和 ID 生成——ORM 不会替你做,语言本身也不支持,所有决策点都在业务代码里。
为什么不能用一个 *sql.DB 连多个物理库
这是最常踩的坑。database/sql 的 *sql.DB 是为单实例设计的:连接池复用、事务绑定、超时控制都基于单一数据源。如果强行把多个 MySQL 实例的 DSN 都塞进同一个 sql.Open(),要么连不上,要么事务只作用于第一个成功连接的库,其余写入完全脱离 ACID 保证。
- 每个物理库必须调用一次
sql.Open(),得到独立的*sql.DB实例 - 这些实例要提前初始化、健康检查、统一配置(如
SetMaxOpenConns) - 不能共用
context超时或sql.Tx,跨库事务天然不成立
分片键选 user_id 而不是 created_at 或 uuid
分片键是整个路由逻辑的锚点,必须稳定、可预测、无歧义。时间类字段会导致热点(比如新用户集中注册,全打到最新分片),UUID 则无法取模或范围路由,且长度大、索引效率低。
-
user_id是最佳候选:业务高频查询字段、不变、整型、分布均匀 - 哈希方式推荐
userID % N(N 为库数),简单、幂等、易调试 - 避免用
time.Now().UnixMilli()做分片依据——时钟回拨或批量导入会直接打乱路由 - 一旦选定,所有读写操作(
INSERT、SELECT、UPDATE、DELETE)必须用同一套计算逻辑,否则刚写进去就查不到
分表名必须动态拼接,且 WHERE 条件必须含分片字段
分库靠连接选择,分表靠 SQL 构造。GORM 的 Table() 或 raw SQL 的字符串拼接是唯一直接手段,但极易出错。
- 示例:
db.Table("orders_" + formatMonth(order.CreatedAt)).Create(&order) - 所有查询必须带
WHERE created_at BETWEEN ? AND ?或WHERE user_id = ?,否则无法确定目标表,只能全表扫描 - GORM v2 中
Session()不继承Table()设置,每次操作都得显式调用 - 禁止
SELECT * FROM orders这类无条件语句——它不会自动路由到任何分表,执行失败或返回空
全局 ID 必须用 sony/flake,不能依赖 AUTO_INCREMENT
分库后,各库自增主键必然冲突。更隐蔽的问题是:即使暂时没重复,ID 无序也会导致 B+ 树频繁分裂,索引性能断崖下跌。
- 用
github.com/sony/gobreaker?不对,是github.com/sony/sonyflake——轻量、毫秒级时间戳 + 机器 ID + 序列号,输出int64 - 初始化时需传入唯一
MachineID(可用 IP 哈希或配置项),并处理时钟回拨(默认 panic,建议降级为 sleep 等待) - 已上线系统若用自增 ID,可临时改用「分库偏移」:库 0 从 1 开始、库 1 从 10000001 开始……但扩容困难,仅作过渡
- 别用数据库生成的 UUID:32 字节存储、索引体积翻倍、排序无意义、JOIN 性能差
真正难的不是写对那几行取模代码,而是让所有团队成员在新增接口、写新查询、加新字段时,都下意识记得:先算分片键,再选连接,再拼表名,再塞 ID——漏掉任意一环,线上就会出现数据写歪、查不到、删错库的事故。


















