乐观锁需同时满足where version = ?和检查受影响行数,缺一不可;select … for update在read committed及以上隔离级别才生效;redis实现须用原子操作如set ex nx或lua脚本。

UPDATE 语句里加 WHERE version = ? 就是乐观锁?
不是。只加 WHERE version = ? 是乐观锁的必要条件,但不是充分条件——漏掉更新后检查影响行数,等于没锁。
典型错误是执行完 UPDATE 就继续往下走,不判断是否真改成功了。比如库存扣减,version 已被别人改过,你的 UPDATE 实际影响 0 行,但代码没感知,还按“扣成功”走了后续逻辑。
- 必须用数据库驱动返回的“受影响行数”做判断,mysql 的
executeUpdate()、postgresql 的RETURNING或 ORM 中的affected_rows都得检查 -
UPDATE product SET stock = stock - 1, version = version + 1 WHERE id = 123 AND version = 42—— 这条语句本身不报错,哪怕 version 不匹配,它只是安静地改了 0 行 - 如果用 mybatis,别依赖
@SelectKey或自动回填;要显式查rowcount,spring JDBC 可用JdbcTemplate.update()返回值
SELECT … FOR UPDATE 在什么事务隔离级别下才真正起作用?
只在 READ COMMITTED 或更高(如 REPEATABLE READ)下生效;设成 READ UNCOMMITTED 时,FOR UPDATE 会被忽略,不加锁。
常见陷阱是框架默认开事务但没显式设隔离级别,底层用了 READ UNCOMMITTED(比如某些旧版 MySQL 配置或测试环境),结果 SELECT ... FOR UPDATE 形同虚设。
- MySQL InnoDB 默认是
REPEATABLE READ,这时SELECT ... FOR UPDATE会加记录锁 + 间隙锁,能防幻读 - PostgreSQL 默认
READ COMMITTED,FOR UPDATE只锁命中的行,不锁间隙,高并发下可能多扣一次(需配合唯一约束或重试) - Spring 的
@Transactional(isolation = Isolation.READ_COMMITTED)要写全,别只写@Transactional依赖默认——不同数据库默认值不同
用 Redis 实现分布式乐观锁,为什么 incr + expire 不能直接组合?
因为 INCR 和 EXPIRE 不是原子操作,中间若进程崩溃或网络断开,key 会永久存在,导致死锁。
真实场景中,有人用 INCR 判断是否首次访问,再立刻 EXPIRE 设过期,但这两步之间没有锁保障,多个客户端可能同时通过 INCR == 1 判断,都去执行业务逻辑。
- 必须用 Lua 脚本把判断、设置、过期三步包进一个原子操作,例如:
EVAL "if redis.call('incr', KEYS[1]) == 1 then redis.call('expire', KEYS[1], ARGV[1]) end return redis.call('get', KEYS[1])" 1 order:lock:123 30 - 别用
SETNX + EXPIRE组合,同样非原子;应改用SET key value EX seconds NX(Redis 2.6.12+),这是真正原子的“设值且带过期” - 注意:Redis 乐观锁本质是“检测变更”,不是“阻止变更”,所以仍需配合业务层的版本比对或 CAS 逻辑,不能只靠 key 是否存在
MyBatis-Plus 的 @Version 注解为啥有时不生效?
因为默认只对 updateById() 和 update()(带 Wrapper)生效,对 updateBatchById()、自定义 xml 中的 UPDATE、或手写 SqlSession.update() 完全不触发。
更隐蔽的问题是:实体类字段类型和数据库字段类型不一致,比如 Java 用 integer,DB 用 BIGINT,MyBatis-Plus 生成的 WHERE 条件可能因类型转换失败而丢掉 version 判断。
- 确认你调用的是 MP 自带的 update 方法,而不是自己写
@Update("UPDATE ...");后者要手动拼WHERE version = #{version} - 实体类
@Version字段必须是包装类型(Integer/Long),不能是基本类型(int/long),否则空值无法识别 - 开启 MP 的 SQL 日志(
mybatis-plus.configuration.log-impl=org.apache.ibatis.Logging.stdout.StdOutImpl),看生成的 SQL 里有没有AND version = ?
事情说清了就结束。锁机制从来不是开关,而是和事务边界、网络稳定性、ORM 行为、甚至数据库配置深度耦合的一组约束——少盯一个点,就可能在凌晨三点收到库存超卖报警。