SpringBoot 系列教程之事务隔离级别知识点小结

上一篇博文介绍了声明式事务@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. <dependency>
  2. <groupId>mysql</groupId>
  3. <artifactId>mysql-connector-java</artifactId>
  4. </dependency>
  5. <dependency>
  6. <groupId>org.springframework.boot</groupId>
  7. <artifactId>spring-boot-starter-jdbc</artifactId>
  8. </dependency>

2. 数据库配置

进入 spring 配置文件application.properties,设置一下 db 相关的信息

  1. ## DataSource
  2. spring.datasource.url=jdbc:mysql://127.0.0.1:3306/story?useUnicode=true&characterEncoding=UTF-8&useSSL=false
  3. spring.datasource.username=root
  4. spring.datasource.password=

3. 数据库

新建一个简单的表结构,用于测试

  1. CREATE TABLE `money` (
  2. `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  3. `name` varchar(20) NOT NULL DEFAULT '' COMMENT '用户名',
  4. `money` int(26) NOT NULL DEFAULT '0' COMMENT '钱',
  5. `is_deleted` tinyint(1) NOT NULL DEFAULT '0',
  6. `create_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  7. `update_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  8. PRIMARY KEY (`id`),
  9. KEY `name` (`name`)
  10. ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;

III. 实例演示

1. 初始化数据

准备一些用于后续操作的数据

  1. @Component
  2. public class DetailDemo {
  3. @Autowired
  4. private JdbcTemplate jdbcTemplate;
  5. @PostConstruct
  6. public void init() {
  7. String sql = "replace into money (id, name, money) values (320, '初始化', 200)," + "(330, '初始化', 200)," +
  8. "(340, '初始化', 200)," + "(350, '初始化', 200)";
  9. jdbcTemplate.execute(sql);
  10. }
  11. }

提供一些基本的查询和修改方法

  1. private boolean updateName(int id) {
  2. String sql = "update money set `name`='更新' where id=" + id;
  3. jdbcTemplate.execute(sql);
  4. return true;
  5. }
  6. public void query(String tag, int id) {
  7. String sql = "select * from money where id=" + id;
  8. Map map = jdbcTemplate.queryForMap(sql);
  9. System.out.println(tag + " >>>> " + map);
  10. }
  11. private boolean updateMoney(int id) {
  12. String sql = "update money set `money`= `money` + 10 where id=" + id;
  13. jdbcTemplate.execute(sql);
  14. return false;
  15. }

2. RU 隔离级别

我们先来测试 RU 隔离级别,通过指定@Transactional注解的isolation属性来设置事务的隔离级别

通过前面的描述,我们知道 RU 会有脏读问题,接下来设计一个 case,进行演示

事务一,修改数据

  1. /**
  2. * ru隔离级别的事务,可能出现脏读,不可避免不可重复读,幻读
  3. *
  4. * @param id
  5. */
  6. @Transactional(isolation = Isolation.READ_UNCOMMITTED, rollbackFor = Exception.class)
  7. public boolean ruTransaction(int id) throws InterruptedException {
  8. if (this.updateName(id)) {
  9. this.query("ru: after updateMoney name", id);
  10. Thread.sleep(2000);
  11. if (this.updateMoney(id)) {
  12. return true;
  13. }
  14. }
  15. this.query("ru: after updateMoney money", id);
  16. return false;
  17. }

只读事务二(设置 readOnly 为 true,则事务为只读)多次读取相同的数据,我们希望在事务二的第一次读取中,能获取到事务一的中间修改结果(所以请注意两个方法中的 sleep 使用)

  1. @Transactional(readOnly = true, isolation = Isolation.READ_UNCOMMITTED, rollbackFor = Exception.class)
  2. public boolean readRuTransaction(int id) throws InterruptedException {
  3. this.query("ru read only", id);
  4. Thread.sleep(1000);
  5. this.query("ru read only", id);
  6. return true;
  7. }

接下来属于测试的 case,用两个线程来调用只读事务,和读写事务

  1. @Component
  2. public class DetailTransactionalSample {
  3. @Autowired
  4. private DetailDemo detailDemo;
  5. /**
  6. * ru 隔离级别
  7. */
  8. public void testRuIsolation() throws InterruptedException {
  9. int id = 330;
  10. new Thread(new Runnable() {
  11. @Override
  12. public void run() {
  13. call("ru: 只读事务 - read", id, detailDemo::readRuTransaction);
  14. }
  15. }).start();
  16. call("ru 读写事务", id, detailDemo::ruTransaction);
  17. }
  18. }
  19. private void call(String tag, int id, CallFunc<Integer, Boolean> func) {
  20. System.out.println("============ " + tag + " start ========== ");
  21. try {
  22. func.apply(id);
  23. } catch (Exception e) {
  24. }
  25. System.out.println("============ " + tag + " end ========== \n");
  26. }
  27. @FunctionalInterface
  28. public interface CallFunc<T, R> {
  29. R apply(T t) throws Exception;
  30. }

输出结果如下

  1. ============ ru 读写事务 start ==========
  2. ============ ru: 只读事务 - read start ==========
  3. 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}
  4. 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}
  5. 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}
  6. ============ ru: 只读事务 - read end ==========
  7. 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}
  8. ============ ru 读写事务 end ==========

关注一下上面结果中ru read only >>>>开头的记录,首先两次输出结果不一致,所以不可重复读问题是存在的

其次,第二次读取的数据与读写事务中的中间结果一致,即读取到了未提交的结果,即为脏读

3. RC 事务隔离级别

rc 隔离级别,可以解决脏读,但是不可重复读问题无法避免,所以我们需要设计一个 case,看一下是否可以读取另外一个事务提交后的结果

在前面的测试 case 上,稍微改一改

  1. // ---------- rc 事物隔离级别
  2. // 测试不可重复读,一个事务内,两次读取的结果不一样
  3. @Transactional(readOnly = true, isolation = Isolation.READ_COMMITTED, rollbackFor = Exception.class)
  4. public boolean readRcTransaction(int id) throws InterruptedException {
  5. this.query("rc read only", id);
  6. Thread.sleep(1000);
  7. this.query("rc read only", id);
  8. Thread.sleep(3000);
  9. this.query("rc read only", id);
  10. return true;
  11. }
  12. /**
  13. * rc隔离级别事务,未提交的写事务,会挂起其他的读写事务;可避免脏读,更新丢失;但不能防止不可重复读、幻读
  14. *
  15. * @param id
  16. * @return
  17. */
  18. @Transactional(isolation = Isolation.READ_COMMITTED, rollbackFor = Exception.class)
  19. public boolean rcTranaction(int id) throws InterruptedException {
  20. if (this.updateName(id)) {
  21. this.query("rc: after updateMoney name", id);
  22. Thread.sleep(2000);
  23. if (this.updateMoney(id)) {
  24. return true;
  25. }
  26. }
  27. return false;
  28. }

测试用例

  1. /**
  2. * rc 隔离级别
  3. */
  4. private void testRcIsolation() throws InterruptedException {
  5. int id = 340;
  6. new Thread(new Runnable() {
  7. @Override
  8. public void run() {
  9. call("rc: 只读事务 - read", id, detailDemo::readRcTransaction);
  10. }
  11. }).start();
  12. Thread.sleep(1000);
  13. call("rc 读写事务 - read", id, detailDemo::rcTranaction);
  14. }

输出结果如下

  1. ============ rc: 只读事务 - read start ==========
  2. 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}
  3. ============ rc 读写事务 - read start ==========
  4. 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}
  5. 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}
  6. ============ rc 读写事务 - read end ==========
  7. 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}
  8. ============ rc: 只读事务 - read end ==========

从上面的输出中,在只读事务,前面两次查询,结果一致,虽然第二次查询时,读写事务修改了这个记录,但是并没有读取到这个中间记录状态,所以这里没有脏读问题;

当读写事务完毕之后,只读事务的第三次查询中,返回的是读写事务提交之后的结果,导致了不可重复读

4. RR 事务隔离级别

针对 rr,我们主要测试一下不可重复读的解决情况,设计 case 相对简单

  1. /**
  2. * 只读事务,主要目的是为了隔离其他事务的修改,对本次操作的影响;
  3. *
  4. * 比如在某些耗时的涉及多次表的读取操作中,为了保证数据一致性,这个就有用了; 开启只读事务之后,不支持修改数据
  5. */
  6. @Transactional(readOnly = true, isolation = Isolation.REPEATABLE_READ, rollbackFor = Exception.class)
  7. public boolean readRrTransaction(int id) throws InterruptedException {
  8. this.query("rr read only", id);
  9. Thread.sleep(3000);
  10. this.query("rr read only", id);
  11. return true;
  12. }
  13. /**
  14. * rr隔离级别事务,读事务禁止其他的写事务,未提交写事务,会挂起其他读写事务;可避免脏读,不可重复读,(我个人认为,innodb引擎可通过mvvc+gap锁避免幻读)
  15. *
  16. * @param id
  17. * @return
  18. */
  19. @Transactional(isolation = Isolation.REPEATABLE_READ, rollbackFor = Exception.class)
  20. public boolean rrTransaction(int id) {
  21. if (this.updateName(id)) {
  22. this.query("rr: after updateMoney name", id);
  23. if (this.updateMoney(id)) {
  24. return true;
  25. }
  26. }
  27. return false;
  28. }

我们希望读写事务的执行周期在只读事务的两次查询之内,所有测试代码如下

  1. /**
  2. * rr
  3. * 测试只读事务
  4. */
  5. private void testReadOnlyCase() throws InterruptedException {
  6. // 子线程开启只读事务,主线程执行修改
  7. int id = 320;
  8. new Thread(new Runnable() {
  9. @Override
  10. public void run() {
  11. call("rr 只读事务 - read", id, detailDemo::readRrTransaction);
  12. }
  13. }).start();
  14. Thread.sleep(1000);
  15. call("rr 读写事务", id, detailDemo::rrTransaction);
  16. }

输出结果

  1. ============ rr 只读事务 - read start ==========
  2. 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}
  3. ============ rr 读写事务 start ==========
  4. 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}
  5. ============ rr 读写事务 end ==========
  6. 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}
  7. ============ rr 只读事务 - read end ==========

两次只读事务的输出一致,并没有出现上面的不可重复读问题

说明

  • @Transactional注解的默认隔离级别为Isolation#DEFAULT,也就是采用数据源的隔离级别,mysql innodb 引擎默认隔离级别为 RR(所有不额外指定时,相当于 RR)

5. SERIALIZABLE 事务隔离级别

串行事务隔离级别,所有的事务串行执行,实际的业务场景中,我没用过... 也不太能想像,什么场景下需要这种

  1. @Transactional(readOnly = true, isolation = Isolation.SERIALIZABLE, rollbackFor = Exception.class)
  2. public boolean readSerializeTransaction(int id) throws InterruptedException {
  3. this.query("serialize read only", id);
  4. Thread.sleep(3000);
  5. this.query("serialize read only", id);
  6. return true;
  7. }
  8. /**
  9. * serialize,事务串行执行,fix所有问题,但是性能低
  10. *
  11. * @param id
  12. * @return
  13. */
  14. @Transactional(isolation = Isolation.SERIALIZABLE, rollbackFor = Exception.class)
  15. public boolean serializeTransaction(int id) {
  16. if (this.updateName(id)) {
  17. this.query("serialize: after updateMoney name", id);
  18. if (this.updateMoney(id)) {
  19. return true;
  20. }
  21. }
  22. return false;
  23. }

测试 case

  1. /**
  2. * Serialize 隔离级别
  3. */
  4. private void testSerializeIsolation() throws InterruptedException {
  5. int id = 350;
  6. new Thread(new Runnable() {
  7. @Override
  8. public void run() {
  9. call("Serialize: 只读事务 - read", id, detailDemo::readSerializeTransaction);
  10. }
  11. }).start();
  12. Thread.sleep(1000);
  13. call("Serialize 读写事务 - read", id, detailDemo::serializeTransaction);
  14. }

输出结果如下

  1. ============ Serialize: 只读事务 - read start ==========
  2. 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}
  3. ============ Serialize 读写事务 - read start ==========
  4. 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}
  5. ============ Serialize: 只读事务 - read end ==========
  6. 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}
  7. ============ Serialize 读写事务 - read end ==========

只读事务的查询输出之后,才输出读写事务的日志,简单来讲就是读写事务中的操作被 delay 了

6. 小结

本文主要介绍了事务的几种隔离级别,已经不同干的隔离级别对应的场景,可能出现的问题;

隔离级别说明

级别 fix not fix
RU 更新丢失 脏读,不可重复读,幻读
RC 更新丢失 脏读 不可重复读,幻读
RR 更新丢、脏读,不可重复读,幻读 -
serialze 更新丢失、 脏读,不可重复读,幻读 -

使用说明

  • mysql innodb 引擎默认为 RR 隔离级别;@Transactinoal注解使用数据库的隔离级别,即 RR
  • 通过指定Transactional#isolation来设置事务的事务级别

IV. 其他

0. 系列博文&源码

系列博文

源码

1. 一灰灰 Blog

尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现 bug 或者有更好的建议,欢迎批评指正,不吝感激

下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

SpringBoot 系列教程之事务隔离级别知识点小结的更多相关文章

  1. SpringBoot系列教程之事务传递属性

    200202-SpringBoot系列教程之事务传递属性 对于mysql而言,关于事务的主要知识点可能几种在隔离级别上:在Spring体系中,使用事务的时候,还有一个知识点事务的传递属性同样重要,本文 ...

  2. SpringBoot 系列教程之事务不生效的几种 case

    SpringBoot 系列教程之事务不生效的几种 case 前面几篇博文介绍了声明式事务@Transactional的使用姿势,只知道正确的使用姿势可能还不够,还得知道什么场景下不生效,避免采坑.本文 ...

  3. SpringBoot 系列教程之编程式事务使用姿势介绍篇

    SpringBoot 系列教程之编程式事务使用姿势介绍篇 前面介绍的几篇事务的博文,主要是利用@Transactional注解的声明式使用姿势,其好处在于使用简单,侵入性低,可辨识性高(一看就知道使用 ...

  4. 一文讲清楚MySQL事务隔离级别和实现原理,开发人员必备知识点

    经常提到数据库的事务,那你知道数据库还有事务隔离的说法吗,事务隔离还有隔离级别,那什么是事务隔离,隔离级别又是什么呢?本文就帮大家梳理一下. MySQL 事务 本文所说的 MySQL 事务都是指在 I ...

  5. MySQL实战 | 03 - 谁动了我的数据:浅析MySQL的事务隔离级别

    原文链接:这一次,带你搞清楚MySQL的事务隔离级别! 使用过关系型数据库的,应该都事务的概念有所了解,知道事务有 ACID 四个基本属性:原子性(Atomicity).一致性(Consistency ...

  6. 面试必问的MySQL锁与事务隔离级别

    之前多篇文章从mysql的底层结构分析.sql语句的分析器以及sql从优化底层分析, 还有工作中常用的sql优化小知识点.面试各大互联网公司必问的mysql锁和事务隔离级别,这篇文章给你打神助攻,一飞 ...

  7. SQL Server 中的事务与事务隔离级别以及如何理解脏读, 未提交读,不可重复读和幻读产生的过程和原因

    原本打算写有关 SSIS Package 中的事务控制过程的,但是发现很多基本的概念还是需要有 SQL Server 事务和事务的隔离级别做基础铺垫.所以花了点时间,把 SQL Server 数据库中 ...

  8. 30分钟全面解析-SQL事务+隔离级别+阻塞+死锁

    以前总是追求新东西,发现基础才是最重要的,今年主要的目标是精通SQL查询和SQL性能优化.  本系列主要是针对T-SQL的总结. [T-SQL基础]01.单表查询-几道sql查询题 [T-SQL基础] ...

  9. MySQL之事务隔离级别--转载

    转自:http://793404905.blog.51cto.com/6179428/1615550 本文通过实例展示MySQL事务的四种隔离级别. 1 概念阐述 1)Read Uncommitted ...

随机推荐

  1. gcc/g++/make/cmake/makefile/cmakelists的恩恩怨怨

    以前在windows下用VS写代码,不管有多少个文件夹,有多少个文件,写完以后只需要一键就什么都搞定了.但是当移步linux下时,除非你使用图形界面,并且使用Qt creater这类的IDE时,才可以 ...

  2. Autoit里用多进程模拟多线程

      一直以来Autoit都不支持多线程,因此一些需要同时运行多个循环的操作也就无法实现.这个问题在其它的某些语言里也经常出现,解决的方法就是使用多进程. 所谓多进程,就是同时运行多个子进程,每个子进程 ...

  3. 隧道技术(Tunneling)

    隧道技术及其应用 隧道技术(Tunneling)是一种通过使用互联网络的基础设施在网络之间传递数据的方式.使用隧道传递的数据(或负载)可以是不同协议的数据帧或包.隧道协议将其它协议的数据帧或包重新封装 ...

  4. 自定义组装json对象

    组装json对象 public string strTree(DataTable dt, string type, string state) { string strjosn = "&qu ...

  5. python 中的 赋值 浅拷贝 深拷贝

    1.对象的赋值 都是进行对象引用(内存地址)传递,即 b is a ,a 变 b也变 2.浅拷贝 会创建一个新的对象,对于对象中的元素,浅拷贝就只会使用原始元素的引用(内存地址) 当我们使用下面的操作 ...

  6. 吴裕雄--天生自然HADOOP操作实验学习笔记:pig简介

    实验目的 了解pig的该概念和原理 了解pig的思想和用途 了解pig与hadoop的关系 实验原理 1.Pig 相比Java的MapReduce API,Pig为大型数据集的处理提供了更高层次的抽象 ...

  7. ionic3记录之弹窗Alert

    一个业务流程需要多个弹窗: 在上一个弹窗的onDidDissmiss写下一个弹窗:

  8. windows远程桌面不显示本地磁盘

    \\tsclient\D 在资源管理器输入上面的内容就可以访问本地的D盘,但是前提是连接远程桌面的时候设置了可以访问本地D盘.

  9. jquery 操作单选框,复选框,下拉列表实现代码

    1.复选框全选操作:其实说到底就是对Jquery 选择器的运用,点我查看Jquery选择器 html代码: 复制代码代码如下: <form> 您爱好的运动是: <input type ...

  10. iScroll.js的用法

    概要 iScroll 4 这个版本完全重写了iScroll这个框架的原始代码.这个项目的产生完全是因为移动版webkit浏览器(诸如iPhone,iPad,Android 这些系统上广泛使用)提供了一 ...