Go微服务分库分表必须手动控制连接选择、SQL路由与表名拼接,ORM不参与分片;需为每个物理库预建独立*sql.DB实例并单独配置连接池,分表名须显式拼接,分片键应选高频查询字段(如user_id),跨分片操作需应用层合并,事务限单库,全局ID用sonyflake。

Go 微服务里做分库分表,没有“开箱即用”的方案,必须由你控制连接选择、SQL 路由和表名拼接——ORM(如 gorm、sqlx)不参与分片决策,只执行单库 SQL。
分库必须预建独立 *sql.DB 实例,不能复用同一个连接池
database/sql 的 *sql.DB 天然绑定单一数据库实例,强行用一个实例连多个物理库会导致连接竞争、事务失效、超时策略错乱。实操要点:
- 为每个物理库(如
user_db_0、user_db_1)调用一次sql.Open,生成独立*sql.DB - 用
map[int]*sql.DB或map[string]*sql.DB缓存,键是分片 ID 或逻辑库名 - 每个实例单独配置连接池:
db0.SetMaxOpenConns(20)、db1.SetMaxOpenConns(10),权重高的库要配更多连接 - 首次初始化时调用
db.Ping()做健康检查,避免请求进来才 panic
分表必须显式拼接表名,gorm.Model() 不起作用
gorm.Model(&Order{}) 永远指向 struct 标签里定义的 table: "orders",不会自动变成 orders_202406。所有 CRUD 都得先算表名:
- 封装纯函数生成表名,例如:
func GetOrderTableName(t time.Time) string { return "orders_" + t.Format("200601") } - 写入时用
db.Table(tableName).Create(&order),不是db.Create(&order) - 查询时 WHERE 必须含分表字段,如
WHERE created_at BETWEEN ? AND ?,否则无法确定查哪张表 - GORM v2 的
Session不继承Table()设置,每次操作都得重新指定
分片键选 user_id 而不是 order_id,路由函数必须幂等
分片键不是主键,而是高频查询条件字段。如果 90% 请求带 user_id 查订单,却用 order_id 分片,等于把所有查询打散到全部分片,失去局部性。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
立即学习“go语言免费学习笔记(深入)”;
- 推荐取模:
shardID := userID % 4,简单、可预测、无状态 - 避免用时间或 UUID 做分片键:时间会倾斜(如秒杀),UUID 无法范围查询、哈希后分布不均
- 组合键(如
tenant_id:user_id)需加固定分隔符,防止"1:23"和"12:3"碰撞 - 哈希别用
sum([]byte(s)) % n,改用hash/fnv或hash/maphash,再映射到权重区间
跨分片操作没银弹,得在应用层兜底
MySQL 不知道你的分片逻辑,JOIN、ORDER BY、COUNT(*)、GROUP BY 都无法跨库原生支持:
-
SELECT * FROM user JOIN order会发到单个库,结果缺失;正确做法是先查user得到一批user_id,再按user_id % 4分组路由并发查对应order分片,最后 Go 层合并 -
ORDER BY created_at LIMIT 10要各分片取 top 20,再全局排序截取;别用LIMIT offset, size,offset 越大越慢 - 事务只能单库生效:
tx, _ := db.Begin()绑定的是某一个*sql.DB;跨库更新必须用本地消息表 + 重试,或放弃强一致 - 全局唯一 ID 不能依赖自增主键,改用
sony/sonyflake,注意时钟回拨会 panic
最容易被忽略的是分片键和表名计算的耦合性——写入用 userID % 4 选库、created_at.Format("200601") 选表,读取时必须用同一套规则,差一个字符或时区就查不到数据。上线前务必用真实样本跑万次验证路由一致性。

















