当前位置: 首页 > news >正文

十分钟了解@Version注解

🎯 @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 {@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 的核心价值

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

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

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

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

使用口诀

  • 配置插件不能忘

  • 字段类型要数值

  • 使用updateById

  • 失败处理要跟上

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

http://www.dtcms.com/a/391310.html

相关文章:

  • vue3+ts+uniapp H5微信小程序app有截止日期的日期date-pcicker组件
  • 设计模式-观察者模式详解
  • centos7--安装海量数据库Vastbase M100
  • Apache Commons DBCP连接池生产环境配置推荐
  • 【11/20】实时数据基础:WebSocket 在 Express 和 Vue 中的整合,实现简单聊天功能
  • 五传输层-TCP UDP慢启动-真题
  • ARM基础知识
  • 从零开始的指针(5)
  • TDMQ CKafka 版客户端实战指南系列之二:消费消息最佳实践
  • Comcast 没有对比就没有伤害
  • AI悬浮窗 1.0 | 快捷提取文字,总结信息,支持提取文字、理解屏幕上的图案、总结分析信息
  • MySQL、PostgreSQL、MongoDB和Redis全面对比
  • 隐私保护与数据安全合规(七)
  • 登录 双层拦截器+redis
  • TM56M152A (SOP16) HITENX海速芯 8位微控制器MCU 芯片深度解析
  • 理解元学习器 - 如何使用机器学习估计异质处理效应(四)
  • [数据结构] Map和Set
  • [Go类库分享]Go template模版库
  • 辅助搜题系统-基于模糊搜索,上传word题库后,可搜索答案
  • 【完整源码+数据集+部署教程】遥感农田森林岩石图像分割系统: yolov8-seg-C2f-DCNV2
  • RTX 4090助力深度学习:从PyTorch到生产环境的完整实践指南
  • AWS中国云中的调用链监控(EC2版)
  • CI/CD到底是什么?
  • 3dmax三维动画渲染很慢怎么办?
  • ASIS CTF 2025 SatoNote
  • BasicForm的使用
  • CSP初赛——STL中的函数整理
  • 小杰机器学习高级(two)——极大似然估计、交叉熵损失函数
  • 关于px4 1.15.0电机控制有效矩阵的更新
  • 【设计模式】职责链模式