SQL中版本号自动递增应直接用UPDATE ... SET version = version + 1 WHERE id = ? AND version = ?,并检查影响行数;失败则重试,不可省略版本校验或拆分读写。

SQL中实现版本号字段的自动递增更新
直接在UPDATE语句中用version = version + 1是最常用且可靠的方式,前提是该字段初始值为0或1,且表结构已定义version为整型(如INT或BIGINT)。不要依赖触发器或应用层计数——容易在并发写入时产生脏读或覆盖。
常见错误是写成SET version = version + 1 WHERE id = ? AND version = ?却忘了在WHERE里校验旧版本号,导致“丢失更新”。这属于乐观锁基础逻辑,必须带上version条件才能防止覆盖他人修改。
- 执行后检查
ROW_COUNT()(MySQL)或GET DIAGNOSTICS ROW_COUNT(PostgreSQL)是否为1,为0说明版本冲突,需重试或报错 - 避免在UPDATE中混用
SELECT MAX(version)+1——会产生竞态,且无法保证原子性 - 如果使用UUID主键,仍可加
version字段;但别用UUID做版本标识本身,它不满足单调递增与可比性
PostgreSQL中用GENERATED ALWAYS AS IDENTITY配合版本列
PostgreSQL 12+支持GENERATED ALWAYS AS IDENTITY,但它只适用于主键自增,不能直接用于版本号。版本号必须由业务逻辑控制,所以实际做法仍是手动UPDATE ... SET version = version + 1。不过可以借助RETURNING子句一次性拿到新版本号:
UPDATE documents SET content = 'new text', version = version + 1 WHERE id = 123 AND version = 5 RETURNING version;
这条语句返回更新后的version值(即6),应用层可立即用于后续日志或响应。注意:若WHERE条件不匹配(比如别人已升到6),则RETURNING无结果,不是空值而是零行——这点常被忽略,导致程序误判成功。
MySQL中处理高并发下的版本更新失败重试
MySQL默认事务隔离级别是REPEATABLE READ,但这对乐观锁没帮助——版本冲突本质是应用层逻辑,数据库不自动介入。真正要解决的是“更新失败后怎么安全重试”。关键点有三个:
- 每次重试前必须重新
SELECT ... FOR UPDATE查当前version和业务数据,否则可能基于过期快照重试 - 重试次数建议设上限(如3次),避免死循环;第3次失败应抛出
OptimisticLockException类错误 - 不要在事务里嵌套多次UPDATE重试——会延长锁持有时间,加剧争用;应把“读-改-写”作为单次事务单位
典型反模式:UPDATE t SET v=v+1 WHERE id=1不带版本校验,然后靠SELECT v再判断——这中间存在时间窗口,别人可能已更新两次,你却只加了一次。
SQLite中模拟乐观锁时要注意WAL模式的影响
SQLite在WAL模式下支持多读一写,并发读不会阻塞,但乐观锁仍有效:因为UPDATE会获取reserved lock,多个写请求会串行化。问题在于,如果应用用的是sqlite3_exec且未开启事务,每次UPDATE都是独立事务,SELECT和UPDATE之间可能被其他连接插入干扰。
正确做法是显式包裹在BEGIN IMMEDIATE事务中:
BEGIN IMMEDIATE; SELECT version, content FROM docs WHERE id = 42; -- 应用层计算新内容与新version UPDATE docs SET content = ?, version = ? WHERE id = 42 AND version = ?; COMMIT;
这里version = ?填的是刚才SELECT出来的值。如果UPDATE影响行为0,就ROLLBACK并重试。注意:不要用BEGIN DEFERRED,它不阻止其他写入提前获得锁,可能导致幻读。
版本化不是加个字段就能跑起来的事——它要求每次写操作都携带前置状态校验,而这个校验必须落在同一事务、同一语句中才真正可靠。漏掉WHERE里的版本条件,或者把SELECT和UPDATE拆成两个事务,都会让版本号变成装饰品。










