场景: 一件商品,成本价是80元,售价是100元。老板先是通知小李,说你去把商品价格增加50元。小李正在玩游戏,耽搁了一个小时。正好一个小时 后,老板觉得商品价格增加到150元,价格太高,可能会影响销量。又通知小王,你把商品价格降低30元。此时,小李和小王同时操作商品后 台系统。小李操作的时候,系统先取出商品价格100元;小王也在操作,取出的商品价格也是100元。小李将价格加了50元,并将100+50=150元 存入了数据库;小王将商品减了30元,并将100-30=70元存入了数据库。是的,如果没有锁,小李的操作就完全被小王的覆盖了。现在商品价格 是70元,比成本价低10元。几分钟后,这个商品很快出售了1千多件商品,老板亏1万多
分析: 上面的故事,如果是乐观锁,小王保存价格前,会检查下价格是否被人修改过了。如果被修改过了,则重新取出的被修改后的价格,150元,这 样他会将120元存入数据库。如果是悲观锁,小李取出数据后,小王只能等小李操作完之后,才能对价格进行操作,也会保证最终的价格是120元
具体操作如下:
第一步: 准备数据,在数据库创建商品表并添加如下数据
CREATE TABLE t_product(
id BIGINT(20) NOT NULL COMMENT '主键ID',
NAME VARCHAR(30) NULL DEFAULT NULL COMMENT '商品名称',
price INT(11) DEFAULT 0 COMMENT '价格',
VERSION INT(11) DEFAULT 0 COMMENT '乐观锁版本号',
PRIMARY KEY (id)
);
INSERT INTO t_product (id, NAME, price) VALUES (1, '外星人笔记本', 100);
第二步: 创建实体类,例如在src/main/java/com.huanf.mybatisplus/poji目录下新建Product类,写入如下
//该注解是为成员变量生成对应的get/set方法、toString方法
public class Product {
private Long id;
private String name;
private Integer price;
private Integer version;
}
第三步: 创建mapper接口,例如在src/main/java/com.huanf.mybatisplus/mapper目录下新建ProductMapper接口并写入如下
//将类或接口表示为持久层组件
public interface ProductMapper extends BaseMapper<Product> {
}
第四步: 在MyBatisPlusPluginsTest类写测试代码,例如添加如下,注意自动装配写在类的外面,测试写在类的里面
xxxxxxxxxx
//自动装配的注解,用于使用我们写的UserService接口
private ProductMapper productMapper;
//模拟乐观锁和悲观锁的问题
public void zidingyi_testPage4(){
//第一步: 小李查询商品价格
Product product_Li = productMapper.selectById(1);//查询id为1的商品数据
System.out.println("小李查询的商品价格: "+product_Li);
//第二步: 小王查询商品价格
Product product_Wang = productMapper.selectById(1);//查询id为1的商品数据
System.out.println("小王查询的商品价格: "+product_Wang);
//第三步: 小李将商品价格+50
product_Li.setPrice(product_Li.getPrice()+50);
productMapper.updateById(product_Li);//数据更新
//第四步: 小王将商品价格-30
product_Wang.setPrice(product_Wang.getPrice()-30);
productMapper.updateById(product_Wang);//数据更新
//第五步: 老板查询商品价格
Product product_boss = productMapper.selectById(1);//查询id为1的商品数据
System.out.println("老板查询的商品价格: "+product_boss);
//然后运行测试,即可模拟乐观锁和悲观锁的问题,原价100成本价80,被小王和小李修改之后,价格变成70,老板就每件亏10元
//问题: 100-30=70
//老板预期: 100+50-30=120
//如何解决,即如何才能达到老板的预期,会在下面的测试进行解决
}
原理: 每一次查询或更新,会进行一次版本号查询 例如在数据库中添加version字段,然后取出记录时,获取当前version,更新时,version + 1,如果where语句中的version版本不对,则更新失败 简单说就是我们在操作数据时假设拿到的版本号是1,当我们想要提交数据时,版本号必须也为1才能成功提交数据,不然就提交失败
先确保你有一个字段是控制版本号的,然后你的实体类里面也是有这个成员变量,然后才能进行如下操作
具体操作如下:
第一步: 在实体类的控制版本号的字段添加@Version注解,例如
//该注解是用来表示版本号字段
private Integer version;
第二步: 在MyBatisPlusConfig类,注意该类是我们的配置类,添加插件配置。注意我们只需要关注下面的乐观锁插件,其它的代码是前面留下来的
xxxxxxxxxx
//这个注解表示该类是配置类
"com.huanf.mybatisplus.mapper")//该注解用于扫描mapper接口所在的包,是从MyBatisplusApplication类剪切过来的 (
public class MyBatisPlusConfig {
//通过MybatisPlusInterceptor拦截器来配置MyBatis-Plus中的分页插件
public MybatisPlusInterceptor zidingyi_mybatisPlusInterceptor(){
//先创建MybatisPlusInterceptor拦截器的对象,创建出来的对象就是插件对象
MybatisPlusInterceptor zidingyi_interceptor = new MybatisPlusInterceptor();
//addInnerInterceptor表示内部插件,PaginationInnerInterceptor表示分页插件,DbType.MYSQL表示数据库类型是MySQL
zidingyi_interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
//addInnerInterceptor表示内部插件,OptimisticLockerInnerInterceptor表示乐观锁插件
zidingyi_interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
//返回你创建好的插件对象
return zidingyi_interceptor;
//如何测试,也就是如何写功能,在src/test/java/com.huanf.mybatisplus目录下新建MyBatisPlusPluginsTest测试类,具体代码见MyBatisPlusPluginsTest类
}
}
第三步: 先去数据库把价格重新改为原价100,然后再去执行'【模拟上面的场景,模拟冲突】'里面写的zidingyi_testPage4方法
查询结果: 会发现此时老板查到的数据是150,也就是只有小李+50,没有小王-20,所以100+50=150
思考为什么小李没有-20成功: 小李先拿到价格的修改权,然后再小王拿到价格的修改权,他俩的版本号是一致的,但是小李先提交,小李提交之后价格的版本号就变了, 由于小王的版本号还是之前的,到小王提交时,版本号对不上,所以小王就没有更新成功。注意代码的执行顺序从上往下执行
使用乐观锁的结论: 虽然没有完成老板的要求,但是起码不会冲突导致老板亏本
由于小王的操作没有执行成功,所以我们来解决一下
这里就直接把新的test测试写出来了,如下
xxxxxxxxxx
//解决小王操作没有执行成功的问题
//思路: 当小王要执行-30操作时,先判断一下小王是否操作成功,没有成功的话,重试
public void zidingyi_testPage5(){
//第一步: 小李查询商品价格
Product product_Li = productMapper.selectById(1);//查询id为1的商品数据
System.out.println("小李查询的商品价格: "+product_Li);
//第二步: 小王查询商品价格
Product product_Wang = productMapper.selectById(1);//查询id为1的商品数据
System.out.println("小王查询的商品价格: "+product_Wang);
//第三步: 小李将商品价格+50
product_Li.setPrice(product_Li.getPrice()+50);
productMapper.updateById(product_Li);//数据更新
//第四步: 小王将商品价格-30
product_Wang.setPrice(product_Wang.getPrice()-30);
int result = productMapper.updateById(product_Wang);//数据更新。返回值是受影响的行数
if(result == 0){//受影响行数为0,表示操作失败,我们在循环里面再操作一次即可
//第二次操作,先查版本号
Product productNew = productMapper.selectById(1);
//再次尝试修改价格
productNew.setPrice(product_Li.getPrice()-30);
productMapper.updateById(productNew);//数据更新
}
//第五步: 老板查询商品价格
Product product_boss = productMapper.selectById(1);//查询id为1的商品数据
System.out.println("老板查询的商品价格: "+product_boss);
//如果能查出价格为120就说明成功
}