# MyBatis-Plus教程 - 13 悲观锁和乐观锁
在讲解悲观锁和乐观锁之前,先说一下可能遇到的问题。
# 13.1 并发问题
以商品减库存为例,假设有一件 ID 为 1
的商品,库存为 100,事务A
和 事务B
同时购买 10 件库存。
购买时,事务A
先查询商品的库存为 100,在 100 的基础上减10 ,更新库存为 90;事务B
同时查询到库存为 100,也在 100 的基础上减10,更新库存为 90。事务A
和 事务B
先后提交(A先B后),导致最终库存值变为 90
,而不是 80
,而 事务A
的更新被覆盖,库存减少 10 的操作丢失了。
你可能会想,通过如下的 SQL 操作是否可以解决:
UPDATE tb_prod SET prod_count = prod_count - 10 WHERE id = 1 AND prod_count > 0;
prod_count = prod_count - 10
是直接在数据库中执行的,不存在中间读取数据的步骤,因此可以避免在应用层读取库存后再减库存带来的并发问题。
但是数据库的事务隔离级别仍然可能导致并发问题,因为数据库的隔离级别可能设置的不是串行化的,如果数据库的隔离级别设置为串行化的,那么可以解决这个问题,但这种方式可能会导致性能下降。
如果数据库隔离级别不是串行化的,那么两个事务同时执行这条语句的时候,会先读取到 prod_count
的值,那么它们读取到的值将都是100,并判断 prod_count > 0
成立,那么后面再同时执行 UPDATE 语句,都设置 prod_count = 100 - 10
,还是出现了问题。
虽然 UPDATE
操作是原子性的,数据库会确保 事务A
的 prod_count = prod_count - 10
操作不会被其他事务中断,但它不能解决并发事务中的竞争问题,也就是说 事务A
和 事务B
在更新操作之前,都需要读取相同的初始值 prod_count = 100
,这部分读取过程并不受原子性保障,因此两个事务都可以基于相同的初始值来进行更新计算。所以出问题了。
怎么解决呢,下面说一下悲观锁和乐观锁。
# 13.2 悲观锁和乐观锁
# 1 悲观锁
悲观锁就是很悲观,总有刁民想害朕,认为在我修改的时候,肯定有别人也在修改,所以在查询数据的时候就会对数据进行加锁,此时别人是无法查询和修改数据的,必须等我修改完成,别人才能查询和修改数据。
在使用悲观锁的时候,查询和修改数据是一致的,在查询到的数据的基础上修改不会出问题。但是悲观锁会降低系统的并发性能,如果对数据一致性要求非常高,并且可以接受一定的性能损失,那么可以使用悲观锁。
# 2 乐观锁
乐观锁相对于乐观一些,在查询数据的时候是不会上锁的,在执行更新的时候判断数据是否被修改过,如果没有修改过就修改,如果被修改过了,就返回修改失败。
实现的思路一般是在数据库的表中添加一个 version
的字段,在要修改数据之前查询到 version
字段的值,然后在修改的时候,判断 version
的值是否和查询到的一致,如果一致,那么就可以修改数据,同时将 version 的值 +1
,如果不一致,说明数据被修改了,则不执行更新。
对应执行的 SQL 大概是:
update 表 set 字段=新值, version = version + 1 where version = 查询到的verison
乐观锁不会影响数据的查询,并发效率高。存在的问题是可能看到的数据不是真实的数据,例如看到有库存,然后购买,购买的时候提示没有库存了。
# 13.3 MyBatis-Plus实现乐观锁
下面看一下在 MyBatis-Plus 中,如何实现乐观锁。
# 1 修改数据库表
在数据库表中添加一个 version 字段(自定义的):
ALTER TABLE tb_user
ADD COLUMN version int DEFAULT 1;
2
默认值可以设置为 1。
# 2 修改pojo
修改实体类,添加 version
属性与数据库对应。
User.java:
@Data
@TableName("tb_user")
public class User {
// ... 其他属性
@Version
private Integer version;
}
2
3
4
5
6
7
8
需要添加 @Version
注解,标识该属性是用来做乐观锁版本控制的。
# 3 插件配置
MyBatis-Plus 中的乐观锁也是通过拦截器来实现的,拦截执行的 SQL,添加版本控制。
和之前的分页插件配置是一样的,在自定义的 MyBatis-Plus 配置类中进行配置:
package com.foooor.helloplus.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 MyBatisPlusConfig {
/**
* 添加分页插件
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 乐观锁配置
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
// 分页插件,如果配置多个插件, 切记分页最后添加
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
添加乐观锁配置。
# 4 测试
编写测试方法进行测试。
首先查询用户,然后修改用户信息:
@Test
void testUpdate() {
// 1.先查询用户
User user = userMapper.selectById(2);
log.info("查询结果:{}", user);
// 2.执行更新操作
user.setUsername("我最牛逼");
int result = userMapper.updateById(user);
log.info("更新结果:{}", result);
}
2
3
4
5
6
7
8
9
10
11
12
13
查询出 user 后,里面包含了 version 信息,然后更新的时候,会比较 user 中的 version 信息。
生成的 SQL 如下:
UPDATE tb_user SET username=?, password=?, age=?, email=?, create_time=?, update_time=?, version=? WHERE id=? AND version=?
所以在查询后,不要手动修改 version 的值,例如下面手动修改了version 的值,那么更新的结果就是 0,因为更新时候的 version 和查询出来的不一样:
@Test
void testUpdate() {
// 1.先查询用户
User user = userMapper.selectById(2);
log.info("查询结果:{}", user);
// 2.执行更新操作
user.setUsername("我最牛逼");
user.setVersion(1); // 修改了version的值,和查询出的不一样
int result = userMapper.updateById(user);
log.info("更新结果:{}", result);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
← 12-自动填充 14-防全表更新与删除插件 →