MyBatis Plus 乐观锁与悲观锁
1、场景展现
一件商品,成本价是80元,售价是100元。老板先是通知小李,说你去把商品价格增加50元。小李正在玩游戏,耽搁了一个小时。正好一个小时后,老板觉得商品价格增加到150元,价格太高,可能会影响销量。又通知小王,你把商品价格降低30元。
此时,小李和小王同时操作商品后台系统。小李操作的时候,系统先取出商品价格100元;小王也在操作,取出的商品价格也是100元。小李将价格加了50元,并将100+50=150元存入了数据库;小王将商品减了30元,并将100-30=70元存入了数据库。是的,如果没有锁,小李的操作就完全被小王的覆盖了。
现在商品价格是70元,比成本价低10元。几分钟后,这个商品很快出售了1千多件商品,老板亏1万多。
2、乐观锁与悲观锁
如果是乐观锁,小王保存价格前,会检查下价格是否被人修改过了。如果被修改过了,则重新取出的被修改后的价格,150元,这样他会将120元存入数据库。
如果是悲观锁,小李取出数据后,小王只能等小李操作完之后,才能对价格进行操作,也会保证最终的价格是120元。
3、模拟示例
1.创建商品表
CREATE TABLEt_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)
);
2.添加一条数据
INSERT INTO t_product (id, NAME, price) VALUES (1, '外星人笔记本 ', 100);
3.添加 JavaBean
注意与表名的下划线构成驼峰命名
package com.goose.entity;import lombok.Data;@Data
public class TProduct {private Long id;private String name;private Integer price;private Integer version;
}
4.创建 mapper
注意添加注解 @Repository
package com.goose.mapper;import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.goose.entity.TProduct;
import org.springframework.stereotype.Repository;@Repository
public interface ProductMapper extends BaseMapper<TProduct> {}
5.没有乐观锁时
这里直接使用mapper对象进行测试,不创建Service和ServiceImpl
@SpringBootTest
public class HappyAndUpsetTest {@Autowiredpublic ProductMapper productMapper;
}
不加锁时
@Test
public void test01() {//1、小李TProduct p1 = productMapper.selectById(1L);System.out.println("小李取出的价格:" + p1.getPrice());//2、小王TProduct p2 = productMapper.selectById(1L);System.out.println("小王取出的价格:" + p2.getPrice());//3、小李将价格加了50元,存入了数据库p1.setPrice(p1.getPrice() + 50);int result1 = productMapper.updateById(p1);System.out.println("小李修改结果:" + result1);//4、小王将商品减了30元,存入了数据库p2.setPrice(p2.getPrice() - 30);int result2 = productMapper.updateById(p2);System.out.println("小王修改结果:" + result2);//最后的结果TProduct p3 = productMapper.selectById(1L);//价格覆盖,最后的结果:70System.out.println("最后的结果:" + p3.getPrice());
}
输出:
最后的结果:70
6.添加乐观锁
(1)修改实体类,添加 @Version 注解
package com.goose.entity;import com.baomidou.mybatisplus.annotation.Version;
import lombok.Data;@Data
public class TProduct {private Long id;private String name;private Integer price;@Versionprivate Integer version;
}
(2)添加乐观锁配置
package com.goose.config;import com.baomidou.mybatisplus.annotation.DbType;
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 MyConfig {@Beanpublic MybatisPlusInterceptor mybatisPlusInterceptor() {MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();interceptor.addInnerInterceptor(newPaginationInnerInterceptor(DbType.MYSQL));//添加乐观锁插件interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());return interceptor;}
}
(3)测试
@Test
public void testConcurrentVersionUpdate() {
//小李取数据TProduct p1 = productMapper.selectById(1L);
//小王取数据TProduct p2 = productMapper.selectById(1L);
//小李修改 + 50p1.setPrice(p1.getPrice() + 50);int result1 = productMapper.updateById(p1);System.out.println("小李修改的结果:" + result1);
//小王修改 - 30p2.setPrice(p2.getPrice() - 30);int result2 = productMapper.updateById(p2);System.out.println("小王修改的结果:" + result2);if (result2 == 0) {//失败重试,重新获取version并更新p2 = productMapper.selectById(1L);p2.setPrice(p2.getPrice() - 30);result2 = productMapper.updateById(p2);}System.out.println("小王修改重试的结果:" + result2);//老板看价格TProduct p3 = productMapper.selectById(1L);System.out.println("老板看价格:" + p3.getPrice());
}
输出:
老板看价格:90
解读:
🧩 第一步:两个线程分别读取同一个商品数据
TProduct p1 = productMapper.selectById(1L); // 小李
TProduct p2 = productMapper.selectById(1L); // 小王
数据库当前数据为:
{id: 1,name: "外星人笔记本"price: 100,version: 1
}
- 小李拿到的是:price=100, version=1
- 小王拿到的也是:price=100, version=1
p1.setPrice(150);
int result1 = productMapper.updateById(p1);
MyBatis Plus 执行类似 SQL:
UPDATE product
SET price = 150, version = version + 1
WHERE id = 1 AND version = 1
✅ 由于数据库中 version = 1,匹配成功,更新成功,version 自动加 1:
数据库变为:
{id: 1,price: 150,version: 2
}
输出:
小李修改的结果:1
🧩 第三步:小王尝试修改价格(-30)
p2.setPrice(70);
int result2 = productMapper.updateById(p2);
MyBatis Plus 执行的 SQL:
UPDATE product
SET price = 70, version = version + 1
WHERE id = 1 AND version = 1
⚠️ 此时数据库 version 已是 2,**不再匹配** → **更新失败** → 返回 0。
输出:
小王修改的结果:0
🧩 第四步:小王检测失败,重试更新
p2 = productMapper.selectById(1L); // 重新读取最新数据(price=150, version=2)
p2.setPrice(p2.getPrice() - 30); // 150 - 30 = 120
result2 = productMapper.updateById(p2); // version=2
MyBatis Plus 执行的 SQL:
UPDATE product
SET price = 120, version = version + 1
WHERE id = 1 AND version = 2
✅ version 匹配,更新成功 → 数据变为:
{id: 1,price: 120,version: 3
}
输出:
小王修改重试的结果:1
🧩 第五步:老板查询商品价格
TProduct p3 = productMapper.selectById(1L);
System.out.println("老板看价格:" + p3.getPrice());
最终输出:
老板看价格:120
执行流程图
1. 小李读取:price=100, version=1
2. 小王读取:price=100, version=1
3. 小李更新成功 → price=150, version=2
4. 小王更新失败(version=1 不匹配)
5. 小王重试 → 重新读取 → price=150, version=2
6. 小王再次更新成功 → price=120, version=3
7. 老板看到价格 = 120