一、事务概述
mysql事务是指一组命令操作,在执行过程中用来保证要么全部成功,要么全部失败。事务是由引擎层面来支持的,MyISM引擎不支持事务,InnoDB引擎支持事务。
事务具有ACID四大特性
原子性(Atomicity):指事务不可分割,要么全部成功,要么全部失败,不可能存在部分成功或部分失败的情况。如果执行某一条语句失败后,将会触发之前所有执行过的语句的回滚,因此靠的是undo log进行回滚。
一致性(Consistency):在事务执行前后,数据的完整性没有遭到破坏。一致性是mysql追求的最终目标,需要数据库层面与应用层面同时来维护。需要先满足原子性、隔离性与持久性,同时也需要应用层面做保障,即在应用层面对数据进行检验。
隔离性(Isolation):事务之前是隔离的,并发执行的事务之间不存在互相影响,mysql通过锁以及MVCC来保证隔离性。
持久性(Durability):事务一旦提交,那么对数据的操作就是永久性的,即使接下来数据库宕机也不会有影响。mysql是通过redo log来实现宕机恢复的,而binlog主要是用来误删恢复与主从复制的。
二、事务并发问题
数据库有读—读操作、读—写操作、写—写操作三种并发场景。三种场景下读写操作和写写操作可能会产生并发安全问题。
读—读操作:多线程同时进行读操作,这种场景下不会产生并发安全问题,不需并发控制。
读—写操作:两个线程在同一时刻分别进行读写操作,这种情况下可能会对数据库产生隔离性问题和出现脏读、幻读、不可重复读的问题。
写—写操作:两个线程同一时刻进行写操作,这种情况下可能会存在数据丢失的问题。
1、脏读
读取到别的事务未提交的数据被称为脏读。
A事务读取到了B事务未提交的数据,若B事务发生错误进行回滚操作,那么事务A读取到的数据就是脏数据。
这种情况常发生在转账场景下,如老板发错工资的情况
时间顺序 | 转账事务 | 查询事务 |
1 | 开始事务 | |
2 | 开始事务 | |
3 | 转账3.9万,未提交 | |
4 | 收入3.9万(涨工资了) | |
5 | 提交事务 | |
6 | 发现转错了,进行事务回滚 | |
7 | 重新转账3.6万 | |
8 | 提交事务 |
分析:实际程序员这个月的工资还是3.6万,但是程序员看到的是3.9万。他看到的是老板还没提交事务时的数据。这就是脏读。
2、幻读(Phantom Reads)
某个事务前后多次读取到的数据总量不一致被称为幻读。
事务A在执行读取操作,需要两次统计数据的总量,前一次查询数据总量后,此时事务B执行了新增数据的操作并提交后,这个时候事务A读取的数据总量和之前统计的不一样,就像产生了幻觉一样,平白无故的多了几条数据,称为幻读。
时间顺序 | 事务A | 事务B |
1 | 开始事务 | |
2 | 第一次查询,数据总量100条 | |
3 | 开始事务 | |
4 | 新增100条数据 | |
5 | 提交事务 | |
6 | 第二次查询 数据总量为200条 | |
7 | 按照正确逻辑,事务A前后两次读取到的数据总量应该一致 |
3、不可重复读
某个事务范围内前后多次相同查询条件读取到的数据内容不一致被称为不可重复读。
事务A前后两次相同条件的查询时间跨度比较大,在其之间事务B对数据进行了修改操作,导致事务A第二次查询获取的数据与第一次不同(数据不重复了),称为不可重复读。
时间顺序 | 事务A | 事务B |
1 | 开始事务 | |
2 | 第一次查询 小红语文成绩90 | |
3 | 开始事务 | |
4 | 更改小红语文成绩为95 | |
5 | 第二次查询,小红语文成绩95 | |
6 | 按照正确逻辑,事务A前后两次读取到的数据应该一致 |
三、事务隔离级别
事务隔离级别就是在不同程度上解决脏读、幻读、不可重复读的问题。数据库事务隔离级别有四种,由低到高分别为读未提交(Read Uncommitted)、读已提交(Read Committed)、可重复读(Repeatable Read)、串行化(Serializable)。
1、读未提交(Read Uncommitted)
读未提交,顾名思义,就是一个事务可以读取另一个未提交事务的数据。在这种隔离级别下所有事务都能读取其他事务未提交的数据。
读取其他事务未提交的数据会造成脏读。因此在此隔离级别下不能解决脏读、幻读和不可重复读。
读未提交可能会产生脏读的现象,那么怎么解决脏读呢?那就是使用读已提交。
2、读已提交(Read Committed)
在这种隔离级别下,所有事务只能读取其他事务已经提交的内容。
能够彻底解决脏读的现象。但在这种隔离级别下,会出现一个事务的前后多次的查询中却返回了不同内容的数据的现象,也就是出现了不可重复读。
这是大多数数据库系统默认的隔离级别,例如Oracle和SQL Server,但mysql不是。
已提交可能会产生不可重复读的现象,我们可以使用可重复读。
3、可重复读(Repeatable Read)
在这种隔离级别下,所有事务前后多次的读取到的数据内容是不变的。
也就是某个事务在执行的过程中,不允许其他事务进行update操作,但允许其他事务进行add操作,造成某个事务前后多次读取到的数据总量不一致的现象,从而产生幻读。
这才是mysql的默认事务隔离级别
可重复读在特殊情况下依然会产生幻读,此时我们可以使用串行化来解决。
4、串行化(Serializable)
在这种隔离级别下,所有的事务顺序执行,所以他们之间不存在冲突,从而能有效地解决脏读、不可重复读和幻读的现象。
但是安全和效率不能兼得,串行化会大大降低数据库的性能,一般不使用这种级别。
通过表格来表示四种不同隔离级别能够处理的问题
隔离级别 | 脏读 | 不可重复读 | 幻读 | 加锁读 |
读未提交 | 否 | 否 | 否 | 否 |
读已提交 | 是 | 否 | 否 | 否 |
可重复读 | 是 | 是 | 否(InnoDB除外) | 否 |
串行化 | 是 | 是 | 是 | 是 |
以上所说的隔离级别及当前级别存在的问题只是一种规范,不同的数据库厂商可以有不同的实现。
例如在mysql的可重复读的级别上,使用临键锁的方式就已经解决了幻读的问题。
四、MVCC工作原理
mysql为了实现以上隔离级别,提出了LBCC(Lock-Based Concurrent Control,基于锁的并发控制)与MVCC(Multi-Version Concurrent Control,基于多版本的并发控制)。
在LBCC中,读写冲突,会使用诸如记录锁、间隙锁与临键锁等锁来实现数据的并发安全,因此读写性能不高。
MVCC是为了解决事务中读-写操作并发安全问题的无锁并发控制技术,是通过数据库记录的隐式字段undo log日志和ReadView来实现的。
1、MVCC解决的问题
1. 并发读—写操作:可以解决读写并发阻塞问题,使读操作不阻塞写操作,写操作不阻塞读操作。
2. 解决脏读、不可重复读、幻读等事务隔离性问题
3. MVCC采用乐观锁方式实现,降低了死锁的概率
2、实现原理
MVCC是通过数据库的undo log版本链和ReadView方式来实现的无锁并发控制的。在InnoDB引擎下读已提交和可重复读隔离级别都是基于MVCC进行并发事务控制。
以下面4个事务为例进行解析MVCC实现原理和执行过程。
事务A、B、C对数据进行修改操作,事务D进行两次查询操作,第一次是在事务B修改后未commit前查询,第二次是在事务C修改后未commit前查询。
2.1 undo_log版本链
undo log
undo log主要用于事务回滚时恢复原来的数据。mysql在执行sql语句时,会将一条逻辑相反的日志保存到undo log中。因此,undo log中记录的也是逻辑日志,主要记录insert、update、delete相关操作的数据。
当sql语句为insert时,会在undo log中记录本次插入的主键id。等事务回滚时,delete此id即可。
当sql语句为update时,会在undo log中记录修改前的数据。等事务回滚时,再执行一次update,得到原来的数据。
当sql语句为delete时,会在undo log中记录删除前的数据。等事务回滚时,insert原来的数据即可。
数据库事务四大特性中的原子性,即事务具有不可分割性,要么全部成功,要么全部失败,其底层就靠undo log实现。在某一步执行失败时,会对之前事务的语句进行回滚。
行隐藏列
在数据库中的每一行上,除了存放真实的数据以外,还存在着3个隐藏列——row_id、trx_id与roll_pointer。
row_id(主键id)
若当前表有整数类型的主键,则row_id就是主键的值。若没有整数类型的主键,则mysql会按照字段顺序选择一个非空的整数类型的唯一索引作为row_id。如果mysql没有找到,则会自动生成一个自动增长的整数作为row_id。
trx_id(事务编号)
当一个事务开始执前,mysql会为这个事务分配一个全局自增的事务id。之后该事务对当前行进行的增、删、改操作时,都会将自己的事务id记录到trx_id中。
roll_pointer(回滚指针)
事务对当前行进行修改时,会将旧数据写入进undo log中,再将新数据写入当前行,且当前行的roll_pointer指向刚生成的undo log,因此可以通过roll_pointer找到该行的前一个版本。
undo log版本链
当一直有事务对该条数据进行修改时,就会一直生成undo log日志,最终会将这些undo log日志形成undo log版本链。
上面的例子事务A、B、C修改id=1088的数据行生成的undo log版本链如下
每一个事务对该行修改时,都会生成一个undo log,用于保存之前的版本,之后再将新版本的roll_pointer指向刚才生成的undo log。因此roll_pointer可以将这些不同版本的undo log串联起来,形成undo log版本链。
2.2 ReadView
ReadView是“快照读”SQL执行时MVCC提取数据的依据。首先需要理解下快照读和当前读。
快照读:就是最普通的Select查询的SQL语句,不包括select... for update 和select... lock in share mode。
当前读:读取的数据库记录,都是当前最新的版本,会对当前读取的数据进行加锁,防止其他事务修改数据。是悲观锁的一种操作。如下操作都是当前读:
- select lock in share mode (共享锁)
- select for update (排他锁)
- update (排他锁)
- insert (排他锁)
- delete (排他锁)
- 串行化事务隔离级别
在mysql中只有在快照读的情况下才会使用MVCC,尤其是在InnoDB引擎RR-可重复读隔离级别下会区分对待,快照读使用的是MVCC进行数据读取,当前读使用的是next-key lock锁(行锁+间隙锁)来实现数据读取。快照读和当前读在RR隔离级别下 对数据提取方式是完全不同的
在事务每一次执行快照读或事务初次执行快照读时,会生成一致性视图ReadView。
ReadView的作用是判断undo log版本链中哪些数据对当前事务可见。
ReadView包含几个重要的参数
m_ids:当前活跃的事务编号集合
min_trx_id:最小活跃事务编号(最小未提交事务编号)
max_trx_id:预分配事务编号,当前最大事务编号+1
create_trx_id:ReadView创建者的事务编号
ReadView生成
在RC-读已提交和RR-可重复读隔离级别下生成ReadView方式是不同的。
RC-读已提交
在RC-读已提交隔离级别下,每一次执行快照读时都会生成一个新的ReadView。
在RC隔离级别下,根据上面例子的事务执行为例,生成的ReadView过程
RR-可重复读
在RR-可重复读隔离级别下,仅在第一次执行快照读时生成ReadView,后续快照读复用第一次生成的ReadView。
在RR隔离级别下,根据上面例子的事务执行为例,生成的ReadView过程
ReadView数据提取过程
事务在执行快照读时,会从undo log历史版本链中从最新版本开始往前比对,通过一系列的规则,根据快照版本中的trx_id字段和ReadView来确定该版本对于当前事务是否可见。具体规则如下:
a. 若trx_id < min_trx_id,说明该版本对于当前事务(ReadView)来说,是已提交事务生成的,那么对于当前事务是可见的。
b. 若trx_id >= max_trx_id,说明该版本对于当前事务(ReadView)来说,是“将来”的事务生成的,那么对于当前事务是不可见的。
c. 若min_trx_id<= trx_id < max_trx_id,需进一步判断trx_id是否在m_ids集合列表中
i. 若 trx_id在m_ids中,说明该版本是由还未提交的事务生成的,对于当前事务不可见。
ii. 若trx_id不在m_ids中,说明该版本是已提交的事务生成的,对于当前事务可见。
d. 若trx_id=create_trx_id,即当前对比版本是由当前事务生成的,那么此版本对于当前事务当然可见。
e. 如果当前对比版本不可见,则通过roll_pointer找到上一个版本进行对比,直到找到可见版本或找不到任何一个可见版本。
RC-读已提交
根据上面执行的例子,RC隔离级别下生成了两个不同的ReadView。在比对数据时,两个ReadView中的m_ids不同,最终就会出现两个结果。
ReadView1与undo log比对结果
ReadView2与undo log比对结果
通过上面两个比对结果可知,两次快照读发生了不可重复读的问题,故在RC隔离级别下是无法解决不可重复读的问题。
RR-可重复读
RR-可重复读隔离级别下只生成一次ReadView,后续快照读都是复用之前的ReadView,所以通过ReadView复用的方式就可以解决不可重复读的情况。
RR-可重复读隔离级别下正常情况下通过MVCC版本控制的方式可以解决幻读的问题。因为作为ReadView在没有发生变化的前提下,mysql在中间执行过程中,无论是产生了新增、修改还是删除的话对于事务4都是不可见的。因此在连续多次快照读情况下ReadView会产生复用,没有幻读问题。
但当两次快照读之间存在当前读,ReadView就会重新生成,导致产生幻读。举例如下
事务B先进行查询,获取ReadView1 显示1088-张三。
事务A 插入数据张三,同名不同人,此时事务1对应的undo_log为两条。
接着事务B执行update操作,当前读后再进行快照读是生成了ReadView2,但ReadView1和ReadView2的提取数据不同了。这种情况下在同一个事务B中前后两次相同的select查询结果不同的,产生了幻读。
产生幻读的问题就是在同一事务中 前后两次快照读中间加入了当前读的操作导致出现了幻读。
五、总结
从以上的分析中我们可以看出来,MVCC指的就是在使用RC-读已提交、RR-可重复读这两种隔离级别的事务在执行普通的SEELCT操作时访问记录的版本链的过程,这样子可以使不同事务的读-写、写-读操作并发执行,从而提升系统性能。