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

告别并发更新噩梦:MyBatis-Plus @Version 乐观锁实战指南

在高并发的系统中,多个用户或进程同时修改同一条数据是常有的事。想象一下这个场景:

  • 用户 A 读取了商品 X 的库存,当前是 10。
  • 几乎同时,用户 B 也读取了商品 X 的库存,也是 10。
  • 用户 A 完成操作,将库存更新为 9 (UPDATE ... SET stock = 9 WHERE id = X​)。
  • 紧接着,用户 B 也完成操作,将库存更新为 8 (UPDATE ... SET stock = 8 WHERE id = X​)。

结果是什么?库存最终变成了 8。但实际上,两次操作应该使库存变为 10 - 1 - 1 = 8 才对吗?不,这里用户 A 的更新被用户 B 的更新覆盖了,这就是经典的 “丢失更新”(Lost Update) 问题。

如何解决这个问题呢?常见的并发控制策略有两种:

  1. 悲观锁 (Pessimistic Locking): 假设冲突总是会发生。在读取数据时就将其锁定(例如数据库的 SELECT ... FOR UPDATE​),阻止其他事务修改,直到当前事务完成。缺点是性能开销大,可能导致长时间等待甚至死锁。
  2. 乐观锁 (Optimistic Locking): 假设冲突很少发生。读取数据时不加锁。在更新数据时,检查一下自上次读取后,数据是否被其他事务修改过。如果没被修改,就执行更新;如果已被修改,就放弃本次更新(或进行重试等其他处理)。

MyBatis-Plus (MP) 提供了一个非常优雅的方式来实现乐观锁,那就是 @Version​ 注解。

什么是 @Version​?

​@Version​ 是 MP 提供的一个注解,用于标记实体类中代表版本号的字段。启用后,MP 会在执行更新操作时,自动利用这个版本号字段来实现乐观锁机制。

如何使用 @Version​?

使用 @Version​ 同样非常简单,只需三步:

第一步:数据库表设计

在需要进行乐观锁控制的表中,添加一个用于存储版本号的字段。通常使用 INT​ 或 BIGINT​ 类型,并给一个初始值(如 1 或 0)。

ALTER TABLE `your_table_name`
ADD COLUMN `version` INT NOT NULL DEFAULT 1 COMMENT '版本号(乐观锁)';
-- 或者使用 BIGINT
-- ALTER TABLE `your_table_name` ADD COLUMN `version` BIGINT NOT NULL DEFAULT 1 COMMENT '版本号(乐观锁)';-- TIMESTAMP 类型也可以,但数值类型更常见且直观

注意: 初始值很重要,通常设为 1​ 或 0​。

第二步:实体类配置

在对应的 Java 实体类中,添加该字段,并使用 @Version​ 注解标记它:

import com.baomidou.mybatisplus.annotation.Version;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;@Data
@TableName("your_table_name")
public class YourEntity {private Long id;private String name;private Integer stock; // 假设这是需要并发控制的字段// ... 其他字段@Version // 标记为版本号字段 (乐观锁)private Integer version; // 字段类型与数据库对应
}

第三步:注册乐观锁拦截器

乐观锁的功能是由 MP 的 OptimisticLockerInnerInterceptor​ 拦截器实现的。你必须在 MP 的配置中注册这个拦截器,它才能生效。

import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; // 如果也需要分页
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;@Configuration
public class MybatisPlusConfig {@Beanpublic MybatisPlusInterceptor mybatisPlusInterceptor() {MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();// 添加乐观锁插件 !!! 这是必需的 !!!interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());// 如果你还需要分页插件或其他内部插件,也在这里添加// interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));return interceptor;}
}

关键: 如果没有注册 OptimisticLockerInnerInterceptor​,@Version​ 注解将完全不起作用!

@Version​ 如何工作(幕后机制)?

配置完成后,当你使用 MP 提供的标准更新方法时,乐观锁拦截器会自动修改 SQL:

  1. 当你执行 selectById​ 或类似查询获取数据时:

    YourEntity entity = yourMapper.selectById(1L);
    // 假设查出的 entity 中 version = 1
    

    SQL 基本不变:

    SELECT id, name, stock, version /*,...*/ FROM your_table_name WHERE id = 1;
    
  2. 当你修改实体并执行 updateById​ 或 update​ 时:

    entity.setStock(entity.getStock() - 1); // stock 变为 9
    int result = yourMapper.updateById(entity); // 尝试更新
    

    MP 的乐观锁拦截器会介入,自动修改 UPDATE​ 语句:

    UPDATE your_table_name
    SETstock = 9,version = 2 -- 版本号会自动加 1
    WHEREid = 1AND version = 1 -- 条件中会带上之前查到的版本号
    
    • 核心: UPDATE​ 语句的 WHERE​ 条件中包含了 version = 1​。这意味着,只有当数据库中当前的 version​ 值仍然是 1​ 时(表示从你上次读取到现在,没有其他事务修改过它),这条 UPDATE​ 才会成功执行,并且将 version​ 更新为 2​。
    • 如果期间有其他事务已经修改了数据并将 version​ 更新为了 2​,那么你的 UPDATE ... WHERE ... version = 1​ 语句将找不到匹配的行,更新操作影响的行数将是 0​。

如何处理更新失败?

当 updateById(entity)​ 或 update(entity, wrapper)​ 方法因为版本冲突而更新失败时,它们的返回值会是 0​(表示影响行数为 0)。

你的应用程序必须检查这个返回值,并根据业务需求进行处理,常见的策略有:

  1. 重试 (Retry): 重新读取最新的数据(包括最新的版本号),再次尝试修改和更新。通常需要设置重试次数限制,避免无限循环。

    int maxRetries = 3;
    int currentRetry = 0;
    boolean success = false;
    while (currentRetry < maxRetries && !success) {YourEntity currentEntity = yourMapper.selectById(entity.getId());if (currentEntity == null) { /* 处理记录不存在的情况 */ break; }// 在新查出的数据上进行业务操作currentEntity.setStock(currentEntity.getStock() - 1);int result = yourMapper.updateById(currentEntity); // 使用最新的 version 尝试更新if (result > 0) {success = true; // 更新成功} else {currentRetry++;// 可选:增加短暂等待,避免活锁Thread.sleep(50);}
    }
    if (!success) {// 达到最大重试次数,抛出异常或通知用户throw new OptimisticLockingFailureException("Failed to update entity after multiple retries.");
    }
    
  2. 通知用户: 提示用户数据已被修改,请刷新后重试。

    int result = yourMapper.updateById(entity);
    if (result == 0) {// 抛出特定异常,由全局异常处理器转换为友好的前端提示throw new DataConcurrentModificationException("Data has been modified by others. Please refresh and try again.");
    }
    
  3. 放弃或合并: 根据业务复杂性,可能选择放弃本次修改,或者尝试将用户的修改与数据库的最新状态进行合并(这通常比较复杂)。

重点: 必须处理更新返回值为 0 的情况,否则你的业务逻辑可能会在并发下出错。

@Version​ 的优势

  • 简单易用: 只需添加一个字段、一个注解和一个拦截器即可启用。
  • 性能较好: 相比悲观锁,它在读取时不产生数据库锁,只有在更新时才增加一个 version​ 条件,并发性能通常更好。
  • 保证数据一致性: 有效防止了“丢失更新”问题。

限制与注意事项

  • 仅支持 MP 内置方法: 乐观锁功能只对 updateById​ 和 update(entity, wrapper)​ 方法生效。对于自定义 SQL (在 XML 中或使用 @Update​ 注解编写的 SQL),乐观锁不会自动生效,你需要手动在 SQL 中实现版本检查和递增逻辑。
  • 拦截器必须注册: 重复强调,没有注册 OptimisticLockerInnerInterceptor​,一切都是空谈。
  • 失败处理是关键: 必须在代码中显式处理更新失败(返回值为 0)的情况。
  • 仅限更新: @Version​ 主要解决的是并发更新冲突,它不解决并发读导致的数据显示陈旧问题。

总结

MyBatis-Plus 的 @Version​ 注解为解决并发更新问题提供了一个强大而简洁的乐观锁实现方案。通过简单的配置,它可以极大地提高系统的并发处理能力和数据一致性。然而,务必记住正确配置拦截器,并在业务代码中妥善处理可能发生的更新失败情况,这样才能真正发挥乐观锁的威力,让你的应用在并发环境下稳如磐石。


http://www.xdnf.cn/news/148339.html

相关文章:

  • 深入详解人工智能数学基础——概率论中的马尔可夫链蒙特卡洛(MCMC)采样
  • CAPL编程_03
  • vue-lottie的使用和配置
  • 正大模型视角下的市场结构判断逻辑
  • 使用 SSE + WebFlux 推送日志信息到前端
  • 矫平机深度解析:操作实务、行业标准与智能化升级
  • 一款好的私有云产品推荐——优刻得私有云(UCloudStack Pro)产品白皮书
  • 示波器测试差分信号
  • cpu性能统计
  • 网络犯罪全球化,数字时代的跨国诈骗危机
  • Linux——线程(1)线程概念与控制
  • 12.thinkphp验证
  • 粒子群优化算法(Particle Swarm Optimization, PSO)的详细解读
  • PR第二课--混剪
  • 嵌入式通信技术实践与教学创新:从蓝牙协议到虚实融合的实验革命
  • 【Nacos-安全与限流机制健全06 】
  • 第19章:Multi-Agent多智能体系统介绍
  • C/C++时间函数详解及使用场景
  • 找出字符串中第一个匹配项的下标
  • 关于hbaseRegion和hbaseRowKey的一些处理
  • 在 Ubuntu 22.04|20.04|18.04 上安装 PostgreSQL 13
  • 4/24杂想
  • 慧星云荣登杭州AI卧龙图
  • windows安装jax和jaxlib的教程(cuda)成功安装
  • C++进阶----多态
  • 这些项目可以在以后年度结转扣除!
  • 从 0 开始认识 WebSocket:前端实时通信的利器!
  • 腾讯云系统盘占满
  • Node.js 应用场景
  • AIGC实战之如何构建出更好的大模型RAG系统