MyBatis-Plus的乐观锁与悲观锁
- 锁机制的必要性
- 模拟并发更新冲突
- 悲观锁
- 模拟实现悲观锁
- 乐观锁
- 模拟实现乐观锁
MyBatis-Plus 的乐观锁是基于版本号机制实现的非阻塞式并发控制方案,对应用层乐观锁逻辑提供轻量化封装;悲观锁则整合数据库原生行锁 / 表锁机制,在操作数据时预先锁定资源以阻塞冲突操作,二者均用于保障并发更新场景下的数据一致性。
下面详细介绍锁机制的必要性以及 MyBatis-Plus 的乐观锁与悲观锁内容。
锁机制的必要性
首先我们假设一个业务场景,探究一下锁机制的必要性
某商品成本价 80 元,当前售价 100 元,老板先后下达两条调价指令:
指令 1(给小李):将商品价格上调 50 元,小李因事务耽搁,延迟 1 小时执行;
指令 2(给小王):1 小时后老板调整策略,要求将商品价格下调 30 元,小王立即执行。
无锁下的执行过程:
小李和小王均从数据库读取到初始价格 100 元,
小李执行操作:100 + 50 = 150 元,写入数据库;
小王执行操作:100 - 30 = 70 元,写入数据库(覆盖小李的更新结果);
最终商品价格为 70 元,低于成本价 80 元。若该商品短时间售出 1000 件,将造成约 10000 元的直接经济损失。
我们将上述场景进行复原模拟
模拟并发更新冲突
首先是在数据库中新增商品表
CREATETABLEproduct(idBIGINT(20)NOTNULLCOMMENT'主键ID',NAMEVARCHAR(30)NULLDEFAULTNULLCOMMENT'商品名称 ',priceINT(11)DEFAULT0COMMENT'价格 ',PRIMARYKEY(id));添加数据
INSERTINTOproduct(id,NAME,price)VALUES(1,'牛奶',100);添加实体类
@Data@NoArgsConstructor@AllArgsConstructorpublicclassProduct{privateLongid;privateStringname;privateIntegerprice;privateIntegerversion;}添加 Mapper 接口
publicinterfaceProductMapperextendsBaseMapper<Product>{}并发更新测试
@SpringBootTestpublicclassMybatisConfigTest{@AutowiredprivateProductMapperproductMapper;@TestpublicvoidtestUpdate(){Productp1=productMapper.selectById(1L);System.out.println("小李取出的价格:"+p1.getPrice());Productp2=productMapper.selectById(1L);System.out.println("小王取出的价格:"+p2.getPrice());p1.setPrice(p1.getPrice()+50);intresult1=productMapper.updateById(p1);System.out.println("小李修改结果:"+p1.getPrice());p2.setPrice(p2.getPrice()-30);intresult2=productMapper.updateById(p2);System.out.println("小王修改结果:"+p2.getPrice());Productp3=productMapper.selectById(1L);System.out.println("最后的结果:"+p3.getPrice());}}执行结果:
在多线程 / 多进程并发操作数据库的场景中,若缺乏锁机制约束,“读取 - 更新” 操作缺乏原子性约束,后执行的更新覆盖先执行的结果,极易引发更新丢失问题,破坏数据一致性。数据库锁机制正是解决此类问题的核心手段。
悲观锁
悲观锁是基于悲观并发假设的数据库并发控制策略,其核心实现依赖 InnoDB 存储引擎原生的SELECT ... FOR UPDATE语法。
MyBatis-Plus 完全兼容数据库层面的悲观锁实现逻辑,只需在自定义查询 SQL 中为目标数据查询语句拼接 FOR UPDATE 子句,即可借助数据库的锁机制实现悲观锁效果。
SELECT ... FOR UPDATE语句的执行逻辑可拆解为 “数据查询 + 排他行锁施加”:执行该语句时,数据库首先检索出符合查询条件的记录,同时为这些记录施加排他行锁(X 锁),该锁的生命周期与当前数据库事务强绑定,仅在事务提交或回滚后释放。在此期间,其他事务若尝试对锁定记录执行加排他锁的操作,会被阻塞直至锁释放。
需要注意的是 SELECT … FOR UPDATE 锁机制的生效前提仅在显式开启的事务内有效,若未通过 BEGIN/START TRANSACTION 或框架注解(如 Spring @Transactional)显式启动事务,MySQL 会将单条 SELECT … FOR UPDATE 语句视为独立事务,执行后立即自动提交,导致锁被瞬间释放,无法起到悲观锁的并发控制作用。
模拟实现悲观锁
创建 Mapper 接口
自定义 SQL 悲观锁查询方法,显式添加 FOR UPDATE
publicinterfaceProductMapperextendsBaseMapper<Product>{/** * 悲观锁查询 */@Select("SELECT * FROM product WHERE id = #{id} FOR UPDATE")ProductselectByIdForUpdate(@Param("id")Longid);}Service 层接口
publicinterfaceProductServiceextendsIService<Product>{booleanliXiaoAddPrice(Longid);booleanxiaoWangReducePrice(Longid);}Service 层实现类,实现操作,加悲观锁和事务
@ServicepublicclassProductServiceImplextendsServiceImpl<ProductMapper,Product>implementsProductService{// 小李操作@Transactional(rollbackFor=Exception.class)publicbooleanliXiaoAddPrice(Longid){// 悲观锁查询(锁定行,直到事务结束)Productproduct=baseMapper.selectByIdForUpdate(id);if(product==null){returnfalse;}try{System.out.println("小李:其他事务耽搁,10秒后再调价...");Thread.sleep(10000);}catch(InterruptedExceptione){Thread.currentThread().interrupt();thrownewRuntimeException("小李操作中断",e);}product.setPrice(product.getPrice()+50);booleanresult=baseMapper.updateById(product)>0;System.out.println("小李:调价完成!当前价格="+product.getPrice());returnresult;}// 小王操作@Transactional(rollbackFor=Exception.class)publicbooleanxiaoWangReducePrice(Longid){// 悲观锁查询Productproduct=baseMapper.selectByIdForUpdate(id);if(product==null){returnfalse;}product.setPrice(product.getPrice()-30);booleanresult=baseMapper.updateById(product)>0;System.out.println("小王:调价完成!当前价格="+product.getPrice());returnresult;}加悲观锁进行模拟测试
@AutowiredprivateProductServiceproductService;/** * 加悲观锁,模拟小李和小王同时操作商品价格 */@TestpublicvoidtestLiXiaoAndXiaoWang()throwsInterruptedException{LongproductId=1L;System.out.println("初始价格:"+productService.getById(productId).getPrice());// 小李线程ThreadliXiaoThread=newThread(()->productService.liXiaoAddPrice(productId));// 小王线程ThreadxiaoWangThread=newThread(()->productService.xiaoWangReducePrice(productId));// 同时启动两个线程(模拟同时操作)System.out.println("===== 小李和小王同时开始操作 =====");liXiaoThread.start();xiaoWangThread.start();// 等待两个线程执行完成liXiaoThread.join();xiaoWangThread.join();// 查询最终价格ProductfinalProduct=productService.getById(productId);System.out.println("最终商品售价:"+finalProduct.getPrice()+"元");}操作效果:
乐观锁
乐观锁(Optimistic Locking)是基于 “并发操作中数据冲突发生概率极低” 的乐观并发假设设计的非阻塞式并发控制机制,核心目标是保障多线程 / 多进程并发更新场景下的数据一致性,避免更新操作覆盖其他并发事务产生的有效数据。
与悲观锁通过数据库原生锁机制预先对数据资源加锁、阻塞其他并发事务的冲突操作不同,乐观锁本质是无预锁定的冲突检测与冲突解决机制。其不依赖数据库层面的主动加锁来限制并发操作,而是在数据更新请求提交至数据库持久化层的最终阶段,通过版本号、时间戳等校验手段检测是否存在并发更新冲突,若检测到冲突则由业务层按需采取重试、终止操作等策略处理,以保证数据更新的正确性。
核心执行逻辑(以主流的版本号机制为例)
- 读取阶段:在事务上下文内读取目标数据时,同步获取该数据关联的版本标识(如version字段)的当前值,仅用于记录版本信息用于后续校验;
- 业务处理阶段:基于读取到的数据及版本值执行业务逻辑处理(该阶段仍保持无锁状态,其他并发事务对该数据的读取、操作不受任何阻塞);
- 更新校验与执行阶段:向数据库提交更新请求时,SQL 进行版本校验,仅当数据库中该数据的当前版本值与事务读取阶段获取的版本值完全一致时,才原子性执行更新操作,若版本校验不通过则判定存在并发更新冲突,此时由业务层按需执行重试、终止事务、返回冲突提示等策略处理。
模拟实现乐观锁
数据库层面添加 version 字段,默认值为0
ALTERTABLEproductADDCOLUMN`version`INTNOTNULLDEFAULT0COMMENT'乐观锁版本号';实体类层面标记 version 字段
@Data@NoArgsConstructor@AllArgsConstructorpublicclassProduct{privateLongid;privateStringname;privateIntegerprice;@VersionprivateIntegerversion;}添加乐观锁插件配置
@ConfigurationpublicclassMybatisPlusConfig{@BeanpublicMybatisPlusInterceptormybatisPlusInterceptor(){MybatisPlusInterceptorinterceptor=newMybatisPlusInterceptor();//添加分页插件interceptor.addInnerInterceptor(newPaginationInnerInterceptor(DbType.MYSQL));//添加乐观锁插件interceptor.addInnerInterceptor(newOptimisticLockerInnerInterceptor());returninterceptor;}}@SpringBootTestpublicclassMybatisConfigTest{@AutowiredprivateProductMapperproductMapper;/** * 乐观锁 */@TestpublicvoidtestConcurrentVersionUpdate(){Productp1=productMapper.selectById(1L);Productp2=productMapper.selectById(1L);p1.setPrice(p1.getPrice()+50);intresult1=productMapper.updateById(p1);System.out.println("小李修改的结果:"+p1.getPrice());p2.setPrice(p2.getPrice()-30);intresult2=productMapper.updateById(p2);System.out.println("小王修改的结果:"+p2.getPrice());if(result2==0){p2=productMapper.selectById(1L);p2.setPrice(p2.getPrice()-30);result2=productMapper.updateById(p2);}System.out.println("小王修改重试的结果:"+p2.getPrice());Productp3=productMapper.selectById(1L);System.out.println("老板看价格:"+p3.getPrice());}}