分库必须为每个物理库预建独立*sql.db实例,不能动态切换;gorm中需用db.table()显式指定分表名,model()无效;分片键应选高频查询且不变的字段(如user_id),避免跨库事务和join。

分库必须预建独立 *sql.DB 实例,不能动态切换
Go 的 database/sql 包不支持运行时“切换数据库”,USE db_name 在已建立连接上无效,且 sql.DB 本身没有切换 API。常见错误是只开一个 *sql.DB,然后试图靠拼接 SQL 或修改 DSN 来路由——这会导致连接复用冲突、事务绑定错乱、甚至 panic: sql: database is closed。
正确做法是为每个物理库(如 user_db_0、user_db_1)单独调用 sql.Open,生成独立实例,并分别配置:
-
db0.SetMaxOpenConns(50)、db1.SetMaxOpenConns(20)—— 权重高的库配更多连接 -
db0.SetConnMaxLifetime(30 * time.Minute)—— 避免长连接老化失效 - 每个实例都应调
db.Ping()验证连通性,失败则提前报错,别等到查询时才崩
gorm.Table() 是唯一可靠的分表手段,Model() 无效
在 GORM 中,db.Model(&User{}) 永远绑定 struct 标签里的 table: "user",不会随分片逻辑变化。想查 user_2,必须显式写 db.Table("user_2"),否则查的是默认表,轻则数据为空,重则写入错误分片。
注意以下细节:
- 所有 CRUD 前都要先算表名,例如
tableName := "user_" + strconv.Itoa(int(userID%16)) -
Session()不继承Table()设置,每次操作都得重新指定:db.Session(&session).Table(tableName).Where("id = ?", id).Find(&u) - 负数
userID必须先abs(userID) % N,否则取模结果为负,映射到不存在的索引
分片键必须匹配高频查询条件,不是主键也不是 UUID
分片键选错,等于把分库分表变成性能黑洞。比如订单服务中 90% 请求带 user_id 查历史订单,却用 order_id 分片——每次查询都得扫全部分片。
实操判断标准:
- 看慢查询日志里
WHERE出现频率最高、过滤性最强的字段 - 优先选不变字段(
user_id、tenant_id),避免用created_at(范围查询难路由)或 UUID(分布随机、索引效率低) - 若同时高频查
tenant_id和user_id,可用组合键tenant_id:user_id,哈希前加固定分隔符防止碰撞 - 别手写
sum([]byte(s)) % n,用hash/fnv或hash/maphash,避免字符串相似导致扎堆
跨库事务必须放弃原生 tx,改用补偿或本地消息表
sql.Tx 天然绑定单个 *sql.DB 实例,跨库调 db1.Begin() 再 db2.Begin(),得到的是两个孤立事务,任一失败无法回滚对方。
生产环境实际做法:
- 单条记录的所有读写必须落在同一库+同一表,这是底线——靠分片键保证
- 避免跨库 JOIN:拆成多次查询,在 Go 层合并;或冗余关键字段(如订单表存
user_name) - 扣库存 + 记流水这类操作,改用 TCC(Try-Confirm-Cancel)或本地消息表 + 消费者重试
- MySQL XA 协议理论上支持跨库事务,但 Go MySQL 驱动普遍不兼容,Proxy 层开启后也常因超时失败,慎用
最易被忽略的一点:分片逻辑必须在 DAO 层封装成纯函数,输入分片键,输出 *sql.DB + 表名,不能散落在业务代码里——否则压测时根本没法验证路由是否均匀,上线后才发现某分片 CPU 100%。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











