MyBatis-Plus乐观锁失效的常见原因及解决方案

 更新时间:2025年05月21日 09:10:17   作者:李少兄  
在高并发场景下,乐观锁是保证数据一致性的核心工具,MyBatis-Plus 通过@Version注解和OptimisticLockerInnerInterceptor插件,为开发者提供了优雅的乐观锁实现,然而,开发者在实际使用中常遇到乐观锁“不生效”的问题,所以本文给大家介绍了乐观锁失效的常见原因及解决方案

前言

在高并发场景下,乐观锁是保证数据一致性的核心工具。MyBatis-Plus 作为 Java ORM 框架的佼佼者,通过 @Version 注解和 OptimisticLockerInnerInterceptor 插件,为开发者提供了优雅的乐观锁实现。然而,开发者在实际使用中常遇到乐观锁“不生效”的问题。

一、乐观锁的核心原理

1. 什么是乐观锁?

乐观锁(Optimistic Locking)是一种假设“冲突很少发生”(乐观锁假设大多数操作不会发生冲突,仅在更新时检查数据版本。若版本匹配,则更新成功;否则,抛出异常)的并发控制策略。其核心思想是:

  • 读取数据时:记录当前版本号(如 version 字段)。
  • 更新数据时:校验版本号是否匹配。若匹配,则更新成功并递增版本号;否则,抛出异常(如 OptimisticLockException)。

2. MyBatis-Plus 的实现机制

MyBatis-Plus 通过以下三步实现乐观锁:

  • 实体类标注 @Version 注解:标记版本号字段。
  • 配置 OptimisticLockerInnerInterceptor 插件:拦截 SQL 并自动处理版本号条件。
  • 更新操作时自动校验版本号:生成类似 UPDATE ... WHERE version = ? 的 SQL。

二、乐观锁失效的常见原因

1. 插件未正确配置

问题:未注册 OptimisticLockerInnerInterceptor 插件,导致 MyBatis-Plus 无法自动处理版本号逻辑。

示例代码

@Configuration
public class MyBatisPlusConfig {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // 必须添加乐观锁插件
        interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
        return interceptor;
    }
}

2. 实体类字段未标注 @Version

问题:实体类中未使用 @Version 注解,MyBatis-Plus 无法识别版本号字段。

示例代码

import com.baomidou.mybatisplus.annotation.Version;

public class User {
    // 正确标注版本号字段
    @Version
    private Integer version;

    // 其他字段...
}

3. 数据库字段未初始化

问题:数据库表的 version 字段为 NULL 或未设置默认值,导致首次更新时无法比较版本号。

解决方案

-- 确保 version 字段有初始值
ALTER TABLE user ADD COLUMN version INT NOT NULL DEFAULT 1;

4. 更新操作未基于查询后的数据

问题:直接构造新对象更新,未读取数据库中的版本号,导致乐观锁失效。

示例代码

// 错误方式:直接构造新对象
User user = new User();
user.setId(1L);
user.setName("New Name");
userMapper.updateById(user); // version 未传递,乐观锁失效

// 正确方式:先查询数据
User user = userMapper.selectById(1L); // 查询当前数据(包含 version)
user.setName("New Name");
userMapper.updateById(user); // 自动处理 version 递增

5. 自定义 SQL 未包含版本号条件

问题:使用自定义 SQL 更新时,未显式添加 version 条件,导致乐观锁插件失效。

解决方案

<!-- 正确方式:显式添加 version 条件 -->
<update id="updateUser">
    UPDATE user
    SET name = #{name}, version = version + 1
    WHERE id = #{id} AND version = #{version}
</update>

6. 事务未正确开启

问题:乐观锁依赖事务的原子性,若事务未开启,可能导致版本号更新失败。

示例代码

@Transactional
public void updateData(Long id, String newName) {
    User user = userMapper.selectById(id); // 查询数据
    user.setName(newName);
    userMapper.updateById(user); // 事务提交后版本号递增
}

7. 并发测试未触发冲突

问题:未模拟并发场景,无法观察到乐观锁的校验效果。

测试代码

@Test
public void testOptimisticLock() {
    User user1 = userMapper.selectById(1L); // version=1
    User user2 = userMapper.selectById(1L); // version=1

    user1.setName("Thread 1");
    userMapper.updateById(user1); // version 变为 2

    user2.setName("Thread 2");
    try {
        userMapper.updateById(user2); // 此处应抛出 OptimisticLockException
    } catch (OptimisticLockException e) {
        System.out.println("乐观锁生效,更新失败");
    }
}

8. 版本字段类型不匹配

问题@Version 注解支持的类型为 Integer/Long/Date/Timestamp/LocalDateTime,若使用其他类型(如 String),会导致乐观锁失效。

示例代码

// 正确类型
@Version
private Integer version; // 或 Long/LocalDateTime

// 错误类型
@Version
private String version; // 类型不匹配,乐观锁失效

9. MyBatis-Plus 版本过低

问题:旧版本可能存在 Bug 或功能限制。
解决方案:升级至最新版本(建议 3.5.7+)。

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.7</version>
</dependency>

三、完整解决方案示例

1. 数据库表设计

CREATE TABLE user (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(100),
    version INT NOT NULL DEFAULT 1 -- 初始化版本号
);

2. 实体类配置

import com.baomidou.mybatisplus.annotation.*;

@TableName("user")
public class User {
    @TableId(type = IdType.AUTO)
    private Long id;

    private String name;

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

    // Getter & Setter
}

3. 配置插件

@Configuration
public class MyBatisPlusConfig {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
        return interceptor;
    }
}

4. 业务逻辑代码

@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    @Transactional
    public void updateUser(Long id, String newName) {
        User user = userMapper.selectById(id); // 查询当前数据
        user.setName(newName);
        int result = userMapper.updateById(user); // 自动处理 version
        if (result == 0) {
            throw new OptimisticLockException("乐观锁更新失败");
        }
    }
}

四、进阶技巧与注意事项

1. 自定义版本号字段名

若数据库字段名与实体类字段名不一致,可通过 @TableId 或 @TableField 指定映射:

@TableField("ver")
@Version
private Integer version;

2. 多条件更新的版本号校验

在复杂更新场景中,需手动处理版本号逻辑:

UpdateWrapper<User> wrapper = new UpdateWrapper<>();
wrapper.eq("id", 1L).eq("version", user.getVersion());
user.setVersion(user.getVersion() + 1);
userMapper.update(user, wrapper);

3. 分布式环境下的版本号一致性

在分布式系统中,确保所有节点共享同一数据库,避免版本号冲突。若需跨节点同步,可结合 Redis 或数据库中间件实现。

4. 性能优化建议

  • 减少事务范围:仅在必要时开启事务,避免长事务影响并发性能。
  • 批量更新的版本号处理:对批量操作需逐条校验版本号,或使用分片策略。

五、总结

MyBatis-Plus 的乐观锁机制本质是“通过版本号比对保证数据一致性”,但其生效依赖多个环节的正确配置与实现。以下是关键要点回顾:

关键点说明
插件配置必须注册 OptimisticLockerInnerInterceptor
实体类注解使用 @Version 标注版本号字段
数据库初始化version 字段必须有非空初始值
更新逻辑基于查询后的数据更新,避免直接构造新对象
自定义 SQL显式添加 version 条件
事务管理使用 @Transactional 确保事务一致性
并发测试模拟多线程场景验证乐观锁效果
字段类型与版本兼容性确保字段类型为 Integer/Long/LocalDateTime,升级 MyBatis-Plus 至最新版

官方文档 MyBatis-Plus 乐观锁指南

以上就是MyBatis-Plus乐观锁失效的常见原因及解决方案的详细内容,更多关于MyBatis-Plus乐观锁失效的资料请关注脚本之家其它相关文章!

相关文章

  • @Cacheable 拼接key的操作

    @Cacheable 拼接key的操作

    这篇文章主要介绍了@Cacheable 拼接key的操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-02-02
  • 常用校验注解之@NotNull,@NotBlank,@NotEmpty的区别及说明

    常用校验注解之@NotNull,@NotBlank,@NotEmpty的区别及说明

    这篇文章主要介绍了常用校验注解之@NotNull,@NotBlank,@NotEmpty的区别及说明,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-01-01
  • JVM 运行时数据区与JMM 内存模型

    JVM 运行时数据区与JMM 内存模型

    这篇文章主要介绍了JVM 运行时数据区与JMM 内存模型,文章围绕主题展开详细的内容介绍,具有一定的参考价值。需要的朋友可以参考一下
    2022-07-07
  • Java中的递增i++与++i的实现原理详解

    Java中的递增i++与++i的实现原理详解

    这篇文章主要介绍了Java中的i++与++i的实现原理详解,在Java中,i++是一种常见的递增操作符,用于将变量i的值增加1,它是一种简洁且方便的方式来实现循环和计数功能,i++可以用于各种情况,本文来看一下其实现原理,需要的朋友可以参考下
    2023-10-10
  • Java通过动态代理实现一个简单的拦截器操作

    Java通过动态代理实现一个简单的拦截器操作

    这篇文章主要介绍了Java通过动态代理实现一个简单的拦截器操作,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-07-07
  • Java中Maven项目导出jar包配置的示例代码

    Java中Maven项目导出jar包配置的示例代码

    这篇文章主要介绍了Java中Maven项目导出jar包配置的示例代码,需要的朋友可以参考下
    2018-11-11
  • Java源码解析之接口Collection

    Java源码解析之接口Collection

    Collection是List、Queue和set的超集,它直接继承于Iterable,也就是说所有的Collection集合类都支持foreach循环.除此之外呢,Collection也是面向接口编程的典范,它可以在多种实现类间转换,这就是面向对象编程的厉害之处.接下来就随着小编一起去看看吧,需要的朋友可以参考下
    2021-05-05
  • 关于ThreadLocal和InheritableThreadLocal解析

    关于ThreadLocal和InheritableThreadLocal解析

    这篇文章主要介绍了关于ThreadLocal和InheritableThreadLocal解析,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2025-03-03
  • springboot使用dubbo和zookeeper代码实例

    springboot使用dubbo和zookeeper代码实例

    这篇文章主要介绍了springboot使用dubbo和zookeeper代码实例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2019-11-11
  • Java 数据结构与算法系列精讲之队列

    Java 数据结构与算法系列精讲之队列

    这篇文章主要介绍了Java队列数据结构的实现,队列是一种特殊的线性表,只允许在表的队头进行删除操作,在表的后端进行插入操作,队列是一个有序表先进先出,想了解更多相关资料的小伙伴可以参考下面文章的详细内容
    2022-02-02

最新评论