MySQL事务隔离
目录
事务隔离级别
读未提交(Read Uncommitted)
演示
读提交(Read Committed)
演示
可重复读(Repeatable Read)
先读取,再插入
先插入,在读取
串行化(Serializable)
隔离性总结
一致性
数据库并发场景
MVCC
3个记录隐藏列字段
undo 日志
模拟MVCC
ReadView
低水位m_up_limit_id是什么?
高水位m_low_limit_id是什么?
m_creator_trx_id正在活跃事务id列表
如何判断是否是当前隔离版本可读?
RR与RC隔离等级的区别
本质区别
事务隔离级别
当前会话的事务隔离级别,影响的只是当前会话,也就是说其他会话对数据进行修改,当前会话是否看的到。
MySQL事务隔离级别可分为四种:
1.读未读交(Read Uncommitted)
2.读提交(Read Committed)
3.可重复读(Repeatable Read)
4.串行化(Serializable)
读未提交(Read Uncommitted)
我们在上一篇文章演示事务的时候,我们可以发现,在事务提交之前,其他客户端是无法发现我们做出的操作的。因为MySQL的默认事务隔离级别为可重复读。
但是处于读未提交状态下,例如说其他会话开启了事务,并将对应的事务隔离级别设置为读未提交,此时我们对数据进行修改,其他会话照样可以看到,没有任何的隔离性。
这会带来很多问题,如脏读,幻读,不可重复读等。
演示
查看全局隔离级别,也就是客户端访问MySQL时默认的隔离级别为可重复读。
当前会话的隔离级别为可重复读。
由此我们可以发现,一个会话登录MySQL时,会将自身的隔离级别设置为MySQL的全局默认隔离级别。
我们如果向仅仅对当前会话的隔离级别进行修改,那么直接修改对应的会话隔离级别即可。
set {global/session} transaction isolation
{read uncommitted/read committed/repeatable read/serializable}
可以看到,我们将右侧的会话设置隔离级别为读未提交,然后开启事务,左侧会话插入数据后,右侧数据仍然可以看到。
也就是说,会话事务隔离级别为读未提交的话,没有任何隔离性。
这种一个事务在执行过程中,可以读到其他事务更新但未提交的数据,这种现象叫做脏读(dirty read)
读提交(Read Committed)
这个隔离级别,也就是说,如果其他会话的事务被提交了,那么在当前事务内才可以看到对应的数据修改。
也就是与读未提交相比,在其他事务没有提交的情况下,就无法看到对应的数据修改。
演示
1.左侧开启事务,右侧查看当前数据,为空。
2.左侧插入数据,并查看当前数据,插入成功。右侧查看数据,读不到,还是为空。
3.左侧提交数据。右侧查看,读到了数据。
但是这样我们也可以发现问题。
一份数据在多次查看时,读取到的结果不一样,这种情况叫做不可重复读(non reapeatable read
)。
可重复读(Repeatable Read)
上面我们提到,一份数据在多次读取时,读到的结果不一样,这叫做不可重复读。
那么可重复读,是不是就是一份数据在多次读取时,读到的结果都一样?
我们在这里进行两次测试,并进行比较,后面会通过原理对这两种情况进行分析。
先读取,再插入
右侧当前隔离级别为可重复读。
1.右侧开启事务之后就进行读取,发现只有一个数据。
2.我们左侧插入一份数据,右侧再次查,发现并没有查到。
这样我们就可以发现,右侧读取到的数据不会因为其他事务对数据的修改而发生变化。
但是现在我们没办法确认的是,右侧事务读取到的数据,究竟是什么时候确定的,因此,我们后面再次进行测试。
先插入,在读取
我们这里做的操作为:
1.右侧开启事务。
2.左侧插入数据,进行查看,发现有3个数据。
3.右侧进行查看,发现也是3个数据。
这说明,我们在可重复读的隔离级别下,我们读取到的数据在是在第一次进行读取时确认的,而不是在开启事务的那一刻确认的。这是通过一种叫做MVCC(多版本并发控制)的技术实现的,我们在后面会进行详细介绍。
串行化(Serializable)
串行化其实没有什么必要演示,因为隔离级别越高,虽然安全性越好,但这是拿效率换的。
串行化作为最高的隔离级别,是将事务中所有除了读取的操作(更新、删除、插入)进行阻塞。
只有当当前正在执行事务执行完毕,也就是其他事务commit提交之后,当前事务才会被执行提交。这样做严格保证了事务执行的顺序,不再是以指令为单位进行执行,而是直接以整个事务为顺序进行执行了,效率非常低,一般都是以可重复读作为隔离级别。
隔离性总结
上面这种表就描述了各种隔离级别会出现的问题
其中,这里出现的幻读,实际上本来应该是可重复读的隔离状态时会出现的问题。
一般的数据库都没有解决这个问题
这个问题实际上就是在进行insert操作时,就算隔离级别是可重复读,insert插入的数据也会被读取出来,这个问题叫做幻读,但是这很明显不符合可重复读,只不过MySQL已经解决了这个问题,采用Next-Key(GAP+行锁)来解决了这个问题。
隔离级别越高,安全性也越高,并发效率也就越低,MySQL的默认隔离级别为可重复读,一般情况下不用修改。
一致性
事务的执行结果,要求MySQL从一个一致性状态转为另一个一致性状态,一致性与原子性强相关。
如果系统运行发生中断,某个事务尚未完成而被迫中断,而改未完成的事务对数据库所做的修改已被写入数据库,此时数据库就处于一种不正确(不一致)的状态。
MySQL本身可以保证每条sql语句的原子性,但是一致性还需要用户业务逻辑做支撑。
因为MySQL只保证每条sql语句,但是业务实际执行时可能会使用多条sql语句完成一个功能,因此,一致性由用户决定。
数据库并发场景
读-读:不存在任何问题,不需要并发控制
读-写:有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读,幻读,不可重复读
写-写:有线程安全问题,可能会存在更新丢失问题
MVCC
多版本并发控制(MVCC)是一种用来解决读-写冲突的无锁并发控制
我们上文也提到,解决可重复读隔离性,靠的就是MVCC。
为事务分配单向增长的事务ID,为每个修改保存一个版本,版本与事务ID关联,读操作只读该事务
开始前的数据库的快照。
因此,MVCC可以解决以下几个问题
1.在并发读写数据库时,可以做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了数据库并发读写的性能
2.同时还可以解决脏读,幻读,不可重复读等事务隔离问题,但不能解决更新丢失问题
3个记录隐藏列字段
1.DB_TRX_ID :6 byte,最近修改( 修改/插入 )事务ID,记录创建这条记录/最后一次修改该记录的事务ID
2.DB_ROLL_PTR : 7 byte,回滚指针,指向这条记录的上一个版本(简单理解成,指向历史版本就行,这些数据一般在 undo log 中)
3.DB_ROW_ID : 6 byte,隐含的自增ID(隐藏主键),如果数据表没有主键, InnoDB 会自动以DB_ROW_ID 产生一个聚簇索引
4.补充:实际还有一个删除flag隐藏字段, 既记录被更新或删除并不代表真的删除,而是删除flag变了
我们先通过以上两条语句,创建一张表,并插入第一条数据。
那么对于这第一条数据,我们这上面的三个隐藏参数就是:
DB_TRX_ID:null(实际上这个不应该为null,只是我们不知道是哪一个事务id)
DB_ROLL_PTR:null(第一次创建,没有上一个版本,因此指针为空)
DB_ROW_ID:1(我们不知道隐式主键是多少,只知道不为空并且和普通主键一样)
undo 日志
我们只需要知道,MySQL是以进程的方式运行,所有操作都是在内存中完成,并且会在合适的实
际刷新到磁盘当中。我们可以就简单将undo log理解为MySQL的一段内存缓冲区,用来保存内存数据即可。
模拟MVCC
直接对上面这张图进行解释即可。当我们在事务中对数据进行修改,我们会对该行数据首先进行一个保存,保存到undo log中进行拷贝,不过这些操作都是加锁执行的,因为可能会由多个事务对同一个份数据进行修改,所以会加上行锁防止冲突。
因此,我们在事务执行过程中,这三个隐藏参数变化如下
1.事务过程中,进行修改时,将原数据拷贝到undo log中进行保存。拷贝完成后,我们新修改的数据就会将回滚指针DB_ROLL_PTR设置为我们拷贝到undo log中的原数据的地址,也就是可以在数据回滚时找到我们当前数据的上一个版本。
2.当前事务完全结束时,设置DB_TRX_ID为对应的事务id。
3.隐式主键保持不变
这样,我们就可以形成一个基于链表的历史版本链,称为一个个快照。
但是别忘了我们数据可能还会有删除操作,但是没关系,删除操作实际上就是修改了删除的flag隐藏字段而已。毕竟如果真删了,数据就找不到了,回退也没法操作了,因此,删除也是可以作为一个版本的。如果当事务完全结束时,该删除字段flag还是表示该数据是被删除的,那么才会真的执行删除操作。
ReadView
现在回到我们我们的隔离级别,我们如何保证不同的事务可以看到不同的内容,也就是如何实现不同的隔离级别?
Read View就是事务进行快照读操作的时候生产的读视图 (Read View)
在事务执行快照读时,会生成一个当前数据库系统的快照,记录当前数据库正在活跃的事务id(前面的事务id派上用场了),(当每个事务开启时,都会被分配一个ID, 这个ID是递增的,所以最新的事务,ID值越大)
class ReadView {
// 省略...
private:
/** 高水位,大于等于这个ID的事务均不可见*/
trx_id_t m_low_limit_id;
/** 低水位:小于这个ID的事务均可见 */
trx_id_t m_up_limit_id;
/** 创建该 Read View 的事务ID*/
trx_id_t m_creator_trx_id;
/** 创建视图时的活跃事务id列表*/
ids_t m_ids;
/** 配合purge,标识该视图不需要小于m_low_limit_no的UNDO LOG,
* 如果其他视图也不需要,则可以删除小于m_low_limit_no的UNDO LOG*/
trx_id_t m_low_limit_no;
/** 标记视图是否被关闭*/
bool m_closed;
// 省略...
};
然后,我们就可以结合我们上面可重复读的隔离级别进行分析了。
低水位m_up_limit_id是什么?
我们知道,低于这个id的事务均可见,那么对于可重复读,什么时候是均可见的?也就是在执行读取操作的那一刻,所有已经结束的事务id均可见。
而m_ids里面存储的是记录下来的那一时刻所有当前正在活跃的事务id,也就是说,比那一时刻所有当前正在活跃的事务中最小的id还要小的话,说明那些事务已经结束了。
因此,低水位m_up_limit_id实际上就是正在活跃的事务id的最小值-1。
即:min(m_ids)-1。
高水位m_low_limit_id是什么?
比记录那一刻最新的事务id还要大的话均不可见。
注意,这里不是正在活跃的事务id中的最大,而是尚未创建的最大事务id。
举个例子:
每一时刻都有很多的事务被提交与创建。在当前事务select之前,可能会由很多事务已经被提交了,但是在这些被提交的事务前面仍然有许多事物未被提交,也就是所谓的正在活跃事务。难道那些已经提交的比最大活跃事务id还要大的事务我们不看了吗?我们还是看得到的。
因此,这里的高水位m_low_limit_id实际上就是记录快照那一刻的最新未被创建的最大事务id。
m_creator_trx_id正在活跃事务id列表
我们在记录快照的时候,那一刻的正在活跃的所有事务。
有可能比当前事务id大,也有可能小。
如何判断是否是当前隔离版本可读?
我们这里以可重复读的隔离等级为例:
实际上,通过上面的分析我们已经知道个大概了
对于可重复读隔离级别来说,我们可以看到的事务id为:
1.首先要在高水位与低水位之间
2.然后不能在读视图的活跃事务列表中(说明已经提交)
我们读取到的每一条数据,都会从根据DB_TRX_ID这个创建/修改当前数据的事务id开始,如果当前数据不符合我们的看到事务id要求,就会通过回滚指针DB_ROLL_PTR顺着版本链向上找,直到找到对应数据可以看到的最新版本。
这就是可重复读隔离等级做的事。
可重复读隔离等级的ReadView是在第一次执行快照读时时创建的。
每一次看到的数据使用的读视图都是一开始创建的那一份,因此第一次查找过后,之后查到的数据都是一样的
RR与RC隔离等级的区别
现在我们知道了我们看到的数据是怎么找出来的,就是数据的版本事务id以及以及版本链查找实现的。
那么RC(Read Committed读提交)是怎么做的?
RC做的是一旦某事务被提交了,那么当前会话事务就可以搜索到。
看着是不是有点眼熟?
是不是和可重复读刚创建读视图时能看到的一样?
那么我们是不是只要每一次进行执行快照都都创建一次读视图就行了?
本质区别
RR:在第一次执行快照读操作时,会生成一个读视图。之后每一次的快照读操作读到的都是同一个读视图。
RC:每一次执行快照读操作,都会重新生成一个读视图。这样在每一次通过读视图看到的都是已经提交了的事务。
那么以上,就是我们关于MySQL事务机制的所有内容,包括事务的操作,原理,以及隔离与MVCC