当程序中出现并发访问时,就需要保证数据的一致性。以商品系统为例,现在有两个管理员均想对同一件售价为 100 元的商品进行修改,A 管理员正准备将商品售价改为 150 元,但此时出现了网络问题,导致 A 管理员的操作陷入了等待状态;此时 B 管理员也进行修改,将商品售价改为了 200 元,修改完成后 B 管理员退出了系统,此时 A 管理员的操作也生效了,这样便使得 A 管理员的操作直接覆盖了 B 管理员的操作,B 管理员后续再进行查询时会发现商品售价变为了 150 元,这样的情况是绝对不允许发生的。
要想解决这一问题,可以给数据表加锁,常见的方式有两种:

  1. 乐观锁
  2. 悲观锁

悲观锁认为并发情况一定会发生,所以在某条数据被修改时,为了避免其它人修改,会直接对数据表进行加锁,它依靠的是数据库本身提供的锁机制(表锁、行锁、读锁、写锁)。
而乐观锁则相反,它认为数据产生冲突的情况一般不会发生,所以在修改数据的时候并不会对数据表进行加锁的操作,而是在提交数据时进行校验,判断提交上来的数据是否会发生冲突,如果发生冲突,则提示用户重新进行操作,一般的实现方式为 设置版本号字段 。
就以商品售价为例,在该表中设置一个版本号字段,让其初始为 1,此时 A 管理员和 B 管理员同时需要修改售价,它们会先读取到数据表中的内容,此时两个管理员读取到的版本号都为 1,此时 B 管理员的操作先生效了,它就会将当前数据表中对应数据的版本号与最开始读取到的版本号作一个比对,发现没有变化,于是修改就生效了,此时版本号加 1。
而 A 管理员马上也提交了修改操作,但是此时的版本号为 2,与最开始读取到的版本号并不对应,这就说明数据发生了冲突,此时应该提示 A 管理员操作失败,并让 A 管理员重新查询一次数据。

乐观锁的优势在于采取了更加宽松的加锁机制,能够提高程序的吞吐量,适用于读操作多的场景。
那么接下来我们就来模拟这一过程。

1.创建一张新的数据表
create table shop(id bigint(20) not null auto_increment,name varchar(30) not null,price int(11) default 0,version int(11) default 1,primary key(id));insert into shop(id,name,price) values(1,'笔记本电脑',8000);
2.创建实体类
@Datapublic class Shop {private Long id;private String name;private Integer price;private Integer version;}
3.创建对应的 Mapper 接口
public interface ShopMapper extends BaseMapper {}
4.编写测试代码
/** * 模拟并发场景 */@Testvoid hh2() {// A、B管理员读取数据Shop a = shopMapper.selectById(1L);Shop b = shopMapper.selectById(1L);// B管理员先修改b.setPrice(9000);int result = shopMapper.updateById(b);if (result == 1) {System.out.println("B管理员修改成功!");} else {System.out.println("B管理员修改失败!");}// A管理员后修改a.setPrice(8500);int result2 = shopMapper.updateById(a);if (result2 == 1) {System.out.println("A管理员修改成功!");} else {System.out.println("A管理员修改失败!");}// 最后查询System.out.println(shopMapper.selectById(1L));}

执行结果:

问题出现了,B 管理员的操作被 A 管理员覆盖,那么该如何解决这一问题呢?
MyBatisPlus 提供了乐观锁机制,只需要在实体类中使用 @Version 声明版本号属性:

@Datapublic class Shop { private Long id; private String name; private Integer price; @Version // 声明版本号属性 private Integer version;}
  • 支持的数据类型只有:int,Integer,long,Long,Date,Timestamp,LocalDateTime
  • 整数类型下 newVersion = oldVersion + 1
  • newVersion 会回写到 entity 中
  • 仅支持 updateById(id) 与 update(entity, wrapper) 方法
  • 在 update(entity, wrapper) 方法下, wrapper 不能复用!!!

然后注册乐观锁插件:

@Configurationpublic class MyBatisConfig {/** * 旧版 */@Beanpublic OptimisticLockerInterceptor optimisticLockerInterceptor() {return new OptimisticLockerInterceptor();}/** * 注册插件(新版) * @return */@Beanpublic MybatisPlusInterceptor mybatisPlusInterceptor() {MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();// 分页插件interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));// 乐观锁插件interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());return interceptor;}}

重新执行测试代码,结果如下: