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

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

上一篇博文介绍了声明式事务@Transactional的简单使用姿式,最文章的最后给出了这个注解的多个属性,本文将着重放在事务隔离级别的知识点上,并经过实例演示不一样的事务隔离级别下,脏读、不可重复读、幻读的具体场景mysql

<!-- more -->git

I. 基础知识

在进入正文以前,先介绍一下事务隔离级别的一些基础知识点,详细内容,推荐参考博文github

mysql 之锁与事务spring

1. 基本概念

如下基本概念源于我的理解以后,经过简单的 case 进行描述,若有问题,欢迎拍砖sql

更新丢失数据库

简单来说,两个事务 A,B 分别更新一条记录的 filedA, filedB 字段,其中事务 B 异常,致使回滚,将这条记录的恢复为修改以前的状态,致使事务 A 的修改丢失了,这就是更新丢失app

脏读ide

读取到另一个事务未提交的修改,因此当另一个事务是失败致使回滚的时候,这个读取的数据实际上是不许确的,这就是脏读spring-boot

不可重复读

简单来说,就是一个事务内,屡次查询同一个数据,返回的结果竟然不同,这就是不可重复度(重复读取的结果不同)

幻读

一样是屡次查询,可是后面查询时,发现多了或者少了一些记录

好比:查询 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) 可重复度,读事务禁止其余写事务;未提交写事务,禁止其余读写事务 更新丢失,脏读,不可重复度 <del>幻读</del>
serializable 序列化读,全部事务依次执行 更新丢失,脏读,不可重复度,幻读 -

说明,下面存为我的观点,不表明权威,谨慎理解和引用

  • 我我的的观点,rr 级别在 mysql 的 innodb 引擎上,配合 mvvc + gap 锁,已经解决了幻读问题
  • 下面这个 case 是幻读问题么?
    • 从锁的角度来看,步骤 一、2 虽然开启事务,可是属于快照读;而 9 属于当前读;他们读取的源不一样,应该不算在幻读定义中的同一查询条件中

II. 配置

接下来进入实例演示环节,首先须要准备环境,建立测试项目

建立一个 SpringBoot 项目,版本为2.2.1.RELEASE,使用 mysql 做为目标数据库,存储引擎选择Innodb,事务隔离级别为 RR

1. 项目配置

在项目pom.xml文件中,加上spring-boot-starter-jdbc,会注入一个DataSourceTransactionManager的 bean,提供了事务支持

<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 相关的信息

## 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. 数据库

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

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. 初始化数据

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

@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);
    }
}

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

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,进行演示

事务一,修改数据

/**
 * ru隔离级别的事务,可能出现脏读,不可避免不可重复读,幻读
 *
 * @param id
 */
@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 使用)

@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,用两个线程来调用只读事务,和读写事务

@Component
public class DetailTransactionalSample {
    @Autowired
    private DetailDemo detailDemo;

     /**
     * ru 隔离级别
     */
    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;
}

输出结果以下

============ 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 上,稍微改一改

// ---------- rc 事物隔离级别
// 测试不可重复读,一个事务内,两次读取的结果不同


@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;
}

/**
 * rc隔离级别事务,未提交的写事务,会挂起其余的读写事务;可避免脏读,更新丢失;但不能防止不可重复读、幻读
 *
 * @param id
 * @return
 */
@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;
}

测试用例

/**
 * rc 隔离级别
 */
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);
}

输出结果以下

============ 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 相对简单

/**
 * 只读事务,主要目的是为了隔离其余事务的修改,对本次操做的影响;
 *
 * 好比在某些耗时的涉及屡次表的读取操做中,为了保证数据一致性,这个就有用了; 开启只读事务以后,不支持修改数据
 */
@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;
}

/**
 * rr隔离级别事务,读事务禁止其余的写事务,未提交写事务,会挂起其余读写事务;可避免脏读,不可重复读,(我我的认为,innodb引擎可经过mvvc+gap锁避免幻读)
 *
 * @param id
 * @return
 */
@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;
}

咱们但愿读写事务的执行周期在只读事务的两次查询以内,全部测试代码以下

/**
 * rr
 * 测试只读事务
 */
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);
}

输出结果

============ 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 事务隔离级别

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

@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;
}

/**
 * serialize,事务串行执行,fix全部问题,可是性能低
 *
 * @param id
 * @return
 */
@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

/**
 * Serialize 隔离级别
 */
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);
}

输出结果以下

============ 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

相关文章
相关标签/搜索