十分钟了解@Version注解
🎯 @Version 注解是什么?
@Version
是 MyBatis-Plus 提供的乐观锁注解。它用于解决并发场景下的数据更新冲突问题。
乐观锁 vs 悲观锁
悲观锁:认为每次操作都会冲突,直接加锁(如
SELECT FOR UPDATE
)乐观锁:认为冲突很少发生,通过版本号机制解决冲突
🔧 工作原理
读取数据时获取当前版本号
更新数据时版本号 +1
WHERE条件中包含旧版本号检查
如果版本号不匹配,更新失败
🛠️ 完整配置示例
1. 添加依赖
<dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.5.0</version>
</dependency>
2. 配置乐观锁插件
@Configuration
public class MybatisPlusConfig {@Beanpublic 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 {@Autowiredprivate 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() {// 线程1new 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); // 失败,返回0System.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"); // 手动版本+1int 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("数据已被修改,请重试");}}// 重试失败后的处理@Recoverpublic 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 的核心价值:
解决并发冲突:防止数据覆盖
无锁性能高:不需要数据库锁,性能更好
使用简单:只需一个注解+插件配置
自动管理:MP自动处理版本号增减
使用口诀:
配置插件不能忘
字段类型要数值
使用updateById
失败处理要跟上
这样就能很好地利用乐观锁来解决并发更新问题了!