go应用中数据库死锁并非go runtime报错,而是数据库层返回错误码(pg为“40p01”,mysql为1213),需在service层显式捕获并幂等重试,严禁在gin handler中实现重试逻辑;gorm须统一主键排序加锁、避免共享*sql.tx、禁用隐式全表扫描。

Go 应用里数据库死锁不是 Go runtime 报的 deadlock
看到 fatal error: all goroutines are asleep - deadlock! 别急着查 channel 或 mutex——90% 情况下,这是数据库死锁的“假面”。PostgreSQL 真实报错是 ERROR: deadlock detected(错误码 "40P01"),MySQL 是 Deadlock found when trying to get lock(错误码 1213)。这些错误被 GORM 或 database/sql 以 error 形式返回,不会触发 panic,也不会被 Go 运行时感知。你得在业务逻辑里显式捕获,而不是等框架“自动兜底”。
Gin handler 中别写重试循环
HTTP handler 职责是接收请求、校验参数、调 service、返回响应。把重试逻辑塞进 handler 会导致:耦合加重、超时控制混乱、日志难以归因、中间件(如 logger、recovery)行为异常。正确做法是下沉到 service 层,且只对明确幂等的操作重试:
- 重试前必须确保事务内无副作用操作:比如不能在重试时重复调
http.Post()或发 Kafka 消息 - 使用
context.WithTimeout()包裹整个重试过程,而非单次事务(避免某次重试卡住拖垮整个请求) - 重试次数严格 ≤ 3;退避用
time.Sleep(time.Millisecond * time.Duration(math.Pow(float64(i), 2) * 10))类指数策略,别用固定 100ms - 捕获错误要精确:
pgerr, ok := err.(*pgconn.PgError); ok && pgerr.Code == "40P01",别用模糊的strings.Contains(err.Error(), "deadlock")(可能误伤日志里的 debug 字符串)
GORM 中必须显式控制加锁顺序
GORM 的 Save()、Updates() 默认不加锁,但并发更新同一组记录时,若两个事务按不同顺序执行 UPDATE ... WHERE id IN (2,1) 和 UPDATE ... WHERE id IN (1,2),极易触发 AB/BA 死锁。解决方案不是关掉事务,而是强制统一顺序:
- 在事务开始前对涉及的主键做排序:
sort.Ints([]int{fromID, toID}),再按升序查询并加锁 - 用
Clauses(clause.Locking{Strength: "UPDATE"})显式声明行级锁,避免 GORM 隐式全表扫描(尤其在无主键条件或 LIKE 查询时) - 避免在事务中混用
SELECT(无锁)和后续UPDATE——应改用SELECT ... FOR UPDATE一次性锁定,否则可能读到旧值再更新失败 - 慎用
FirstOrInit()或FindOrCreate():它们内部可能先 SELECT 再 INSERT,若并发高且无唯一索引约束,会放大锁竞争
别让 *sql.Tx 在 goroutine 间共享
Gin 的 handler 天然并发,但 *sql.Tx 不是线程安全的。常见反模式:主 goroutine 调 db.Begin(),然后把 tx 传给多个子 goroutine 去写 DB。结果是未定义行为——可能卡在 tx.Exec()、panic 报 transaction has already been committed or rolled back,或数据错乱。
- 每个需要写 DB 的 goroutine 必须自己调
db.BeginTx(ctx, nil),各自管理生命周期 - 若需跨 goroutine 强一致性(比如转账必须原子),应放弃“共享 tx”,改用数据库层行锁:
SELECT ... FOR UPDATE WHERE id IN (?, ?)加锁后,在单个 goroutine 内完成所有更新 - 务必检查
tx.Commit()前是否有 panic:用defer tx.Rollback()仅作保险,核心逻辑必须配if err != nil { tx.Rollback(); return err } - 本地验证连接泄漏:设
db.SetMaxOpenConns(2)+db.SetConnMaxLifetime(1 * time.Second),跑几轮压测,看是否快速复现all goroutines are asleep
最易被忽略的一点:死锁预防不是加锁越细越好,而是让锁的获取顺序可预测、可收敛。应用层排序 ID、统一 SQL 执行路径、拒绝 ORM 黑盒批量操作——这些比调大连接池或换数据库更治本。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











