update 返回 0 行影响说明发生并发写冲突,属乐观锁正常表现,应视为需重试信号而非错误;须限制重试次数、采用指数退避策略,避免无限循环或雪崩。

并发写冲突时 UPDATE 返回 0 行影响,说明什么
这通常不是错误,而是数据库层面的乐观锁表现:两条并发请求同时读取同一条记录,都基于旧值构造 UPDATE 语句,后执行的那条因 WHERE 条件不匹配(比如 version = 1 已被前序事务更新为 2)而未修改任何行。GORM 或原生 db.Exec 都会返回 RowsAffected == 0,而非报错。
关键判断点在于:不能把 RowsAffected == 0 当作“数据不存在”或“逻辑失败”,而应视为“需重试”的信号。
- 避免在事务外盲目重试——可能造成无限循环或雪崩
- 不要用
SLEEP硬等待,Go 协程调度不可控,且浪费资源 - 必须限制最大重试次数(如 3–5 次),否则长尾延迟不可控
用 for 循环 + 指数退避实现可控重试
Go 原生没有内置重试库,但用 for + time.Sleep 就足够清晰。重点是退避策略:固定间隔易引发重试碰撞,指数退避能分散压力。
const maxRetries = 3
for i := 0; i 0 {
return nil // 成功退出
}
if i == maxRetries {
return errors.New("update failed after retries")
}
time.Sleep(time.Millisecond * time.Duration(1
1 是标准指数退避,比 <code>rand.Intn更可预测、更易调试- 重试前不重新查询最新值——除非业务逻辑要求强一致性读,否则应在循环内重读
version字段 - 若使用 GORM,注意
Save()默认不校验 version,必须显式用Where("version = ?", v).Save()
在事务中嵌套重试时,Begin 和 Rollback 的位置很关键
常见错误是每次重试都新建事务,导致嵌套事务或连接泄漏。正确做法是:事务只开启一次,所有重试都在同一事务上下文中进行。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
tx := db.Begin()
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
for i := 0; i 0 {
return tx.Commit()
}
if i
- 事务内重试必须重读最新
version,否则下次还是拿旧值撞墙 - 不要在循环里调
tx.Commit()后继续重试——已提交的事务无法回滚 - 若用 GORM,
tx.Save()不自动重读,需手动tx.First(&o, id)刷新结构体
PostgreSQL 的 SELECT FOR UPDATE 不适合高并发场景
虽然它能规避冲突,但本质是悲观锁:阻塞其他事务直到本事务结束。在订单支付、库存扣减等高频场景下,容易形成锁队列,吞吐骤降。
真正需要的是「无锁重试」,而非「排队等待」。只有当冲突率极低(SELECT FOR UPDATE 才值得考虑。
- MySQL 的
SELECT ... FOR UPDATE在 RR 隔离级下会加间隙锁,可能锁住更大范围 - SQLite 不支持行级锁,
FOR UPDATE退化为表锁,完全不可用 - 如果已有唯一索引约束(如
order_no),可用INSERT ... ON CONFLICT替代更新逻辑,天然幂等
重试机制的复杂点不在代码长度,而在对「何时重读」「何时放弃」「事务边界是否干净」的精确控制。漏掉任意一环,就可能从「防冲突」变成「制造热点」。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










