1 隔离级别
- READ UNCOMMITTED(读未提交):事务的修改,即使没有提交,对其他事务也都是可见的。事务可以读取未提交的数据,也就是会产生脏读,在实际应用中一般很少使用。
- READ COMMITTED(读已提交):大多数数据库系统的默认隔离级别都是它,但是MySQL不是。它能够避免脏读问题,但是在一个事务里对同一条数据的多次查询可能会得到不同的结果,也就是会产生不可重复读问题。
- REPEATABLE READ(可重复读):该隔离级别是MySQL默认的隔离级别,看名字就知道它能够防止不可重复读问题(通过mvcc的readview不同的创建方式实现),但是在一个事务里对一段数据的多次读取可能会导致不同的结果,也就是会有幻读的问题(注:这里说的无法解决是MySQL定义层面,对于InnoDB引擎使用next-key解决了幻读问题)
- SERIALIZABLE(可串行化):该隔离级别是级别最高的,它通过锁来强制事务串行执行,避免了前面说的所有问题。在高并发下,可能导致大量的超时和锁争用问题。实际应用中也很少用到这个隔离级别,因为RR级别解决了所有问题。
2 Redo log
redo log(重做日志)用来实现事务的持久性,即事务ACID中的D。其由两部分组成,一是内存中的重做日志缓冲(redo log buffer),因为保存在内存中是容易丢失的。二是重做日志文件(redo log file),保存在磁盘,是持久的。
在一个事务中,每次sql操作都要记录到redo log buffer中,在事务commit前,要先把buffer中的数据冲刷到file中,才能结束commit。所以事务的提交受到磁盘性能的限制,我们不能在循环中不断的提交事务。
参数 innodb_flush_log_at_trx_commit 用来控制重做日志刷新到磁盘的策略,该参数有3个值:0、1和2。
- 0:表示事务提交时不进行写redo log file的操作,这个操作仅在master thread中完成(master thread每隔1秒进行一次fsync操作)。
- 1:默认值,表示每次事务提交时进行写redo log file的操作。
- 2:表示事务提交时将redo log写入文件,不过仅写入文件系统的缓存中,不进行fsync操作。我们可以看到0和2的设置都比1的效率要高,但是破坏了数据库的ACID特性,不建议使用!
3 binlog
在MySQL数据库中还有一种二进制日志(binlog),从表面上来看它和redo log很相似,都是记录了对数据库操作的日志,但是,它们有着非常大的不同。
首先,redo log是在MySQL的InnoDB引擎层产生,而binlog则是在MySQL的上层产生,它不仅针对InnoDB引擎,其他任何引擎对于数据库的更改都会产生binlog。
其次,两种日志记录的内容形式不同,binlog是一种逻辑日志,其记录的是对应的SQL语句。而redo log则是记录的物理格式日志,其记录的是对于每个页的修改。
此外,两种日志记录写入磁盘的时间点不同,binlog只在事务提交完成后一次性写入,而redo log在上面也说了是在事务进行中不断被写入,这表现为日志并不是随事务提交的顺序进行写入的。
4 Undo log
Undo log是InnoDB MVCC事务特性的重要组成部分。当我们对记录做了变更操作时就会产生Undo记录,Undo记录默认被记录到系统表空间(ibdata)中,但从5.6开始,也可以使用独立的Undo 表空间。
Undo记录中存储的是老版本数据,当一个旧的事务需要读取数据时,为了能读取到老版本的数据,需要顺着undo链找到满足其可见性的记录。当版本链很长时,通常可以认为这是个比较耗时的操作。
5 MVCC
MVCC 多版本并发控制技术,用于多事务环境下,对数据读写在不加读写锁的情况下实现互不干扰,从而实现数据库的隔离性,在事务隔离级别为Read Commit 和 Repeatable Read中使用到,今天我们就用最简单的方式,来分析下MVCC具体的原理,先解释几个概念。
5.1 InnoDB存储引擎的行结构
InnoDB表数据的组织方式为主键聚簇索引,二级索引中采用的是(索引键值, 主键键值)的组合来唯一确定一条记录。
InnoDB表数据为主键聚簇索引,mysql默认为每个索引行添加了4个隐藏的字段,分别是:
- DB_ROW_ID:InnoDB引擎中一个表只能有一个主键,用于聚簇索引,如果表没有定义主键会选择第一个非Null的唯一索引作为主键,如果还没有,生成一个隐藏的
- DB_ROW_ID作为主键构造聚簇索引。
- DB_TRX_ID:最近更改该行数据的事务ID。
- DB_ROLL_PTR:undo log的指针,用于记录之前历史数据在undo log中的位置。
- DELETE BIT:索引删除标志,如果DB删除了一条数据,是优先通知索引将该标志位设置为1,然后通过(purge)清除线程去异步删除真实的数据。
整个MVCC的机制都是通过DB_TRX_ID,DB_ROLL_PTR这2个隐藏字段来实现的。
5.2 事务链表
当一个事务开始的时候,会将当前数据库中正在活跃的所有事务(执行begin,但是还没有commit的事务)保存到一个叫trx_sys的事务链表中,事务链表中保存的都是未提交的事务,当事务提交之后会从其中删除。
5.3 Readview
有了前面隐藏列和事务链表的基础,接下去就可以构造MySQL实现MVCC的关键——ReadView。
ReadView说白了就是一个数据结构,在事务开始的时候会根据上面的事务链表构造一个ReadView,初始化方法如下:
总共做了以下几件事:
- 活跃事务链表(trx_sys)中事务id最大的值被赋值给m_low_limit_id。
- 活跃事务链表中第一个值(也就是事务id最小)被赋值给m_up_limit_id。
- m_ids 为事务链表。
通过该ReadView,新的事务可以根据查询到的所有活跃事务记录的事务ID来匹配能够看见该记录,从而实现数据库的事务隔离,主要逻辑如下:
- 通过聚簇索引的行结构中DB_TRX_ID隐藏字段可以知道最近被哪个事务ID修改过。
- 一个新的事务开始时会根据事务链表构造一个ReadView。
- 当前事务根据ReadView中的数据去跟检索到的每一条数据去校验,看看当前事务是不是能看到这条数据。
判断逻辑:
- 当检索到的数据的事务ID小于事务链表中的最小值(数据行的DB_TRX_ID < m_up_limit_id)表示这个数据在当前事务开启前就已经被其他事务修改过了,所以是可见的。
- 当检索到的数据的事务ID表示的是当前事务自己修改的数据(数据行的DB_TRX_ID = m_creator_trx_id) 时,数据可见。
- 当检索到的数据的事务ID大于事务链表中的最大值(数据行的DB_TRX_ID >= m_low_limit_id) 表示这个数据在当前事务开启后到下一次查询之间又被其他的事务修改过,那么就是不可见的。
- 如果事务链表为空,那么也是可见的,也就是当前事务开始的时候,没有其他任意一个事务在执行。
- 当检索到的数据的事务ID在事务链表中的最小值和最大值之间,从m_low_limit_id到m_up_limit_id进行遍历,取出DB_ROLL_PTR指针所指向的回滚段的事务ID,把它赋值给 trx_id_current ,然后从步骤1重新开始判断,这样总能最后找到一个可用的记录。
5.4 RC和RR隔离级别ReadView的实现方式
我们知道,RC隔离级别是能看到其他事务提交后的修改记录的,也就是不可重复读,但是RR隔离级别完美的避免了,但是它们都是使用的MVCC机制,那又为何有两种截然不同的结果呢?其实我们看一下他们创建ReadView的区别就知道了。
在RC事务隔离级别下,每次语句执行都关闭ReadView,然后重新创建一份ReadView。
在RR下,事务开始后第一个读操作创建ReadView,一直到事务结束关闭。