首页/数据/SQL 速查/乐观锁:版本号条件更新
变更与事务52 条

乐观锁:版本号条件更新

不提前加锁,用版本号在 UPDATE 时校验:更新影响行数为 0 说明发生并发修改,业务层决定重试或报冲突。

SQL 速查数据更新于 2026-10-13

版本号条件更新

乐观锁不提前锁住数据,而是在更新时校验版本:读出记录时记住版本号,写回时把旧版本号写进条件,只有影响行数为一时才算成功。影响行数为零说明这期间有别的请求改过它,业务层据此决定重试还是提示冲突。它的价值在于把并发冲突的判断从数据库的锁等待挪到业务代码里,读多写少的场景下几乎没有额外开销,代价是冲突时必须由应用决定怎么收敛。

基本信息

适用平台MySQL 8.0 / PostgreSQL 12 及以上,应用层通用模式
读取阶段查询时带上版本号字段
写入阶段更新条件里带旧版本号,同时把版本号加一
冲突判定影响行数为零即表示发生并发修改
PostgreSQL 变体可用系统列作为版本标记,省去业务字段

典型使用场景

  • 内容编辑:两个人同时编辑同一篇文档时避免后写覆盖前写
  • 库存扣减:校验与扣减在一条语句里完成,避免超卖
  • 工单状态流转:防止重复点击导致的重复流转
  • 配置修改:多人维护配置项时避免互相覆盖
  • 外部接口幂等:把版本号作为乐观锁的一部分实现幂等写入

条件更新写法

条件更新写法
-- 读取时记住版本号(这里是 7)
SELECT id, content, version FROM documents WHERE id = 42;

-- 写入时带旧版本号,并把版本号加一
UPDATE documents
SET content = '新内容', version = version + 1
WHERE id = 42 AND version = 7;
-- 影响行数为 1:更新成功
-- 影响行数为 0:版本冲突,需要重新读取后再试或提示用户

-- 库存扣减:把校验与扣减合并,避免超卖
UPDATE stock
SET qty = qty - 1, version = version + 1
WHERE sku_id = 100 AND version = 3 AND qty >= 1;

-- PostgreSQL:也可以用系统列作为版本标记,无需业务字段
-- UPDATE documents SET content = '新内容'
-- WHERE id = 42 AND xmin::text = '旧值';

要点对照

环节做法容易出错的地方
读取把版本号一并查出只读业务字段,写回时无处校验
写入条件里带旧版本号并自增忘写条件,锁形同虚设
判定按影响行数判断成功与否口径不统一导致判断失真
重试冲突后重新读取再试沿用旧版本号重试,永远冲突
查询与更新尽量合成一条语句先查后改之间有窗口期
影响行数的口径要统一:MySQL 默认返回的是「实际发生变化」的行数,值没变时可能返回零,因此更新条件里带上版本号自增可以避免这种歧义。判断冲突与连接配置要放在一起确认。
把校验与修改合并到一条语句是避免竞态的关键:先查后改之间永远存在时间窗口,期间别的请求可能已经改过数据。

易错点

  • 更新条件里只写自增版本号忘了带旧版本号判断,乐观锁完全失效,并发覆盖照样发生
  • 影响行数口径与连接配置不一致(变化行数与匹配行数),冲突判断出现误判
  • 冲突后沿用旧版本号重试,每次都会冲突,应重新读取最新版本
  • 先查询再更新分两条语句执行,中间窗口期内的并发修改被覆盖
  • 把乐观锁当万能方案:写热点数据时冲突率极高,重试成本反而不如悲观锁

数据来源与参考

  • PostgreSQL Documentation · Concurrency Control
  • MySQL 8.0 Reference Manual · UPDATE
​‌‌​​‌​​​‌‌​‌‌‌‌​‌‌​​​‌‌​‌‌‌​​‌‌​​‌​‌‌‌​​‌‌‌​​​‌​‌‌​‌​​‌​‌‌‌​​​‌​‌‌​‌​​‌​‌‌​​​‌‌​‌‌‌​‌​​​‌‌​‌‌‌‌​​‌​‌‌‌​​‌‌​​​‌‌​‌‌​‌‌‌‌​‌‌​‌‌​‌