Spring事务的隔离级别与传播级别

事务的4个隔离级别与7个传播级别

 @Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.REPEATABLE_READ)


查看spring Isolation的源码,有4个隔离级别:

public enum Isolation {

	READ_UNCOMMITTED(TransactionDefinition.ISOLATION_READ_UNCOMMITTED),

	
	READ_COMMITTED(TransactionDefinition.ISOLATION_READ_COMMITTED),

	
	REPEATABLE_READ(TransactionDefinition.ISOLATION_REPEATABLE_READ),

	
	SERIALIZABLE(TransactionDefinition.ISOLATION_SERIALIZABLE),

	DEFAULT(TransactionDefinition.ISOLATION_DEFAULT);


	private final int value;


	Isolation(int value) { this.value = value; }

	public int value() { return this.value; }

}
READ_UNCOMMITTED,READ_COMMITTED,REPEATABLE_READ,SERIALIZABLE,分别对应的问题是

脏读问题,重复读有问题(数据被更新了),重复读数据量count不一致(幻读),没问题

在我们日常开发中事务是很常用的,今天我们一起来了解下MySQL的事务及实现原理。
一. 什么是事务?

事务在计算机术语中指访问或更新数据库中各数据项的一个程序执行单元,事务里面可能只有一个操作,也可能有多个操作。而事务的作用就是要保证事务内的所有操作要么全部成功,要么全部失败。

举个关于事务最经典的例子-转账:假设阿东现在有200元,张三最近手头有点紧,阿东就给张三转100元支援一下;那么,这个转账你认为会有哪些操作?
    第一步:查询阿东账户余额是否大于等于100,如果小于就直接返回余额不足,大于就继续后面的操作;

    第二步:将张三的账户余额+100;

    第三步:将阿东的账户余额-100;

    第三步:完成转账。

如果没有事务,上面的操作会有什么问题?

    假如操作2执行成功,操作3因为数据库或者网络原因导致操作失败了,阿东的账户还是100元而张三的账户也有100元,这么搞银行就亏死了。所以我们需要在操作2和操作3外面加一层事务,让操作2和操作3要么都成功,要么都失败。

二. 事务四大特性

事务的四大特性:原子性、一致性、隔离性、持久性。

    原子性(atomicity):组成一个事务的多个数据库操作是不可分割的工作单元。只有所有操作都成功才会提交事务,否则会撤销所有操作,让数据库恢复事务执行前的状态。

    一致性(consistency):不管事务是否操作成功,数据库所处的状态和业务规则是一致的。(比如上面提到的转账,阿东给张三转账100,不管操作是否成功,总额不会变)。

    隔离性(isolation):一个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。

    持久性(durability):指事务一旦提交,对数据库中数据的改变就是永久性的。

MySQL通过undo log来实现事务失败的回滚,保证了事务的原子性和一致性。而写操作在MySQL中是先写到缓存中,并且写入到redo log保证内存中还没刷盘的脏页不会丢失,最终都会根据一些场景刷盘持久化到磁盘保证了事务的持久性(redo log的知识可以看下我之前的文章《MySQL日志之redo log与binlog》)。

三. 事务的隔离级别

当数据库中有多个事务同时执行且事务内的操作资源相同或相关时,就可能出现脏读,不可重复读,幻读这些问题,为了解决这些问题,就有了“隔离级别”。

    我们知道,隔离级别肯定是通过一些手段去实现的,隔离级别越高,性能肯定越差,所以很多时候都要有一个选择。SQL的标准隔离级别由低到高是以下四种:读未提交(read uncommitted)、读已提交(read  committed)、可重复读(repeatable read)和串行化(serializable)。

怎么理解这四个隔离级别呢?

    读未提交:一个事务还没提交,它的变更就可以被其他事务看到。会出现脏读、不可重复读、幻读的问题。

    读已提交:一个事务提交后,它的变更才可以被其他事务看到。解决了脏读,但是有不可重复读和幻觉读的问题。

    可重复读:一个事务整个执行过程中看到的数据,总是跟在这个事务中在第一次看到的数据是一致的。只有幻读的问题。

    串行化:所有对记录的操作都会加锁,读加读锁,写加写锁。除了并发读不会串行,读写和写写都会有冲突互斥;对同一条记录的操作,后访问的事务会等前一个事务提交释放记录锁后才能操作,否则会阻塞。

在这里给大家解释下脏读就是读的是事务还未提交的数据,不可重复读是同一个事务内多次读的数据内容不一致(更新导致),幻读是同一个事务内多次读到的数据数量不一致(增删导致)。

了解了事务隔离级别的概念,我们来看下MySQL是怎么实现这些隔离级别的(MySQL只有InnoDB引擎的支持事务,所以我们下面的实现都是在InnoDB引擎的前提下)。

    在读未提交中,读操作都不会去加锁,读的数据都是最新的。写操作会写锁,但是写完就会立刻释放,不等事务提交;串行化我们刚才讲了是通过将所有的读写操作加锁来实现的。下面我们重点来讲下读未提交和可重复读的隔离级别在MySQL中是怎么实现的。

    MySQL读未提交和可重复读的隔离级别下的写操作也是会加锁的,同时会去记录一条更新前的数据在undo log里来保证失败事务回滚,然后直到事务提交才会释放锁。

那读也是加锁吗?

    其实目前我们很多应用都是读多写少的场景,读操作要远远大于写操作,所以读操作之间的互斥锁就没必要了,所以就有了读写锁,读锁和读锁之间不会阻塞,但是读锁和写锁之间会互斥;如果在一个事务更新一批数据的同时,别的所有查询该批数据的事务都阻塞不可用,肯定会对业务影响比较大;那有没有什么办法让读和写之间不互斥,再提升一下性能呢?

可以通过MVCC来解决读锁和写锁之间的互斥。

    MVCC(Multi-Version Concurrency Control)多版本并发控制,MVCC的原理简单来说就是通过一种类似快照的形式将数据保存下来,不同的事务通过一些规则限定只能看到特定的快照版本,来实现并发读写(快照一种抽象的概念,可以简单理解为不同版本的数据,这就对应着MVCC的MV-多版本)。

那在MySQL中是怎么实现MVCC的呢?

MySQL是通过undo log的版本链和创建一致性视图来实现MVCC的。

undo log

    MySQL的增删改操作(rc和rr隔离级别下)同时会记录undo log,它记录了反向操作,比如删除就记录新增,新增就记录删除,更新就记录更新前的数据。在InnoDB里,UPDATE和DELETE操作产生的Undo日志被归成一类,即update_undo在回滚段中的undo logs分为: insert undo log 和 update undo log。

    insert undo log : 事务对insert新记录时产生的undo log,只在事务回滚时需要, 并且在事务提交后就可以立即丢弃。

    update undo log : 事务对记录进行delete和update操作时产生的undo log,事务回滚和一致性读(快照读创建的一致性视图)也需要,所以不能随便删除,只有当数据库所使用的快照中不涉及该日志记录,对应的回滚日志才会被purge线程删除(undo log的记录不会立即删除,而是标记一下deleted_bit,purge线程去定时清理标记了deleted_bit的记录,类似Java垃圾回收中的标记清除算法)。

所以我们所说的版本链只存在更新和删除操作。

InnoDB存储引擎在数据库每行数据的后面隐式添加了三个字段:

    DB_TRX_ID:事务id,用来记录最近一次对本行记录做修改的事务id。

    DB_ROW_ID:这个字段就是主键id,如果建表没有设置主键,InnoDB会默认生成一个DB_ROW_ID来隐式作为表的主键。

    DB_ROLL_PTR:回滚指针,指向undo log中的回滚记录。

所以数据表中的记录会有多个版本,这个多版本就是存在undo log中,组成版本链;

每个记录形成的版本链的结构大概是这样:

(借用思否平台上@大佬的图)

那么创建一致性视图会做哪些事情呢?

1. trx_ids(活跃事务id集合):将当前所有未提交的事务id(DB_TRX_ID)存进视图的单独维护的活跃事务id数组trx_ids里;

2. up_limit_id(低水位):记录下trx_ids(活跃事务id集合)最小的值为up_limit_id;

3. low_limit_id(高水位):当前系统里已经创建过的事务id的最大值+1(也就是下一个生成的事务id)。

那么在什么时机会触发创建一致性视图呢?

在不同的隔离级别下,创建一致性视图的时机不同,但切记创建一致性视图的时机并不是在事务开启时。

    在读已提交(read  committed)隔离级别下,事务内的每次快照读(快照读的概念后面再讲,这里可以先理解为简单的select查询)都会触发创建一致性视图,从而防止幻读,但是正因为每次快照读都会创建新的视图,所以不可重复读的问题还是会发生。

    在可重复读(repeatable read)隔离级别下,事务内的第一次快照读且只有一次时,会触发创建一致性视图。因为在整个事务内只创建一次视图,整个事务内多次读都会在这个唯一的视图上进行查找,所以能防止不可重复读的发生。

什么是快照读和当前读?

    快照读(snapshot read):普通的 select 语句(不包括 select ... lock in share mode, select ... for update)

    当前读(current read) :select ... lock in share mode,select ... for update,insert,update,delete 语句(这些语句获取的是数据库中的最新数据,和写操作互斥)

下面就讲一下一致性视图是怎么保证事务隔离的:

select * from t where id = 1;

当执行这个语句时会创建一致性视图,包括生成好trx_ids(活跃事务id集合),up_limit_id(低水位),low_limit_id(高水位)。假设现在id = 1的这条记录内的DB_TRX_ID是trx_current:

创建完视图后,首先会去将trx_current与up_limit_id(低水位)和low_limit_id(高水位)进行比较,分为三种情况:

1. trx_current < up_limit_id:当前记录的事务id小于低水位,意味着最新修改该行的事务已经提交或者是当前事务自己生成的,数据可见,直接返回。

2. trx_current >= low_limit_id:当前记录的事务id大于等于高水位,意味最新修改该行的事务在“当前事务”创建视图之后才修改该行,数据不可见,根据该记录行的 DB_ROLL_PTR 指针所指向的undo log回滚段中,取出最新的的旧事务号DB_TRX_ID, 将它赋给trx_current,然后重新进行水位的比较。

3. up_limit_id <= trx_current< low_limit_id:当前记录的事务id大于等于低水位且小于高水位,这个时候就有两种情况:

    a.  第一种情况:trx_current在活跃事务集合trx_ids中:最新修改该行的事务还未提交,数据不可见,和上述2一样,去版本链中找上一个版本的DB_TRX_ID去继续判断操作。

    b.  第二种情况:trx_current不在活跃事务集合trx_ids中:最新修改该行的事务已经提交,数据可见,直接返回数据。

上述的逻辑就可以实现不同事务更新时数据的隔离性,同时又能保证读和写的并发。

    我们在上面说了RR隔离级别可以防止脏读和可重复读,防止不了幻读,确实,在只有MVCC的情况下确实防止不了幻读,但是在RR隔离级别下InnoDB引擎通过next-key lock(record lock和gap lock)来防止幻读的产生,就是在当前读时,会锁住匹配到的记录和记录左开右闭的间隙(间隙锁-gap lock)来防止幻读(锁这块知识后面的文章会详细讲),所以RR隔离级别下MySQL是可以根据MVCC+next-key lock来解决脏读、不可重复读及幻读等一系列问题的。

并且MySQL的默认隔离级别是可重复读(RR):

show variables like "transaction_isolation";

但是一般线上不会去用可重复读(RR)隔离级别,为什么呢?

上面我们也讲了,RR隔离级别会加更多的锁,首先加锁会带来额外的开销,其次性能下降,最后并且很容易发生死锁的情况!所以一般线上业务都会去设置读已提交隔离级别(RC)。


事务传播级别

1) PROPAGATION_REQUIRED ,默认的spring事务传播级别,使用该级别的特点是,如果上下文中已经存在事务,那么就加入到事务中执行,如果当前上下文中不存在事务,则新建事务执行。所以这个级别通常能满足处理大多数的业务场景。

2)PROPAGATION_SUPPORTS ,从字面意思就知道,supports,支持,该传播级别的特点是,如果上下文存在事务,则支持事务加入事务,如果没有事务,则使用非事务的方式执行。所以说,并非所有的包在transactionTemplate.execute中的代码都会有事务支持。这个通常是用来处理那些并非原子性的非核心业务逻辑操作。应用场景较少。    

3)PROPAGATION_MANDATORY , 该级别的事务要求上下文中必须要存在事务,否则就会抛出异常!配置该方式的传播级别是有效的控制上下文调用代码遗漏添加事务控制的保证手段。比如一段代码不能单独被调用执行,但是一旦被调用,就必须有事务包含的情况,就可以使用这个传播级别。

4)PROPAGATION_REQUIRES_NEW ,从字面即可知道,new,每次都要一个新事务,该传播级别的特点是,每次都会新建一个事务,并且同时将上下文中的事务挂起,执行当前新建事务完成以后,上下文事务恢复再执行。

5)PROPAGATION_NOT_SUPPORTED ,这个也可以从字面得知,not supported ,不支持,当前级别的特点就是上下文中存在事务,则挂起事务,执行当前逻辑,结束后恢复上下文的事务。

6)PROPAGATION_NEVER ,该事务更严格,上面一个事务传播级别只是不支持而已,有事务就挂起,而PROPAGATION_NEVER传播级别要求上下文中不能存在事务,一旦有事务,就抛出runtime异常,强制停止执行!这个级别上辈子跟事务有仇。

7)PROPAGATION_NESTED ,字面也可知道,nested,嵌套级别事务。该传播级别特征是,如果上下文中存在事务,则嵌套事务执行,如果不存在事务,则新建事务

文/程忠 浏览次数:0次   2022-12-08 07:58:13

相关阅读


评论:
点击刷新

↓ 广告开始-头部带绿为生活 ↓
↑ 广告结束-尾部支持多点击 ↑