Mybatis-Plus-乐观锁

1. 设计思路

  • 获取需要更新的记录
  • 获取当前记录的版本号
  • 更新时带上历史版本号作为条件,当原始版本没变时,才能更新成功
  • 更新时,同时对历史版本号+1
  • 如果版本已经变过了,就不会更新成功

2. 代码示例

2.1 数据库新建一个测试表

1
2
3
4
5
6
7
CREATE TABLE `goods` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,
`num` bigint(20) DEFAULT NULL,
`version` int(11) DEFAULT '1',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

2.2 创建一个springboot项目

2.3 导入maven依赖

1
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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>cn.aacopy.learn</groupId>
<artifactId>mybatisplus-learn</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>mybatisplus-learn</name>
<description>mybatisplus-learn</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.1</version>
</dependency>

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

2.4 设置数据库连接配置

1
2
3
4
5
6
7
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://192.168.25.216:3306/aacopy
spring.datasource.username=root
spring.datasource.password=aacopy.cn

mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

2.5 添加mybatisPlus 乐观锁插件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package cn.aacopy.learn.mybatisplus.config;

import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
* @author iseven.yang
* @date 2022/11/2 20:56
*/
@Configuration
public class MybatisPlusConfig {

@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加乐观锁插件
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}
}

  • 同时需要添加mapper扫描路径
1
2
3
4
5
6
7
8
9
@SpringBootApplication
@MapperScan("cn.aacopy.learn.mybatisplus.mapper")
public class MybatisplusLearnApplication {

public static void main(String[] args) {
SpringApplication.run(MybatisplusLearnApplication.class, args);
}

}

2.6 通过代码生成工具生成service和mapper

2.7 编写测试代码

1
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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
package cn.aacopy.learn.mybatisplus;

import cn.aacopy.learn.mybatisplus.domain.GoodsDO;
import cn.aacopy.learn.mybatisplus.service.GoodsService;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
@Slf4j
class MybatisplusLearnApplicationTests {

@Autowired
private GoodsService goodsService;

@Test
void testMybatisPlusOptimisticLocker() {
log.info("测试乐观锁======start");
GoodsDO goodsDO = new GoodsDO();
goodsDO.setName("物品1");
goodsDO.setNum(100L);
goodsService.save(goodsDO);
Long id = goodsDO.getId();
//线程1:获取当前记录,睡眠1秒,再执行更新num为50
new Thread(() -> {
GoodsDO goodsDO1 = goodsService.getById(id);
log.info("{}, 保存物品成功,goodsDO={}", Thread.currentThread().getName(), goodsDO1);
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
goodsDO1.setNum(50L);
goodsService.updateById(goodsDO1);
}).start();
//线程2:获取当前记录,睡眠2秒,再执行更新num为200
new Thread(() -> {
GoodsDO goodsDO1 = goodsService.getById(id);
log.info("{}, 保存物品成功,goodsDO={}", Thread.currentThread().getName(), goodsDO1);
try {
Thread.sleep(2000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
goodsDO1.setNum(200L);
goodsService.updateById(goodsDO1);
}).start();
try {
Thread.sleep(3000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("更新物品数量成功,goodsDO={}", goodsService.getById(id));
log.info("测试乐观锁======end");
}

}

2.8 执行测试

1
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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
2022-11-02 23:22:44.378  INFO 20540 --- [           main] c.a.l.m.MybatisplusLearnApplicationTests : 测试乐观锁======start
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@79518e00] was not registered for synchronization because synchronization is not active
2022-11-02 23:22:44.434 INFO 20540 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting...
2022-11-02 23:22:45.119 INFO 20540 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.
JDBC Connection [HikariProxyConnection@970900790 wrapping com.mysql.cj.jdbc.ConnectionImpl@4a1a256d] will not be managed by Spring
==> Preparing: INSERT INTO goods ( name, num ) VALUES ( ?, ? )
==> Parameters: 物品1(String), 100(Long)
<== Updates: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@79518e00]
Creating a new SqlSession
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@17b1f1c0] was not registered for synchronization because synchronization is not active
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@584e9813] was not registered for synchronization because synchronization is not active
JDBC Connection [HikariProxyConnection@596671943 wrapping com.mysql.cj.jdbc.ConnectionImpl@4a1a256d] will not be managed by Spring
==> Preparing: SELECT id,name,num,version FROM goods WHERE id=?
==> Parameters: 8(Long)
<== Columns: id, name, num, version
<== Row: 8, 物品1, 100, 1
<== Total: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@17b1f1c0]
JDBC Connection [HikariProxyConnection@999750580 wrapping com.mysql.cj.jdbc.ConnectionImpl@4a1a256d] will not be managed by Spring
==> Preparing: SELECT id,name,num,version FROM goods WHERE id=?
2022-11-02 23:22:45.218 INFO 20540 --- [ Thread-2] c.a.l.m.MybatisplusLearnApplicationTests : Thread-2, 保存物品成功,goodsDO=GoodsDO(id=8, name=物品1, num=100, version=1)
==> Parameters: 8(Long)
<== Columns: id, name, num, version
<== Row: 8, 物品1, 100, 1
<== Total: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@584e9813]
2022-11-02 23:22:45.221 INFO 20540 --- [ Thread-3] c.a.l.m.MybatisplusLearnApplicationTests : Thread-3, 保存物品成功,goodsDO=GoodsDO(id=8, name=物品1, num=100, version=1)
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@50aa8c9b] was not registered for synchronization because synchronization is not active
JDBC Connection [HikariProxyConnection@908498390 wrapping com.mysql.cj.jdbc.ConnectionImpl@4a1a256d] will not be managed by Spring
==> Preparing: UPDATE goods SET name=?, num=?, version=? WHERE id=?
==> Parameters: 物品1(String), 50(Long), 1(Integer), 8(Long)
<== Updates: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@50aa8c9b]
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@432171d4] was not registered for synchronization because synchronization is not active
JDBC Connection [HikariProxyConnection@2006681630 wrapping com.mysql.cj.jdbc.ConnectionImpl@4a1a256d] will not be managed by Spring
==> Preparing: UPDATE goods SET name=?, num=?, version=? WHERE id=?
==> Parameters: 物品1(String), 200(Long), 1(Integer), 8(Long)
<== Updates: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@432171d4]
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@49433c98] was not registered for synchronization because synchronization is not active
JDBC Connection [HikariProxyConnection@190605872 wrapping com.mysql.cj.jdbc.ConnectionImpl@4a1a256d] will not be managed by Spring
==> Preparing: SELECT id,name,num,version FROM goods WHERE id=?
==> Parameters: 8(Long)
<== Columns: id, name, num, version
<== Row: 8, 物品1, 200, 1
<== Total: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@49433c98]
2022-11-02 23:22:48.214 INFO 20540 --- [ main] c.a.l.m.MybatisplusLearnApplicationTests : 更新物品数量成功,goodsDO=GoodsDO(id=8, name=物品1, num=200, version=1)
2022-11-02 23:22:48.214 INFO 20540 --- [ main] c.a.l.m.MybatisplusLearnApplicationTests : 测试乐观锁======end
2022-11-02 23:22:48.231 INFO 20540 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown initiated...
2022-11-02 23:22:48.237 INFO 20540 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown completed.

进程已结束,退出代码为 0

  • 执行结果发现,在模拟2个并发同时操作更新同一条数据时,两条SQL都执行成功了
  • 更新sql为 SELECT id,name,num,version FROM goods WHERE id=?
  • 执行最终结果为200
  • 更新后记录的version版本号为1

2.9 添加乐观锁配置

1
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
package cn.aacopy.learn.mybatisplus.domain;

import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;

import java.io.Serializable;

@TableName(value ="goods")
@Data
public class GoodsDO implements Serializable {
/**
*
*/
@TableId(type = IdType.AUTO)
private Long id;

private String name;

private Long num;
// 开启乐观锁
@Version
private Integer version;

@TableField(exist = false)
private static final long serialVersionUID = 1L;
}

在version字段上添加了@Version注解,用于开启乐观锁

2.10 有乐观锁的执行结果

1
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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
2022-11-02 23:45:44.194  INFO 7112 --- [           main] c.a.l.m.MybatisplusLearnApplicationTests : 测试乐观锁======start
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7d70638] was not registered for synchronization because synchronization is not active
2022-11-02 23:45:44.251 INFO 7112 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting...
2022-11-02 23:45:44.869 INFO 7112 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.
JDBC Connection [HikariProxyConnection@1287215032 wrapping com.mysql.cj.jdbc.ConnectionImpl@3cdc7b09] will not be managed by Spring
==> Preparing: INSERT INTO goods ( name, num ) VALUES ( ?, ? )
==> Parameters: 物品1(String), 100(Long)
<== Updates: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7d70638]
Creating a new SqlSession
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@3d4881fa] was not registered for synchronization because synchronization is not active
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7af5595d] was not registered for synchronization because synchronization is not active
JDBC Connection [HikariProxyConnection@1796948188 wrapping com.mysql.cj.jdbc.ConnectionImpl@3cdc7b09] will not be managed by Spring
==> Preparing: SELECT id,name,num,version FROM goods WHERE id=?
==> Parameters: 9(Long)
<== Columns: id, name, num, version
<== Row: 9, 物品1, 100, 1
<== Total: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@3d4881fa]
JDBC Connection [HikariProxyConnection@82868218 wrapping com.mysql.cj.jdbc.ConnectionImpl@3cdc7b09] will not be managed by Spring
==> Preparing: SELECT id,name,num,version FROM goods WHERE id=?
2022-11-02 23:45:44.950 INFO 7112 --- [ Thread-2] c.a.l.m.MybatisplusLearnApplicationTests : Thread-2, 保存物品成功,goodsDO=GoodsDO(id=9, name=物品1, num=100, version=1)
==> Parameters: 9(Long)
<== Columns: id, name, num, version
<== Row: 9, 物品1, 100, 1
<== Total: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7af5595d]
2022-11-02 23:45:44.952 INFO 7112 --- [ Thread-3] c.a.l.m.MybatisplusLearnApplicationTests : Thread-3, 保存物品成功,goodsDO=GoodsDO(id=9, name=物品1, num=100, version=1)
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@5a7240f4] was not registered for synchronization because synchronization is not active
JDBC Connection [HikariProxyConnection@1513447050 wrapping com.mysql.cj.jdbc.ConnectionImpl@3cdc7b09] will not be managed by Spring
==> Preparing: UPDATE goods SET name=?, num=?, version=? WHERE id=? AND version=?
==> Parameters: 物品1(String), 50(Long), 2(Integer), 9(Long), 1(Integer)
<== Updates: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@5a7240f4]
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@55205d07] was not registered for synchronization because synchronization is not active
JDBC Connection [HikariProxyConnection@829715892 wrapping com.mysql.cj.jdbc.ConnectionImpl@3cdc7b09] will not be managed by Spring
==> Preparing: UPDATE goods SET name=?, num=?, version=? WHERE id=? AND version=?
==> Parameters: 物品1(String), 200(Long), 2(Integer), 9(Long), 1(Integer)
<== Updates: 0
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@55205d07]
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@3bfae028] was not registered for synchronization because synchronization is not active
JDBC Connection [HikariProxyConnection@393594087 wrapping com.mysql.cj.jdbc.ConnectionImpl@3cdc7b09] will not be managed by Spring
==> Preparing: SELECT id,name,num,version FROM goods WHERE id=?
==> Parameters: 9(Long)
<== Columns: id, name, num, version
<== Row: 9, 物品1, 50, 2
<== Total: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@3bfae028]
2022-11-02 23:45:47.946 INFO 7112 --- [ main] c.a.l.m.MybatisplusLearnApplicationTests : 更新物品数量成功,goodsDO=GoodsDO(id=9, name=物品1, num=50, version=2)
2022-11-02 23:45:47.946 INFO 7112 --- [ main] c.a.l.m.MybatisplusLearnApplicationTests : 测试乐观锁======end
2022-11-02 23:45:47.960 INFO 7112 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown initiated...
2022-11-02 23:45:47.964 INFO 7112 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown completed.

进程已结束,退出代码为 0

  • 执行结果发现,在模拟2个并发同时操作更新同一条数据时,只有第一天更新50的成功了,更新200的SQL因为version不匹配更新失败
  • 更新sql为 UPDATE goods SET name=?, num=?, version=? WHERE id=? AND version=?
  • 执行最终结果为50
  • 更新后记录的version版本号为2

注意事项

  • 在更新的对象里version字段不能为NULL,必须要有值,不然不生效