直接用 database/sql 做读写分离易出错,因事务不一致、连接泄漏、上下文传播失控;需接口隔离读写、context 动态路由、统一 driver 或禁用读库事务、用 on conflict/insert...on duplicate 原子更新读模型。

为什么直接用 database/sql 做读写分离容易出错
Go 里没有开箱即用的 CQRS 框架,很多人一上来就手动拆 *sql.DB 实例,给写库配一个、读库配几个,再用 sync.Pool 或全局变量管理——这会立刻撞上事务一致性、连接泄漏和上下文传播三座大山。
典型错误是:在写操作事务中调用读接口,结果读到了旧快照(尤其用 MySQL 的 REPEATABLE READ 隔离级别时),或者读库没配置 SetMaxOpenConns,高峰期连接数爆满却查不出原因。
- 写库必须走事务,读库不能参与事务(否则主从延迟导致读不到刚写的记录)
- 读库连接池要单独配置,
SetMaxIdleConns和SetConnMaxLifetime得比写库更激进(避免读库连接卡在过期主从同步链路上) - 不能靠函数名区分读写,得靠接口契约——比如定义
Writer和Reader两个 interface,而非混用GetUser和SaveUser
如何用 context.Context 控制读库路由策略
硬编码读库地址或随机轮询不现实。真实场景里,你得根据请求特征动态选库:比如后台管理请求强制走主库(避免查不到刚改的配置),API 请求按用户 ID 哈希到某个从库,健康检查失败时自动降级到主库。
推荐在 http.Handler 中注入路由策略,而不是在 DAO 层 if-else 判定:
func withReadDB(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var db *sql.DB
switch {
case isAdminRequest(r):
db = primaryDB // 强制主库
case isStaleReadAllowed(r):
db = pickReplicaByUserID(r.Context().Value("user_id").(int))
default:
db = primaryDB // 默认兜底
}
ctx := context.WithValue(r.Context(), "read_db", db)
r = r.WithContext(ctx)
next.ServeHTTP(w, r)
})
}
DAO 层只认 ctx.Value("read_db"),不关心怎么选的——这样策略变更不用动数据访问代码。
sqlx + pgx 混用时的事务穿透陷阱
如果写库用 pgx(支持 PostgreSQL 的自定义类型和批量操作),读库用 sqlx(方便结构体扫描),要注意:事务对象无法跨 driver 传递。你不能把 pgx.Tx 塞进 sqlx.DB.QueryRowContext,会 panic 报 "tx not associated with this DB"。
解决方案只有两个:
初始化并管理阿里云SDK Go 客户端,涵盖 sync.Once 单例、goroutine 安全、endpoint 与 region 配置、VPC 端点及调试。
- 统一用
database/sql标准接口封装所有 DB 操作(放弃pgx的高级特性,但稳定) - 写操作全程用
pgx,读操作也用pgx,但为读库单独建pgxpool.Pool实例,并禁用其事务能力(不调Begin())
别试图用 sqlx.BindNamed 处理 pgx 的 $1 占位符——语法不兼容,运行时报错才去查文档就晚了。
事件驱动更新读模型时,如何避免 SELECT FOR UPDATE 锁表
CQRS 的读模型更新常由领域事件触发(比如订单创建后更新用户统计)。如果多个事件并发更新同一行,直接 UPDATE ... SET count = count + 1 可能丢更新;加 SELECT FOR UPDATE 又容易锁表阻塞其他查询。
更稳妥的做法是用原子操作替代锁:
// PostgreSQL 示例:避免显式锁
_, err := readDB.ExecContext(ctx,
"INSERT INTO user_stats (user_id, order_count) VALUES ($1, 1) "+
"ON CONFLICT (user_id) DO UPDATE SET order_count = user_stats.order_count + 1",
userID)
MySQL 对应的是 INSERT ... ON DUPLICATE KEY UPDATE。关键是让数据库自己处理竞态,而不是在 Go 层做 SELECT → calc → UPDATE 三步。
注意:这种写法要求读模型表有唯一索引(如 user_id),且事件消费逻辑必须幂等——因为网络重试可能导致同一条事件被处理多次。
真正难的不是分库,而是让写模型变更能可靠、低延迟地反映到读模型。主从延迟、事件丢失、消费者 crash 后状态不一致……这些问题不会因为用了 CQRS 就自动消失,反而会因为职责拆分变得更隐蔽。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










