高并发下数据库事务冲突必然发生,关键在于优雅收场而非避免;需区分可重试错误(如序列化失败、死锁),设置3~5次指数退避重试,超限则返回429或降级。

高并发下数据库事务冲突不是“能不能避免”的问题,而是“怎么优雅收场”的问题——它必然发生,关键在于不崩溃、不丢数据、不误判。
事务提交失败时别直接 panic
MySQL 或 PostgreSQL 在高并发更新同一行时,常抛出类似 ERROR: could not serialize access due to concurrent update(PostgreSQL)或 Deadlock found when trying to get lock(MySQL)这类错误。硬编码 if err != nil { log.Fatal(err) } 会让整个服务雪崩。
- 必须区分错误类型:用
sql.ErrNoRows、pgconn.IsSQLState(PostgreSQL)或mysql.MySQLError(MySQL)判断是否为可重试的事务冲突 - 对可重试错误(如序列化失败、死锁)设置重试上限,通常 3~5 次足够;超过则返回
http.StatusTooManyRequests或降级逻辑 - 每次重试前加指数退避(如
time.Sleep(time.Millisecond * time.Duration(1),避免重试风暴
用 SELECT FOR UPDATE + 显式事务控制写操作顺序
当多个请求争抢修改同一条记录(如库存扣减、账户余额变更),靠应用层 if-check-update 一定出错。数据库行锁才是唯一可信的协调者。
- 必须在事务内执行
SELECT ... FOR UPDATE,且该语句要命中索引(否则升级为表锁) - 不要在
FOR UPDATE后做耗时操作(如 HTTP 调用、复杂计算),否则锁持有时间过长,放大阻塞 - 注意 MySQL 的
autocommit=1默认行为:单独执行SELECT FOR UPDATE不会加锁,必须显式db.Begin() - PostgreSQL 中若使用
READ COMMITTED隔离级别,FOR UPDATE只锁当前快照命中的行;升级到REPEATABLE READ才能防止幻读,但代价是更易冲突
乐观锁适合低冲突、高读取场景
当冲突概率低于 5%(例如配置中心、用户资料页),乐观锁比行锁更轻量,避免阻塞和死锁风险。
- 字段设计上必须带
version或updated_at时间戳,且更新 SQL 中强制校验:UPDATE users SET name=?, version=version+1 WHERE id=? AND version=? - Go 层检查
result.RowsAffected()是否为 1;为 0 表示版本已变,需重读再试 - 不能把乐观锁当成“免锁银弹”:如果业务逻辑中需要基于旧值做多次判断(如“余额 ≥ 扣款额 && 用户状态正常 && 今日未超限”),仍建议用
FOR UPDATE保序
测试阶段就暴露事务竞争问题
本地跑单测没报错,上线后突然大量 TransactionRetryError?大概率是测试没模拟真实并发压力。
- 单元测试里别只用
t.Parallel(),要在事务函数内手动启 10+ goroutine 并发调用,观察是否出现RowsAffected == 0或数据库错误 - 集成测试连接真实数据库时,必须加
go test -p=1 ./...,否则多个包测试共享同一 DB 实例会导致 DROP TABLE 冲突、schema 错乱 - 用
go run -race编译运行压测脚本,能提前捕获 Go 层面的变量竞态(比如共用tx对象、未隔离的缓存 map)
真正难的从来不是加锁或重试,而是判断哪条路径该悲观、哪条该乐观、哪条该异步化——这取决于你对业务冲突模式的真实观测,而不是文档里的“推荐做法”。


















