分库分表规则须与go模块包结构解耦,核心是规则定义、路由计算、连接获取三者分离;推荐建shard包暴露shardkeyresolver和dbrouter接口,用闭包函数配置分片策略,避免硬编码;每个分片应持独立*sql.db实例,禁止全局共享;动态扩分片需原子切换连接池并确保旧池 graceful 关闭。

分库分表规则必须和 Go 模块的包结构解耦
硬编码分库分表逻辑到 dao 或 model 包里,会导致规则变更时要改多处、测试难、无法复用。核心原则是:规则定义、路由计算、连接获取三者分离。
推荐做法是单独建 shard 包,只暴露两个接口:
-
ShardKeyResolver:输入业务 ID(如user_id),输出shard_key(如"shard_01") -
DBRouter:输入shard_key,返回对应*sql.DB实例(或连接池名)
这样 dao 层只调用 shard.GetDB(ctx, userID),完全不感知分片细节。
用函数式配置替代结构体硬编码分片策略
别用 struct 字段存分片数、模数、前缀这些——它们常随环境变化(开发/测试/生产分片数不同)。直接用闭包函数封装规则更灵活:
func NewModSharder(mod int) shard.ShardKeyResolver {
return func(id interface{}) string {
uid, ok := id.(int64)
if !ok {
return "shard_00"
}
return fmt.Sprintf("shard_%02d", uid%int64(mod))
}
}
启动时按配置注入:
- 开发环境:
NewModSharder(2) - 线上环境:
NewModSharder(64)
避免在代码里写死 64,也避免用 map 存 64 个预定义字符串——函数能延迟计算、便于 mock 测试。
初始化并管理阿里云SDK Go 客户端,涵盖 sync.Once 单例、goroutine 安全、endpoint 与 region 配置、VPC 端点及调试。
注意 context 传递和连接池生命周期错配
常见错误是把 *sql.DB 存在全局变量或结构体字段里,再用 context.WithValue 传 shard_key,结果导致连接池被多个分片共享、事务混乱、连接泄漏。
正确做法:
- 每个分片维护独立
*sql.DB实例(哪怕底层用同一物理库) -
shard.GetDB()返回的是已初始化好的、带独立连接池的*sql.DB - 不要在 handler 层用
ctx传分片信息,而应在 service 层明确调用shard.GetDB(ctx, userID)获取对应 DB
否则你写的 tx, err := db.BeginTx(ctx, nil) 可能跨分片,MySQL 不支持跨库事务,会静默失败或报 ERROR 1390 (HY000): Prepared statement contains too many placeholders 这类误导性错误。
动态规则更新必须触发连接池重建而非热替换
线上需要扩分片(比如从 64 扩到 128),不能只改配置然后 reload 规则函数——旧连接池还在用老分片名,新请求走新分片,数据就散了。
必须让 shard 包支持「原子切换」:
- 新规则加载后,先创建所有新分片的
*sql.DB - 等全部就绪,再原子替换内部
map[string]*sql.DB - 旧连接池调用
db.Close(),但不立刻释放,等活跃连接自然归还
漏掉 db.Close() 会导致连接堆积;过早关闭又会让正在执行的查询 panic 报 sql: database is closed。这个切换窗口期比想象中敏感,建议加 health check 接口验证各分片连通性后再切流量。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










