分库分表必须由业务层控制,orm无法自动实现;需为每个物理库预建独立*sql.db实例,分片键选高频查询字段(如user_id),用取模或哈希路由,跨库事务须用补偿机制。

分库分表在 Go 框架里不是 ORM 能自动解决的,必须由业务层显式控制连接选择、SQL 路由和表名拼接;用 gorm 或 sqlx 时,全局 *gorm.DB 或 *sql.DB 实例不能复用,否则会路由错、事务失效、连接池争抢。
分库必须预建独立 *sql.DB 实例,不能动态切换
Go 的 database/sql 不支持运行时“切换数据库”,sql.DB 是单库抽象。常见错误是只开一个 db 然后试图用 USE db_name 切库——MySQL 协议不支持该语句在已建立连接上生效,且 sql.DB 本身无此 API。
- 每个物理库(如
user_db_0、user_db_1)必须调用一次sql.Open,生成独立*sql.DB实例 - 每个实例需单独调
SetMaxOpenConns、SetConnMaxLifetime,权重高的库连接数要配多些 - 别把所有连接塞进一个
map[int]*sql.DB就完事——得确保每次查询前通过分片键算出索引,再取对应实例,例如:db := dbPool[userID%4] - panic: sql: database is closed 常见于连接被提前
Close()或未初始化就访问,检查是否漏了db.Ping()验证
gorm.Table() 是唯一可靠的分表手段,Model() 无效
用 gorm 时,db.Model(&User{}) 永远绑定 struct 标签里的 table: "user",不会随分片逻辑变化。想查 user_2,必须显式调 db.Table("user_2"),否则查不到数据或写错表。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- 所有 CRUD 操作前,先调
GetTableNameByUserID(id)这类纯函数算出真实表名 - 分表字段(如
created_at)必须出现在 WHERE 条件中,否则无法确定查哪张表;禁止SELECT * FROM user这种无分片键的全表扫描 - 注意
Session()不继承Table()设置,每次操作都得重新指定:db.Session(&session).Table(tableName).Where(...).Find() - 负数 ID 必须先
abs(id) % N,否则取模结果为负,映射失败
分片键选 user_id 而非 order_id,优先匹配高频查询条件
分片键不是主键,而是 WHERE 查询中最常出现的字段。选错会导致大量跨分片查询,性能断崖下跌。比如订单服务里 90% 请求带 user_id 查历史订单,却用 order_id 分片——等于每次都要扫全部分片。
- 统计慢查询日志,挑出现频率最高、过滤性最强的字段作为分片键
- 若同时高频查
tenant_id和user_id,可用组合键tenant_id:user_id,哈希前加固定分隔符避免碰撞 - 避免用
UUID或自增id:前者分布随机难索引,后者增长快易导致单分片热点 - hash 函数别手写
sum([]byte(s)) % n,用hash/fnv或hash/maphash,再映射到权重区间
跨库事务必须放弃,用本地消息表或 TCC 补偿
MySQL XA 在 Go 里基本不可用:驱动不支持、Proxy 解析不稳定、性能差三倍以上。生产环境真要跨库写入(如扣库存+记流水),只能降级为最终一致性。
- 单条记录的所有读写必须落在同一库+同一表,靠分片键保证——这是底线
- JOIN 跨库?拆成多次查询,在 Go 层
map合并或冗余字段(如订单表存用户昵称) - 批量写不同分片?改异步写入 + 成功回调,别强求原子性
- 本地消息表方案:先写业务表 + 写消息表(同一库事务),再由 worker 异步投递到其他库,失败重试
最容易被忽略的是表名生成逻辑的测试覆盖和连接池参数隔离——线上跑一周后才发现某分片连接池长期 WaitCount > 0,或某类查询因没传分片键直接扫全库拖垮整个集群。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










