一、核心概念

什么是 @Version

@Version 是 MyBatis-Plus 提供的乐观锁注解,用于标识实体类中的版本字段。在执行 UPDATE 操作时,MyBatis-Plus 会自动将版本号 +1,并在 SQL 中添加 WHERE version = 原值 条件,确保数据未被其他事务修改过。

乐观锁 vs 悲观锁

类型 原理 适用场景
乐观锁 假设不会发生冲突,更新时检查版本号 读多写少、并发不高
悲观锁 假设会发生冲突,直接加锁(如 SELECT ... FOR UPDATE 写多、高并发、强一致性

@Version 工作原理

-- 假设当前 version = 1
UPDATE user SET name = 'newName', version = 2 WHERE id = 1 AND version = 1;
  • 如果返回 affectedRows == 1 → 更新成功
  • 如果返回 affectedRows == 0 → 版本不匹配,更新失败(数据已被修改)

二、操作步骤(超详细)

第一步:数据库表添加版本字段

ALTER TABLE `user` ADD COLUMN `version` INT DEFAULT 1 COMMENT '版本号';
-- 或建表时包含
-- `version` INT DEFAULT 1 COMMENT '版本号'

✅ 建议:初始值设为 1,便于识别是否被更新过。


第二步:实体类添加 @Version 注解

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

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

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

    private String name;

    private Integer age;

    // 版本字段
    @Version
    @TableField("version")
    private Integer version;

    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;
}

⚠️ 注意:

  • 字段类型必须是 IntegerLong
  • 必须配合 @TableField 使用(指定数据库字段名)
  • 通常与 MetaObjectHandler 配合自动填充 updateTime

第三步:配置 MyBatis-Plus 乐观锁插件

在 Spring Boot 配置类中注册 MybatisPlusInterceptor

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;

@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;
    }
}

✅ 从 MP 3.4.0 起,推荐使用 MybatisPlusInterceptor,替代旧的 OptimisticLockerInterceptor


第四步:编写业务代码测试乐观锁

@Service
@RequiredArgsConstructor
public class UserService {

    private final UserMapper userMapper;

    /**
     * 更新用户信息(演示乐观锁)
     */
    public boolean updateUser(UserDO user) {
        // 假设前端传来的 user 对象包含当前 version
        int result = userMapper.updateById(user);
        return result > 0; // 返回是否更新成功
    }

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

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

        // 用户 A 先更新
        userA.setName("A修改了");
        userMapper.updateById(userA); // version=1 → version=2
        System.out.println("A更新后: " + userA.getVersion()); // version=2

        // 用户 B 后更新(使用的是旧版本数据)
        userB.setName("B修改了");
        int result = userMapper.updateById(userB); // WHERE version=1 → 失败
        System.out.println("B更新结果: " + result); // result=0
        if (result == 0) {
            throw new RuntimeException("数据已被其他用户修改,请刷新后重试!");
        }
    }
}

第五步:处理更新失败(推荐做法)

public boolean updateUserWithRetry(UserDO user, int maxRetries) {
    for (int i = 0; i < maxRetries; i++) {
        try {
            int result = userMapper.updateById(user);
            if (result > 0) {
                return true; // 更新成功
            }
            // 更新失败,重新查询最新数据
            UserDO latest = userMapper.selectById(user.getId());
            if (latest == null) {
                throw new RuntimeException("数据不存在");
            }
            // 更新业务字段,保留最新版本号
            user.setVersion(latest.getVersion());
            // 可选:合并业务变更
            user.setName(latest.getName()); // 示例:以最新名为准
        } catch (Exception e) {
            log.error("更新失败,重试 {}/{}", i + 1, maxRetries, e);
            if (i == maxRetries - 1) throw e;
            try {
                Thread.sleep(100 * (i + 1)); // 指数退避
            } catch (InterruptedException ie) {
                Thread.currentThread().interrupt();
            }
        }
    }
    return false;
}

三、常见错误与解决方案

错误现象 原因 解决方案
@Version 无效 未配置 OptimisticLockerInnerInterceptor 添加插件配置
更新后 version 未 +1 字段未加 @Version 或类型错误 确保注解正确且类型为 Integer/Long
UPDATE 语句无 version 条件 插件未生效 检查 MybatisPlusInterceptor 是否注册
version 字段为 null 数据库默认值未设置 设置默认值 DEFAULT 1
批量更新不支持 updateBatchById 不支持乐观锁 需手动逐条更新或自定义 SQL
SELECT FOR UPDATE 冲突 混用悲观锁和乐观锁 统一使用一种锁机制

四、注意事项

  1. 必须配置插件OptimisticLockerInnerInterceptor@Version 生效的前提。
  2. 字段类型:只能是 IntegerLong,不能是 int/long(避免 null 问题)。
  3. 初始值:建议设为 1,避免 0 导致的意外行为。
  4. 并发更新updateBatchById 方法不支持乐观锁,需逐条处理。
  5. null 值处理:如果 version 为 null,更新会失败,确保查询时能取到值。
  6. 逻辑删除:乐观锁与逻辑删除可同时使用。
  7. 分布式场景:乐观锁适用于单数据库,跨服务需结合分布式锁或事件溯源。

五、使用技巧

1. 自定义版本生成策略

// 默认是 +1,可自定义
@Version
@TableField("version")
private Integer version;

// 在 update 前手动设置 version = oldVersion + 1
// 但通常不建议,让 MP 自动处理

2. 结合 MetaObjectHandler 初始化版本

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

3. 版本号用于缓存更新

// 更新成功后,通过 version 变化通知缓存失效
redisTemplate.delete("user:" + user.getId());

4. 前端提示“请刷新页面”

当更新失败时,返回特定错误码,前端提示用户刷新数据:

{
  "code": 409,
  "msg": "数据版本冲突,请刷新后重试"
}

六、最佳实践与性能优化

✅ 最佳实践

实践 说明
✅ 所有可变表都加 version 字段 统一数据安全标准
✅ 配合 updateTime 使用 记录最后修改时间
✅ 前端展示版本信息 调试时显示 version
✅ 重试机制 重要操作添加有限重试
✅ 日志记录 记录乐观锁失败事件,便于分析
✅ 不用于高并发强一致场景 如库存扣减,建议用悲观锁或分布式锁

⚡ 性能优化建议

  • 避免频繁更新:减少不必要的 UPDATE 操作
  • 索引优化version 字段通常已在主键索引中,无需额外索引
  • 批量处理:非并发场景可批量更新(不启用乐观锁)
  • 缓存结合:更新后及时清理缓存,避免脏读

七、高级用法:自定义乐观锁逻辑

场景:使用时间戳作为版本号

// 不推荐,但可行
@Version
@TableField("update_time")
private LocalDateTime updateTime;

// 需要手动在 update 前 setUpdateTime(oldTime)
// 且精度问题可能导致冲突

⚠️ 建议:仍使用 INT 类型的 version 字段,更简单可靠。


总结

@Version 注解是 MyBatis-Plus 实现乐观锁的核心,通过简单的注解和插件配置,即可防止并发修改导致的数据覆盖。

🚀 四步启用

  1. 数据库添加 version 字段(INT,默认 1)
  2. 实体类字段加 @Version
  3. 配置 OptimisticLockerInnerInterceptor
  4. 业务代码捕获更新失败并处理(提示刷新)

掌握 @Version,让你的系统在并发环境下更加安全、稳定。适用于用户信息、配置管理、订单状态等读多写少的场景。