十分钟了解@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 {
@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 的核心价值:
-
解决并发冲突:防止数据覆盖
-
无锁性能高:不需要数据库锁,性能更好
-
使用简单:只需一个注解+插件配置
-
自动管理:MP自动处理版本号增减
使用口诀:
-
配置插件不能忘
-
字段类型要数值
-
使用updateById
-
失败处理要跟上
这样就能很好地利用乐观锁来解决并发更新问题了!

更多推荐



所有评论(0)