必须检查rowsaffected()才能确认乐观锁更新是否生效,因mysql/postgresql的update在where不匹配时仍返回err==nil;正确做法是执行后立即调用res.rowsaffected(),返回0即表示版本冲突失败,需重新select最新version并重试,最多3次且不可嵌套事务。

直接结论:用 UPDATE ... WHERE version = ? + RowsAffected() 判断,配合最多 3 次重试,别碰 updated_at 或事务嵌套重试逻辑。
为什么必须检查 RowsAffected() 才算真正生效
MySQL 和 PostgreSQL 的 UPDATE 即使 WHERE 不匹配也返回 err == nil,不会报错。只靠 “没报错” 就认为更新成功,等于默认放弃并发控制。
- 错误写法:
_, err := db.Exec("UPDATE users SET name=? WHERE id=? AND version=?", name, id, oldVer),之后不查影响行数 - 正确做法:立刻调用
res.RowsAffected();返回0表示版本已变,当前操作被拒绝 - GORM 用户注意:
db.Model(&u).Where("version = ?", u.Version).Update("name", name)后,必须检查result.RowsAffected,不能只看result.Error
version 字段怎么设才靠谱
别用 updated_at 或 timestamp 当版本号——精度低、可被客户端伪造、跨服务时钟不同步直接导致误判。
- 类型必须是单调递增整型:
INT或BIGINT(对应 Go 的int64),初始值设为1而非0 - 建表语句示例:
ALTER TABLE users ADD COLUMN version BIGINT NOT NULL DEFAULT 1 - 更新 SQL 必须包含两处:
SET version = version + 1和WHERE version = ?,缺一不可 - 别在
version上建唯一索引——它本就不唯一,还可能拖慢写入
重试逻辑该封装成什么样
重试不是循环加 time.Sleep,而是「查最新 + 判断 + 更新」三步闭环,且必须限制次数。
- 每次重试前,必须重新
SELECT当前行(含最新version),不能复用旧值 - 建议上限 3 次:第 1 次失败后等 1–5ms 再试,第 2 次失败后等 5–10ms,第 3 次失败直接返回错误
- 别把重试塞进数据库事务里——乐观锁本身不依赖事务隔离级别,长事务反而放大阻塞风险
- 示例片段(非完整函数):
for i := 0; i
什么场景下不该硬上乐观锁
乐观锁不是银弹。高频单字段变更(如阅读数、点赞数)或跨多表强一致性更新,强行加 version 只会让代码更脆、性能更差。
- 计数类更新:直接用
UPDATE article SET views = views + 1 WHERE id = ?,原子自增比 CAS 更轻量 - 多表关联更新(如订单+库存+物流):乐观锁只能保单行,中间态不一致风险高,该上 Saga 或消息最终一致性
- 冲突率长期 >15% 的热点行(比如秒杀商品):重试成本远超加锁开销,此时应考虑分片、缓存预热或改用悲观锁
最容易被忽略的一点:乐观锁成败只取决于 UPDATE 语句的 WHERE 条件是否命中,和事务是否开启、隔离级别设成什么都没关系。别为了“保险”而包一层 Begin/Commit——它不增加安全性,只增加锁持有时间和死锁概率。











