🎯 @Version 注解是什么?

@Version 是 MyBatis-Plus 提供的乐观锁注解。它用于解决并发场景下的数据更新冲突问题。

乐观锁 vs 悲观锁

  • 悲观锁:认为每次操作都会冲突,直接加锁(如 SELECT FOR UPDATE

  • 乐观锁:认为冲突很少发生,通过版本号机制解决冲突

🔧 工作原理

  1. 读取数据时获取当前版本号

  2. 更新数据时版本号 +1

  3. WHERE条件中包含旧版本号检查

  4. 如果版本号不匹配,更新失败

🛠️ 完整配置示例

1. 添加依赖

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

2. 配置乐观锁插件

@Configuration
public class MybatisPlusConfig {
    
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // 关键:添加乐观锁插件
        interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
        return interceptor;
    }
}

3. 实体类中使用 @Version

@Data
@TableName("user")
public class User {
    
    @TableId(type = IdType.ASSIGN_ID)
    private Long id;
    
    private String name;
    
    private Integer age;
    
    @Version
    @TableField("revision")
    private Integer revision;  // 版本号字段
    
    // 其他字段...
}

📋 示例场景

示例1:基本使用

@Service
public class UserService {
    
    @Autowired
    private UserMapper userMapper;
    
    public void updateUser(Long userId, String newName) {
        // 1. 先查询获取当前数据和版本号
        User user = userMapper.selectById(userId);
        System.out.println("当前版本号: " + user.getRevision()); // 比如: 1
        
        // 2. 修改数据
        user.setName(newName);
        
        // 3. 更新操作(MP会自动处理版本号)
        int result = userMapper.updateById(user);
        
        if (result > 0) {
            System.out.println("更新成功,新版本号: " + user.getRevision()); // 现在: 2
        } else {
            throw new RuntimeException("更新失败,数据可能已被其他线程修改");
        }
    }
}

示例2:并发冲突模拟

@Test
public void testConcurrentUpdate() {
    // 线程1
    new Thread(() -> {
        User user1 = userMapper.selectById(1L);
        user1.setName("Thread1");
        userMapper.updateById(user1); // 成功,version+1
    }).start();
    
    // 线程2(稍晚一点执行)
    new Thread(() -> {
        try { Thread.sleep(100); } catch (InterruptedException e) {}
        User user2 = userMapper.selectById(1L);
        user2.setName("Thread2");
        int result = userMapper.updateById(user2); // 失败,返回0
        System.out.println("线程2更新结果: " + result); // 输出: 0
    }).start();
}

示例3:数据库表结构

CREATE TABLE user (
    id BIGINT PRIMARY KEY COMMENT '主键',
    name VARCHAR(50) COMMENT '姓名',
    age INT COMMENT '年龄',
    revision INT DEFAULT 0 COMMENT '版本号,乐观锁字段',
    created_time DATETIME COMMENT '创建时间',
    updated_time DATETIME COMMENT '更新时间'
);

🔍 生成的SQL语句

更新时MP会生成这样的SQL

UPDATE user 
SET name = '新名字', revision = revision + 1 
WHERE id = 1 AND revision = 1

💡 高级用法

1. 使用Wrapper时的版本控制

public void updateWithWrapper(Long userId, String newName) {
    User user = userMapper.selectById(userId);
    Integer oldVersion = user.getRevision();
    
    UpdateWrapper<User> wrapper = new UpdateWrapper<>();
    wrapper.eq("id", userId)
           .eq("revision", oldVersion)  // 重要:包含版本条件
           .set("name", newName)
           .setSql("revision = revision + 1");  // 手动版本+1
    
    int result = userMapper.update(null, wrapper);
}

2. 批量更新处理

public void batchUpdate(List<Long> userIds, String newName) {
    List<User> users = userMapper.selectBatchIds(userIds);
    
    for (User user : users) {
        user.setName(newName);
        try {
            userMapper.updateById(user);
        } catch (Exception e) {
            log.warn("用户 {} 更新失败: {}", user.getId(), e.getMessage());
            // 可以重试或记录失败
        }
    }
}

3. 自定义异常处理

@Service
@Slf4j
public class UserService {
    
    @Retryable(value = OptimisticLockingFailureException.class, maxAttempts = 3)
    public void updateWithRetry(Long userId, String newName) {
        User user = userMapper.selectById(userId);
        user.setName(newName);
        
        int result = userMapper.updateById(user);
        if (result == 0) {
            throw new OptimisticLockingFailureException("数据已被修改,请重试");
        }
    }
    
    // 重试失败后的处理
    @Recover
    public void recover(OptimisticLockingFailureException e, Long userId, String newName) {
        log.error("用户 {} 更新失败,经过3次重试仍失败: {}", userId, e.getMessage());
        // 发送通知或记录日志
    }
}

⚠️ 注意事项

1. 字段类型必须为数值类型

// 正确 ✅
@Version
private Integer revision;

@Version
private Long version;

// 错误 ❌
@Version
private String version;  // 不支持字符串类型

2. 初始值设置

// 插入数据时,版本号通常从1开始
User user = new User();
user.setName("张三");
// revision 会自动设为1(如果数据库默认值为0)
userMapper.insert(user);

3. 数据库默认值

建议在数据库中设置默认值:

ALTER TABLE user MODIFY revision INT DEFAULT 1;

4. 不支持的情况

// 这些操作不会触发乐观锁:
userMapper.update(null, updateWrapper);  // 如果wrapper中没有包含版本条件
userMapper.deleteById(id);              // 删除操作不触发乐观锁
自定义SQL更新                           // 需要手动处理版本号

🎯 实际应用场景

场景1:库存扣减

public boolean reduceStock(Long productId, Integer quantity) {
    Product product = productMapper.selectById(productId);
    if (product.getStock() < quantity) {
        throw new RuntimeException("库存不足");
    }
    
    product.setStock(product.getStock() - quantity);
    int result = productMapper.updateById(product);
    
    return result > 0;
}

场景2:账户余额更新

public boolean transfer(Long fromAccountId, Long toAccountId, BigDecimal amount) {
    Account fromAccount = accountMapper.selectById(fromAccountId);
    if (fromAccount.getBalance().compareTo(amount) < 0) {
        throw new RuntimeException("余额不足");
    }
    
    fromAccount.setBalance(fromAccount.getBalance().subtract(amount));
    int result = accountMapper.updateById(fromAccount);
    
    if (result > 0) {
        // 更新对方账户
        Account toAccount = accountMapper.selectById(toAccountId);
        toAccount.setBalance(toAccount.getBalance().add(amount));
        accountMapper.updateById(toAccount);
    }
    
    return result > 0;
}

📊 总结

@Version 的核心价值

  1. 解决并发冲突:防止数据覆盖

  2. 无锁性能高:不需要数据库锁,性能更好

  3. 使用简单:只需一个注解+插件配置

  4. 自动管理:MP自动处理版本号增减

使用口诀

  • 配置插件不能忘

  • 字段类型要数值

  • 使用updateById

  • 失败处理要跟上

这样就能很好地利用乐观锁来解决并发更新问题了!

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐