MyBatis-Plus 提供了强大的 乐观锁插件(OptimisticLockerInterceptor),用于解决多线程或高并发环境下对同一数据的并发修改问题。相比悲观锁(数据库行锁),乐观锁性能更高、开销更小,是现代高并发系统的首选方案。


一、核心概念

1.1 什么是乐观锁?

  • 乐观锁(Optimistic Locking):假设数据一般不会发生冲突,只在提交更新时检查是否被其他事务修改过。
  • 实现方式:通常使用一个版本号字段(version)或时间戳字段,在更新时判断版本是否变化。
  • SQL 示例
    UPDATE user SET name = '新名字', version = version + 1 
    WHERE id = 1 AND version = 10;
    
    如果返回影响行数为 0,说明数据已被其他事务修改,当前更新失败。

1.2 MyBatis-Plus 乐观锁机制

  • 基于 @Version 注解标记版本字段。
  • 配合 OptimisticLockerInnerInterceptor(新版)自动在 UPDATE 语句中添加 version = 当前值 条件,并自动 +1。
  • 无需手动编写 SQL,完全透明集成。

1.3 适用场景

✅ 适合读多写少的场景
✅ 高并发系统(如秒杀、库存更新)
✅ 数据冲突概率较低的业务

❌ 不适合频繁并发修改的极端场景(此时应考虑分布式锁或队列)


二、操作步骤(非常详细)

步骤 1:添加依赖(Maven)

确保已引入 MyBatis-Plus 核心依赖:

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

✅ 推荐使用 3.4.0 及以上版本,使用 InnerInterceptor 新架构。


步骤 2:数据库表添加 version 字段

在需要加锁的表中添加 version 字段:

ALTER TABLE `user` 
ADD COLUMN `version` INT NOT NULL DEFAULT 1 COMMENT '乐观锁版本号';

🔍 字段说明:

  • 类型:INTBIGINT
  • 默认值:1
  • 非空:NOT NULL
  • 注释:便于理解

步骤 3:实体类添加 @Version 注解

import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;

@Data
@TableName("user")
public class User {

    @TableId(type = IdType.AUTO)
    private Long id;

    private String name;

    private String email;

    /**
     * 乐观锁版本号字段
     * 必须使用 @Version 注解
     */
    @Version
    @TableField("version")
    private Integer version;

    /**
     * 其他字段...
     */
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;
}

⚠️ 注意:

  • 必须使用 @Version 注解。
  • 字段类型支持:intIntegerlongLongshortShortTimestamp
  • 初始值建议为 1,避免 0 导致条件判断问题。

步骤 4:配置乐观锁插件(MyBatis-Plus 3.4.0+)

在 Spring Boot 配置类中注册 MybatisPlusInterceptor 并添加 OptimisticLockerInnerInterceptor

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * MyBatis-Plus 配置类
 */
@Configuration
public class MyBatisPlusConfig {

    /**
     * 配置 MyBatis-Plus 插件
     */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();

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

        // 可同时添加其他插件,如分页
        // interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));

        return interceptor;
    }
}

✅ 关键点:

  • 使用 MybatisPlusInterceptor 替代旧版 OptimisticLockerInterceptor
  • OptimisticLockerInnerInterceptor 是 MyBatis-Plus 3.4.0+ 推荐方式。
  • 插件会自动拦截 update 操作,无需手动调用。

步骤 5:使用 Mapper 进行更新操作(自动生效)

@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    /**
     * 更新用户信息(自动启用乐观锁)
     */
    public boolean updateUser(User user) {
        // 假设前端传来了 id 和 name,version 也必须传入
        int result = userMapper.updateById(user);
        return result > 0; // 返回是否更新成功
    }

    /**
     * 模拟并发更新场景
     */
    public void concurrentUpdate() {
        // 用户 A 查询数据
        User userA = userMapper.selectById(1L);
        System.out.println("User A Version: " + userA.getVersion()); // 1

        // 用户 B 查询数据
        User userB = userMapper.selectById(1L);
        System.out.println("User B Version: " + userB.getVersion()); // 1

        // 用户 A 先更新
        userA.setName("A 更新");
        userMapper.updateById(userA); // UPDATE ... WHERE id=1 AND version=1
        System.out.println("A 更新后 Version: " + userA.getVersion()); // 2

        // 用户 B 后更新(此时 version 已变为 2,但 userB.version 仍是 1)
        userB.setName("B 更新");
        int result = userMapper.updateById(userB); // WHERE id=1 AND version=1 → 无匹配
        System.out.println("B 更新结果: " + result); // 0(失败)
        if (result == 0) {
            throw new RuntimeException("数据已被其他用户修改,请刷新重试!");
        }
    }
}

✅ 输出示例:

User A Version: 1
User B Version: 1
A 更新后 Version: 2
B 更新结果: 0

三、常见错误与解决方案

错误 原因 解决方案
version 字段未参与 WHERE 条件 未正确配置插件 检查 MybatisPlusInterceptor 是否添加 OptimisticLockerInnerInterceptor
更新失败但无提示 未处理返回值 检查 updateById 返回值是否 > 0
@Version 字段为 null 实体未查出或未传入 查询后必须将 version 字段传回前端,更新时带回
插件不生效 使用了旧版配置 确保使用 InnerInterceptor 架构(3.4.0+)
批量更新不支持 乐观锁不支持 updateBatchById 需逐条更新或自定义 SQL

四、注意事项

  1. @Version 字段必须非空:数据库和 Java 对象都应保证有值。
  2. 更新时必须传入 version:前端需将 version 作为参数传回。
  3. 不支持 wrapper 更新中的 set 操作:如 update().set("version", 999) 会被忽略。
  4. null 值不参与乐观锁:如果 versionnull,插件不会添加 WHERE 条件。
  5. 避免在 insert 时设置 version:建议由数据库默认值或自动填充处理。

五、使用技巧

5.1 结合自动填充设置初始版本

@Component
public class MyMetaObjectHandler implements MetaObjectHandler {

    @Override
    public void insertFill(MetaObject metaObject) {
        this.strictInsertFill(metaObject, "version", Integer.class, 1);
    }

    @Override
    public void updateFill(MetaObject metaObject) {
        // updateTime 等
    }
}

5.2 自定义异常处理

@ExceptionHandler(DataIntegrityViolationException.class)
public ResponseEntity<String> handleOptimisticLockFailure(DataIntegrityViolationException ex) {
    return ResponseEntity.badRequest().body("数据已被修改,请刷新页面后重试。");
}

⚠️ 注意:MyBatis-Plus 不会抛出特定异常,需通过返回值判断。

5.3 支持 Timestamp 类型版本

@Version
private LocalDateTime versionTime;

适用于以时间戳作为版本依据的场景。

5.4 手动重试机制(高级)

public boolean updateWithRetry(User user, int maxRetries) {
    for (int i = 0; i < maxRetries; i++) {
        User dbUser = userMapper.selectById(user.getId());
        user.setVersion(dbUser.getVersion()); // 使用最新版本
        int result = userMapper.updateById(user);
        if (result > 0) {
            return true;
        }
        // 可加入延迟
        try { Thread.sleep(100); } catch (InterruptedException e) {}
    }
    return false;
}

六、最佳实践与性能优化

6.1 最佳实践

统一基类封装 version 字段

@Data
public class BaseEntity {
    @TableId(type = IdType.AUTO)
    private Long id;

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

// 实体继承
public class User extends BaseEntity {
    private String name;
}

前端展示版本信息(可选)

  • 显示“最后修改时间”或“版本号”,提升用户体验。

日志记录冲突事件

if (result == 0) {
    log.warn("乐观锁冲突:用户 {} 尝试更新已被修改的数据,ID={}", currentUser, user.getId());
}

结合业务重试或合并

  • 对于非关键冲突,可提示用户“数据已变更”,让用户决定是否覆盖。

6.2 性能优化

  • 无额外查询开销:乐观锁只在 UPDATE 时增加一个 WHERE 条件,性能几乎无损。
  • 减少数据库锁等待:相比悲观锁,不会阻塞其他读操作。
  • 适合分布式环境:无需跨节点协调锁。
  • 批量操作优化:避免对同一记录频繁更新,可合并操作或使用队列。

七、总结

MyBatis-Plus 乐观锁插件是高并发系统的利器:

功能 实现方式
启用乐观锁 @Version 注解 + OptimisticLockerInnerInterceptor
版本控制 数据库 version 字段 + 自动填充初始值
冲突处理 检查 update 返回值,提示用户重试

🔧 推荐流程

  1. 数据库添加 version 字段
  2. 实体类添加 @Version 注解
  3. 配置 MybatisPlusInterceptor 添加乐观锁插件
  4. 更新时传入 version,检查返回值
  5. 处理失败情况(提示、重试、日志)