终于渡过漫长的自我隔离期,健康的活着真好;为武汉祈福,希望快点渡过,能早日回归大武汉 😭😭😭
上一篇博文介绍了声明式事务@Transactional
的简单使用姿势,最文章的最后给出了这个注解的多个属性,本文将着重放在事务隔离级别的知识点上,并通过实例演示不同的事务隔离级别下,脏读、不可重复读、幻读的具体场景
I. 基础知识
在进入正文之前,先介绍一下事务隔离级别的一些基础知识点,详细内容,推荐参考博文
mysql之锁与事务
1. 基本概念
以下基本概念源于个人理解之后,通过简单的case进行描述,如有问题,欢迎拍砖
更新丢失
简单来讲,两个事务A,B分别更新一条记录的filedA, filedB字段,其中事务B异常,导致回滚,将这条记录的恢复为修改之前的状态,导致事务A的修改丢失了,这就是更新丢失
脏读
读取到另外一个事务未提交的修改,所以当另外一个事务是失败导致回滚的时候,这个读取的数据其实是不准确的,这就是脏读
不可重复读
简单来讲,就是一个事务内,多次查询同一个数据,返回的结果居然不一样,这就是不可重复度(重复读取的结果不一样)
幻读
同样是多次查询,但是后面查询时,发现多了或者少了一些记录
比如:查询id在[1,10]之间的记录,第一次返回了1,2,3三条记录;但是另外一个事务新增了一个id为4的记录,导致再次查询时,返回了1,2,3,4四条记录,第二次查询时多了一条记录,这就是幻读
幻读和不可重复读的主要区别在于:
- 幻读针对的是查询结果为多个的场景,出现了数据的增加or减少
- 不可重复度读对的是某些特定的记录,这些记录的数据与之前不一致
2. 隔离级别
后面测试的数据库为mysql,引擎为innodb,对应有四个隔离级别
隔离级别 |
说明 |
fix |
not fix |
RU(read uncommitted) |
未授权读,读事务允许其他读写事务;未提交写事务禁止其他写事务(读事务ok) |
更新丢失 |
脏读,不可重复读,幻读 |
RC(read committed) |
授权读,读事务允许其他读写事务;未提交写事务,禁止其他读写事务 |
更新丢失,脏读 |
不可重复读,幻读 |
RR(repeatable read) |
可重复度,读事务禁止其他写事务;未提交写事务,禁止其他读写事务 |
更新丢失,脏读,不可重复度 |
幻读 |
serializable |
序列化读,所有事务依次执行 |
更新丢失,脏读,不可重复度,幻读 |
- |
说明,下面存为个人观点,不代表权威,谨慎理解和引用
- 我个人的观点,rr级别在mysql的innodb引擎上,配合mvvc + gap锁,已经解决了幻读问题
- 下面这个case是幻读问题么?
- 从锁的角度来看,步骤1、2虽然开启事务,但是属于快照读;而9属于当前读;他们读取的源不同,应该不算在幻读定义中的同一查询条件中

II. 配置
接下来进入实例演示环节,首先需要准备环境,创建测试项目
创建一个SpringBoot项目,版本为2.2.1.RELEASE
,使用mysql作为目标数据库,存储引擎选择Innodb
,事务隔离级别为RR
1. 项目配置
在项目pom.xml
文件中,加上spring-boot-starter-jdbc
,会注入一个DataSourceTransactionManager
的bean,提供了事务支持
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> ```
### 2. 数据库配置
进入spring配置文件`application.properties`,设置一下db相关的信息
```properties ## DataSource spring.datasource.url=jdbc:mysql://127.0.0.1:3306/story?useUnicode=true&characterEncoding=UTF-8&useSSL=false spring.datasource.username=root spring.datasource.password=
|
3. 数据库
新建一个简单的表结构,用于测试
1 2 3 4 5 6 7 8 9 10
| CREATE TABLE `money` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `name` varchar(20) NOT NULL DEFAULT '' COMMENT '用户名', `money` int(26) NOT NULL DEFAULT '0' COMMENT '钱', `is_deleted` tinyint(1) NOT NULL DEFAULT '0', `create_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`), KEY `name` (`name`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;
|
III. 实例演示
1. 初始化数据
准备一些用于后续操作的数据
1 2 3 4 5 6 7 8 9 10 11 12
| @Component public class DetailDemo { @Autowired private JdbcTemplate jdbcTemplate;
@PostConstruct public void init() { String sql = "replace into money (id, name, money) values (320, '初始化', 200)," + "(330, '初始化', 200)," + "(340, '初始化', 200)," + "(350, '初始化', 200)"; jdbcTemplate.execute(sql); } }
|
提供一些基本的查询和修改方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| private boolean updateName(int id) { String sql = "update money set `name`='更新' where id=" + id; jdbcTemplate.execute(sql); return true; }
public void query(String tag, int id) { String sql = "select * from money where id=" + id; Map map = jdbcTemplate.queryForMap(sql); System.out.println(tag + " >>>> " + map); }
private boolean updateMoney(int id) { String sql = "update money set `money`= `money` + 10 where id=" + id; jdbcTemplate.execute(sql); return false; }
|
2. RU隔离级别
我们先来测试RU隔离级别,通过指定@Transactional
注解的isolation
属性来设置事务的隔离级别
通过前面的描述,我们知道RU会有脏读问题,接下来设计一个case,进行演示
事务一,修改数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
|
@Transactional(isolation = Isolation.READ_UNCOMMITTED, rollbackFor = Exception.class) public boolean ruTransaction(int id) throws InterruptedException { if (this.updateName(id)) { this.query("ru: after updateMoney name", id); Thread.sleep(2000); if (this.updateMoney(id)) { return true; } } this.query("ru: after updateMoney money", id); return false; }
|
只读事务二(设置readOnly为true,则事务为只读)多次读取相同的数据,我们希望在事务二的第一次读取中,能获取到事务一的中间修改结果(所以请注意两个方法中的sleep使用)
1 2 3 4 5 6 7
| @Transactional(readOnly = true, isolation = Isolation.READ_UNCOMMITTED, rollbackFor = Exception.class) public boolean readRuTransaction(int id) throws InterruptedException { this.query("ru read only", id); Thread.sleep(1000); this.query("ru read only", id); return true; }
|
接下来属于测试的case,用两个线程来调用只读事务,和读写事务
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
| @Component public class DetailTransactionalSample { @Autowired private DetailDemo detailDemo;
public void testRuIsolation() throws InterruptedException { int id = 330; new Thread(new Runnable() { @Override public void run() { call("ru: 只读事务 - read", id, detailDemo::readRuTransaction); } }).start();
call("ru 读写事务", id, detailDemo::ruTransaction); } }
private void call(String tag, int id, CallFunc<Integer, Boolean> func) { System.out.println("============ " + tag + " start ========== "); try { func.apply(id); } catch (Exception e) { } System.out.println("============ " + tag + " end ========== \n"); }
@FunctionalInterface public interface CallFunc<T, R> { R apply(T t) throws Exception; }
|
输出结果如下
1 2 3 4 5 6 7 8 9
| ============ ru 读写事务 start ========== ============ ru: 只读事务 - read start ========== ru read only >>>> {id=330, name=初始化, money=200, is_deleted=false, create_at=2020-01-20 11:37:51.0, update_at=2020-01-20 11:37:51.0} ru: after updateMoney name >>>> {id=330, name=更新, money=200, is_deleted=false, create_at=2020-01-20 11:37:51.0, update_at=2020-01-20 11:37:52.0} ru read only >>>> {id=330, name=更新, money=200, is_deleted=false, create_at=2020-01-20 11:37:51.0, update_at=2020-01-20 11:37:52.0} ============ ru: 只读事务 - read end ==========
ru: after updateMoney money >>>> {id=330, name=更新, money=210, is_deleted=false, create_at=2020-01-20 11:37:51.0, update_at=2020-01-20 11:37:54.0} ============ ru 读写事务 end ==========
|
关注一下上面结果中ru read only >>>>
开头的记录,首先两次输出结果不一致,所以不可重复读问题是存在的
其次,第二次读取的数据与读写事务中的中间结果一致,即读取到了未提交的结果,即为脏读
3. RC事务隔离级别
rc隔离级别,可以解决脏读,但是不可重复读问题无法避免,所以我们需要设计一个case,看一下是否可以读取另外一个事务提交后的结果
在前面的测试case上,稍微改一改
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
|
@Transactional(readOnly = true, isolation = Isolation.READ_COMMITTED, rollbackFor = Exception.class) public boolean readRcTransaction(int id) throws InterruptedException { this.query("rc read only", id); Thread.sleep(1000); this.query("rc read only", id); Thread.sleep(3000); this.query("rc read only", id); return true; }
@Transactional(isolation = Isolation.READ_COMMITTED, rollbackFor = Exception.class) public boolean rcTranaction(int id) throws InterruptedException { if (this.updateName(id)) { this.query("rc: after updateMoney name", id); Thread.sleep(2000); if (this.updateMoney(id)) { return true; } }
return false; }
|
测试用例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
|
private void testRcIsolation() throws InterruptedException { int id = 340; new Thread(new Runnable() { @Override public void run() { call("rc: 只读事务 - read", id, detailDemo::readRcTransaction); } }).start();
Thread.sleep(1000);
call("rc 读写事务 - read", id, detailDemo::rcTranaction); }
|
输出结果如下
1 2 3 4 5 6 7 8 9
| ============ rc: 只读事务 - read start ========== rc read only >>>> {id=340, name=初始化, money=200, is_deleted=false, create_at=2020-01-20 11:46:17.0, update_at=2020-01-20 11:46:17.0} ============ rc 读写事务 - read start ========== rc: after updateMoney name >>>> {id=340, name=更新, money=200, is_deleted=false, create_at=2020-01-20 11:46:17.0, update_at=2020-01-20 11:46:23.0} rc read only >>>> {id=340, name=初始化, money=200, is_deleted=false, create_at=2020-01-20 11:46:17.0, update_at=2020-01-20 11:46:17.0} ============ rc 读写事务 - read end ==========
rc read only >>>> {id=340, name=更新, money=210, is_deleted=false, create_at=2020-01-20 11:46:17.0, update_at=2020-01-20 11:46:25.0} ============ rc: 只读事务 - read end ==========
|
从上面的输出中,在只读事务,前面两次查询,结果一致,虽然第二次查询时,读写事务修改了这个记录,但是并没有读取到这个中间记录状态,所以这里没有脏读问题;
当读写事务完毕之后,只读事务的第三次查询中,返回的是读写事务提交之后的结果,导致了不可重复读
4. RR事务隔离级别
针对rr,我们主要测试一下不可重复读的解决情况,设计case相对简单
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
|
@Transactional(readOnly = true, isolation = Isolation.REPEATABLE_READ, rollbackFor = Exception.class) public boolean readRrTransaction(int id) throws InterruptedException { this.query("rr read only", id); Thread.sleep(3000); this.query("rr read only", id); return true; }
@Transactional(isolation = Isolation.REPEATABLE_READ, rollbackFor = Exception.class) public boolean rrTransaction(int id) { if (this.updateName(id)) { this.query("rr: after updateMoney name", id); if (this.updateMoney(id)) { return true; } }
return false; }
|
我们希望读写事务的执行周期在只读事务的两次查询之内,所有测试代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
private void testReadOnlyCase() throws InterruptedException { int id = 320; new Thread(new Runnable() { @Override public void run() { call("rr 只读事务 - read", id, detailDemo::readRrTransaction); } }).start();
Thread.sleep(1000);
call("rr 读写事务", id, detailDemo::rrTransaction); }
|
输出结果
1 2 3 4 5 6 7 8
| ============ rr 只读事务 - read start ========== rr read only >>>> {id=320, name=初始化, money=200, is_deleted=false, create_at=2020-01-20 11:46:17.0, update_at=2020-01-20 11:46:17.0} ============ rr 读写事务 start ========== rr: after updateMoney name >>>> {id=320, name=更新, money=200, is_deleted=false, create_at=2020-01-20 11:46:17.0, update_at=2020-01-20 11:46:28.0} ============ rr 读写事务 end ==========
rr read only >>>> {id=320, name=初始化, money=200, is_deleted=false, create_at=2020-01-20 11:46:17.0, update_at=2020-01-20 11:46:17.0} ============ rr 只读事务 - read end ==========
|
两次只读事务的输出一致,并没有出现上面的不可重复读问题
说明
@Transactional
注解的默认隔离级别为Isolation#DEFAULT
,也就是采用数据源的隔离级别,mysql innodb引擎默认隔离级别为RR(所有不额外指定时,相当于RR)
5. SERIALIZABLE事务隔离级别
串行事务隔离级别,所有的事务串行执行,实际的业务场景中,我没用过… 也不太能想像,什么场景下需要这种
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
| @Transactional(readOnly = true, isolation = Isolation.SERIALIZABLE, rollbackFor = Exception.class) public boolean readSerializeTransaction(int id) throws InterruptedException { this.query("serialize read only", id); Thread.sleep(3000); this.query("serialize read only", id); return true; }
@Transactional(isolation = Isolation.SERIALIZABLE, rollbackFor = Exception.class) public boolean serializeTransaction(int id) { if (this.updateName(id)) { this.query("serialize: after updateMoney name", id); if (this.updateMoney(id)) { return true; } }
return false; }
|
测试case
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
|
private void testSerializeIsolation() throws InterruptedException { int id = 350; new Thread(new Runnable() { @Override public void run() { call("Serialize: 只读事务 - read", id, detailDemo::readSerializeTransaction); } }).start();
Thread.sleep(1000);
call("Serialize 读写事务 - read", id, detailDemo::serializeTransaction); }
|
输出结果如下
1 2 3 4 5 6 7 8
| ============ Serialize: 只读事务 - read start ========== serialize read only >>>> {id=350, name=初始化, money=200, is_deleted=false, create_at=2020-01-20 12:10:23.0, update_at=2020-01-20 12:10:23.0} ============ Serialize 读写事务 - read start ========== serialize read only >>>> {id=350, name=初始化, money=200, is_deleted=false, create_at=2020-01-20 12:10:23.0, update_at=2020-01-20 12:10:23.0} ============ Serialize: 只读事务 - read end ==========
serialize: after updateMoney name >>>> {id=350, name=更新, money=200, is_deleted=false, create_at=2020-01-20 12:10:23.0, update_at=2020-01-20 12:10:39.0} ============ Serialize 读写事务 - read end ==========
|
只读事务的查询输出之后,才输出读写事务的日志,简单来讲就是读写事务中的操作被delay了
6. 小结
本文主要介绍了事务的几种隔离级别,已经不同干的隔离级别对应的场景,可能出现的问题;
隔离级别说明
级别 |
fix |
not fix |
RU |
更新丢失 |
脏读,不可重复读,幻读 |
RC |
更新丢失 脏读 |
不可重复读,幻读 |
RR |
更新丢、脏读,不可重复读,幻读 |
- |
serialze |
更新丢失、 脏读,不可重复读,幻读 |
- |
使用说明
- mysql innodb引擎默认为RR隔离级别;
@Transactinoal
注解使用数据库的隔离级别,即RR
- 通过指定
Transactional#isolation
来设置事务的事务级别
IV. 其他
0. 系列博文&源码
系列博文
源码
1. 一灰灰Blog
尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激
下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛
一灰灰blog
Be the first person to leave a comment!