Skip to content

MyBatis-Plus 乐观锁

  乐观锁用于解决并发更新问题,MyBatis-Plus 通过 @Version 注解和乐观锁插件,一行配置即可实现。

一、为什么需要乐观锁?

并发更新问题:

  线程 A:读取 user.age = 25
  线程 B:读取 user.age = 25
  线程 A:更新 age = 26 → 提交
  线程 B:更新 age = 30 → 提交(覆盖了 A 的更新!)

  结果:A 的更新丢失了,这就是并发更新覆盖问题。
乐观锁解决思路:

  ① 数据表增加 version 字段(版本号)
  ② 读取数据时,获取当前 version
  ③ 更新时,WHERE 条件带上 version
  ④ 更新后 version + 1
  ⑤ 如果 version 不匹配(别人已经改过),更新失败

  UPDATE user SET age = 26, version = 2 WHERE id = 1 AND version = 1
  如果 version 已经是 2(被 B 改了),这次更新影响行数为 0,更新失败

二、配置方式

2.1 添加乐观锁插件

java
@Configuration
public class MybatisPlusConfig {

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();

        // 分页插件
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));

        // 乐观锁插件
        interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());

        return interceptor;
    }
}

2.2 实体类标注

java
@Data
public class User {

    private Long id;
    private String name;
    private Integer age;

    @Version
    private Integer version;  // 版本号字段
}

2.3 数据库加字段

sql
ALTER TABLE user ADD COLUMN version INT DEFAULT 1 COMMENT '版本号';

三、使用效果

java
// 正常更新
User user = userMapper.selectById(1L);
// user: {id=1, name="张三", age=25, version=1}

user.setAge(26);
int rows = userMapper.updateById(user);
// 执行 SQL:UPDATE user SET age = 26, version = 2
//           WHERE id = 1 AND version = 1
// rows = 1(更新成功)

// 并发更新时
User userA = userMapper.selectById(1L);  // version=1
User userB = userMapper.selectById(1L);  // version=1

userA.setAge(26);
userMapper.updateById(userA);  // 成功,version 变为 2

userB.setAge(30);
int rows = userMapper.updateById(userB);
// 执行 SQL:UPDATE user SET age = 30, version = 2
//           WHERE id = 1 AND version = 1
// rows = 0(更新失败,version 已经是 2 了)

四、完整示例

java
@Service
public class UserService extends ServiceImpl<UserMapper, User> {

    /**
     * 扣减余额(带乐观锁)
     */
    @Transactional
    public boolean deductBalance(Long userId, BigDecimal amount) {
        // 查询用户
        User user = getById(userId);
        if (user.getBalance().compareTo(amount) < 0) {
            throw new RuntimeException("余额不足");
        }

        // 扣减余额
        user.setBalance(user.getBalance().subtract(amount));

        // 更新(带乐观锁)
        int rows = userMapper.updateById(user);
        if (rows == 0) {
            // 版本号不匹配,说明被其他人修改了
            throw new RuntimeException("并发更新失败,请重试");
        }

        return true;
    }

    /**
     * 带重试的乐观锁更新
     */
    public boolean deductBalanceWithRetry(Long userId, BigDecimal amount) {
        int maxRetries = 3;
        for (int i = 0; i < maxRetries; i++) {
            User user = getById(userId);
            if (user.getBalance().compareTo(amount) < 0) {
                return false;
            }
            user.setBalance(user.getBalance().subtract(amount));
            int rows = userMapper.updateById(user);
            if (rows > 0) {
                return true;
            }
            // 更新失败,重试
        }
        throw new RuntimeException("更新失败,超过最大重试次数");
    }
}

五、乐观锁 vs 悲观锁

维度乐观锁悲观锁
实现方式@Version + 版本号SELECT ... FOR UPDATE
锁粒度无锁,版本号校验行锁
并发性能
适用场景读多写少,冲突概率低写多,冲突概率高
失败处理重试或提示用户排队等待
实现复杂度

六、乐观锁原理

MyBatis-Plus 乐观锁执行流程:

  ① 数据查询
     SELECT id, name, age, version FROM user WHERE id = 1
     → version = 1

  ② 更新时,拦截器修改 SQL
     UPDATE user SET age = 30, version = 2 WHERE id = 1 AND version = 1

  ③ 执行更新
     → 如果 version 匹配 → 更新成功,rows = 1
     → 如果 version 不匹配 → 更新失败,rows = 0

  ④ 拦截器检查更新结果
     → rows = 0 → 抛 OptimisticLockException(可选配置)

七、注意事项

① 仅支持 updateById 和 update(entity, wrapper)
   自定义 SQL 不会自动加乐观锁

② 支持的数据类型
   int, Integer, long, Long, Date, Timestamp, LocalDateTime

③ 批量更新不支持
   updateBatchById 不会触发乐观锁

④ 版本号类型选择
   - int:数字递增(推荐)
   - LocalDateTime:时间戳版本

⑤ 不要手动修改 version
   让插件自动管理,手动修改会导致逻辑混乱

八、速记总结

乐观锁三步走:
  ① 数据库加字段:version INT DEFAULT 1
  ② 插件加拦截器:OptimisticLockerInnerInterceptor
  ③ 实体加注解:@Version

更新流程:
  SELECT → version = 1
  UPDATE SET ... AND version = 1(如果 version 变了,更新 0 行)

速记口诀:
  版本号字段加注解,插件拦截改 SQL,
  WHERE 条件带版本,更新失败需重试。

九、面试要点

问题答案要点
乐观锁原理?版本号机制,UPDATE 时 WHERE 带 version,不匹配则失败
乐观锁和悲观锁区别?乐观锁无锁,版本号校验;悲观锁行锁,SELECT FOR UPDATE
乐观锁失败怎么办?重试机制或提示用户刷新
自定义 SQL 支持乐观锁吗?不支持,需要手动加 version 条件