相信工做了一段时间的同窗确定都用过事务,也都据说过事务的4大特性ACID。ACID表示原子性、一致性、隔离性和持久性。一个很好的事务处理系统,必须具有这些标准特性:html
而咱们最常说的隔离性其实有对应的隔离级别,MySQL规定的隔离级别有4种,分别是:mysql
能够看到隔离级别里最重要的只有两个隔离级别:RC和RR。那么问题来了,咱们知道上面说的ACID以及隔离级别的实现原理吗?不管是平时工做仍是面试,这部分的问题都重中之重,接下来,我会抛出几个问题,你们能够带着问题来看此文:面试
ACID问题:sql
隔离性里隔离级别的问题:数据库
解决这些问题以前,咱们要首先知道Redo log、Undo log以及MVCC都是什么。设计模式
redo log(重作日志)用来实现事务的持久性,即事务ACID中的D。其由两部分组成,一是内存中的重作日志缓冲(redo log buffer),其实易失的。二是重作日志文件(redo log file),其是持久的。缓存
在一个事务中的每一次SQL操做以后都会写入一个redo log到buffer中,在最后COMMIT的时候,必须先将该事务的全部日志写入到redo log file进行持久化(这里的写入是顺序写的),待事务的COMMIT操做完成才算完成。数据结构
因为重作日志文件打开没有使用O_DIRECT选项,所以重作日志缓冲先写入文件系统缓存。为了确保重作日志写入磁盘,必须进行一次fsync操做。因为fsync的效率取决于磁盘的性能,所以磁盘的性能决定了事务提交的性能,也就是数据库的性能。由此咱们能够得出在进行批量操做的时候,不要for循环里面嵌套事务。并发
参数 innodb_flush_log_at_trx_commit
用来控制重作日志刷新到磁盘的策略,该参数有3个值:0、1和2。异步
咱们能够看到0和2的设置都比1的效率要高,可是破坏了数据库的ACID特性,不建议使用!
在MySQL数据库中还有一种二进制日志(binlog),从表面上来看它和redo log很类似,都是记录了对数据库操做的日志,可是,它们有着很是大的不一样。
首先,redo log是在MySQL的InnoDB引擎层产生,而binlog则是在MySQL的上层产生,它不只针对InnoDB引擎,其余任何引擎对于数据库的更改都会产生binlog。
其次,两种日志记录的内容形式不一样,binlog是一种逻辑日志,其记录的是对应的SQL语句。而redo log则是记录的物理格式日志,其记录的是对于每一个页的修改。
此外,两种日志记录写入磁盘的时间点不一样,binlog只在事务提交完成后一次性写入,而redo log在上面也说了是在事务进行中不断被写入,这表现为日志并非随事务提交的顺序进行写入的。
在InnoDB引擎中,redo log都是以512字节进行存储的(和磁盘扇区的大小同样,所以redo log写入能够保证原子性,不须要double write),也就是重作日志缓存和文件都是以块的方式进行保存的,称为redo log block,每一个block占512字节。
重作日志除了日志自己以外,还由日志块头(log block header)及日志块尾(log block tailer)两部分组成。
下面我来解释一下组成Log Block header的4个部分各自的含义:
Log Block tailer只包含一个LOG_BLOCK_TRL_NO,它的值和LOG_BLOCK_HDR_NO相同,并在函数log_block_init中被初始化。
前面提到了redo log是用来实现ACID的持久性的,也就是只要事务提交成功后,事务内的全部修改都会保存到数据库,哪怕这时候数据库crash了,也要有办法来进行恢复。也就是Crash Recovery。
说到恢复,咱们先来了解一个概念:什么是LSN?
LSN(log sequence number) 用于记录日志序号,它是一个不断递增的 unsigned long long 类型整数,占用8字节。它表明的含义有:
checkpoint:它是redo log中的一个检查点,这个点以前的全部数据都已经刷新回磁盘,当DB crash后,经过对checkpoint以后的redo log进行恢复就能够了。
咱们能够经过命令show engine innodb status来观察LSN的状况:
--- LOG --- Log sequence number 33646077360 Log flushed up to 33646077360 Last checkpoint at 33646077360 0 pending log writes, 0 pending chkp writes 49687445 log i/o's done, 1.25 log i/o's/second
Log sequence number表示当前的LSN,Log flushed up to表示刷新到redo log文件的LSN,Last checkpoint at表示刷新到磁盘的LSN。若是把它们三个简写为 A、B、C
的话,它们的值的大小确定为 A>=B>=C
。
InnoDB引擎在启动时无论上次数据库运行时是否正常关闭,都会进行恢复操做。由于重作日志记录的是物理日志,所以恢复的速度比逻辑日志,如二进制日志要快不少。恢复的时候只须要找到redo log的checkpoint进行恢复便可。
重作日志记录了事务的行为,能够很好的经过其对页进行“重作”操做。可是事务有时候还须要进行回滚操做,也就是ACID中的A(原子性),这时就须要Undo log了。所以在数据库进行修改时,InnoDB存储引擎不但会产生Redo,还会产生必定量的Undo。这样若是用户执行的事务或语句因为某种缘由失败了,又或者用户一条ROLLBACK语句请求回滚,就能够利用这些Undo信息将数据库回滚到修改以前的样子。
Undo log是InnoDB MVCC事务特性的重要组成部分。当咱们对记录作了变动操做时就会产生Undo记录,Undo记录默认被记录到系统表空间(ibdata)中,但从5.6开始,也可使用独立的Undo 表空间。
Undo记录中存储的是老版本数据,当一个旧的事务须要读取数据时,为了能读取到老版本的数据,须要顺着undo链找到知足其可见性的记录。当版本链很长时,一般能够认为这是个比较耗时的操做。
为了保证事务并发操做时,在写各自的undo log时不产生冲突,InnoDB采用回滚段(Rollback Segment,简称Rseg)的方式来维护undo log的并发写入和持久化。回滚段其实是一种 Undo 文件组织方式,每一个回滚段又有多个undo log slot。具体的文件组织方式以下图所示:
上图展现了基本的Undo回滚段布局结构,其中:
innodb_undo_logs
设置)存放到独立undo表空间中(若是没有打开独立Undo表空间,则存放于ibdata中,独立表空间能够经过参数 innodb_undo_directory
设置),用于普通事务的undo。如图所示,每一个回滚段维护了一个段头页,在该page中又划分了1024个slot(TRX_RSEG_N_SLOTS),每一个slot又对应到一个undo log对象,所以理论上InnoDB最多支持 96 * 1024个普通事务。
在InnoDB引擎中,undo log分为:
insert undo log是指在insert操做中产生的undo log,由于insert操做的记录,只对事务自己可见,对其余事务不可见(这是事务隔离性的要求),故该undo log能够在事务提交后直接删除,不须要进行purge操做。而update undo log记录的是delete和update操做产生的undo log。该undo log可能须要提供MVCC机制,所以不能在事务提交时就进行删除,提交时放入undo log链表,等待purge线程进行最后的删除。下面是两种undo log的结构图。
对于一条delete语句 delete from t where a = 1
,若是列a有汇集索引,则不会进行真正的删除,而只是在主键列等于1的记录delete flag设置为1,即记录仍是存在在B+树中。而对于update操做,不是直接对记录进行更新,而是标识旧记录为删除状态,而后新产生一条记录。那这些旧版本标识位删除的记录什么时候真正的删除?怎么删除?
其实InnoDB是经过undo日志来进行旧版本的删除操做的,在InnoDB内部,这个操做被称之为purge操做,原来在srv_master_thread主线程中完成,后来进行优化,开辟了purge线程进行purge操做,而且能够设置purge线程的数量。purge操做每10s进行一次。
为了节省存储空间,InnoDB存储引擎的undo log设计是这样的:一个页上容许多个事务的undo log存在。虽然这不表明事务在全局过程当中提交的顺序,可是后面的事务产生的undo log总在最后。此外,InnoDB存储引擎还有一个history列表,它根据事务提交的顺序,将undo log进行链接,以下面的一种状况:
在执行purge过程当中,InnoDB存储引擎首先从history list中找到第一个须要被清理的记录,这里为trx1,清理以后InnoDB存储引擎会在trx1所在的Undo page中继续寻找是否存在能够被清理的记录,这里会找到事务trx3,接着找到trx5,可是发现trx5被其余事务所引用而不能清理,故再去history list中取查找,发现最尾端的记录时trx2,接着找到trx2所在的Undo page,依次把trx六、trx4清理,因为Undo page2中全部的记录都被清理了,所以该Undo page能够进行重用。
InnoDB存储引擎这种先从history list中找undo log,而后再从Undo page中找undo log的设计模式是为了不大量随机读操做,从而提升purge的效率。
MVCC 多版本并发控制技术,用于多事务环境下,对数据读写在不加读写锁的状况下实现互不干扰,从而实现数据库的隔离性,在事务隔离级别为Read Commit 和 Repeatable Read中使用到,今天咱们就用最简单的方式,来分析下MVCC具体的原理,先解释几个概念。
InnoDB表数据的组织方式为主键聚簇索引,二级索引中采用的是(索引键值, 主键键值)的组合来惟一肯定一条记录。
InnoDB表数据为主键聚簇索引,mysql默认为每一个索引行添加了4个隐藏的字段,分别是:
整个MVCC的机制都是经过DB_TRX_ID
,DB_ROLL_PTR
这2个隐藏字段来实现的。
当一个事务开始的时候,会将当前数据库中正在活跃的全部事务(执行begin,可是尚未commit的事务)保存到一个叫trx_sys
的事务链表中,事务链表中保存的都是未提交的事务,当事务提交以后会从其中删除。
有了前面隐藏列和事务链表的基础,接下去就能够构造MySQL实现MVCC的关键——ReadView。
ReadView说白了就是一个数据结构,在事务开始的时候会根据上面的事务链表构造一个ReadView,初始化方法以下:
// readview 初始化 // m_low_limit_id = trx_sys->max_trx_id; // m_up_limit_id = !m_ids.empty() ? m_ids.front() : m_low_limit_id; ReadView::ReadView() : m_low_limit_id(), m_up_limit_id(), m_creator_trx_id(), m_ids(), m_low_limit_no() { ut_d(::memset(&m_view_list, 0x0, sizeof(m_view_list))); }
总共作了如下几件事:
trx_sys
)中事务id最大的值被赋值给m_low_limit_id
。m_up_limit_id
。m_ids
为事务链表。经过该ReadView,新的事务能够根据查询到的全部活跃事务记录的事务ID来匹配可以看见该记录,从而实现数据库的事务隔离,主要逻辑以下:
那么问题来了,怎么来判断可见性呢?咱们来经过源码一探究竟:
// 判断数据对应的聚簇索引中的事务id在这个readview中是否可见 bool changes_visible( trx_id_t id, // 记录的id const table_name_t& name) const MY_ATTRIBUTE((warn_unused_result)) { ut_ad(id > 0); // 若是当前记录id < 事务链表的最小值或者等于建立该readview的id就是它本身,那么是可见的 if (id < m_up_limit_id || id == m_creator_trx_id) { return(true); } check_trx_id_sanity(id, name); // 若是该记录的事务id大于事务链表中的最大值,那么不可见 if (id >= m_low_limit_id) { return(false); // 若是事务链表是空的,那也是可见的 } else if (m_ids.empty()) { return(true); } const ids_t::value_type* p = m_ids.data(); //判断是否在ReadView中,若是在说明在建立ReadView时 此条记录还处于活跃状态则不该该查询到,不然说明建立ReadView是此条记录已是不活跃状态则能够查询到 return(!std::binary_search(p, p + m_ids.size(), id)); }
总结一下可见性判断逻辑:
trx_id_current
,而后从步骤1从新开始判断,这样总能最后找到一个可用的记录。咱们知道,RC隔离级别是能看到其余事务提交后的修改记录的,也就是不可重复读,可是RR隔离级别完美的避免了,可是它们都是使用的MVCC机制,那又为什么有两种大相径庭的结果呢?其实咱们看一下他们建立ReadView的区别就知道了。
上面的总结英文版为:With REPEATABLE READ isolation level, the snapshot is based on the time when the first read operation is performed. With READ COMMITTEDisolation level, the snapshot is reset to the time of each consistent read operation.
来源自MySQL官网:MySQL Glossary-glos_consistent_read
由于RC每次查询语句都建立一个新的ReadView,因此活跃的事务列表一直在变,也就致使若是事务B update提交了后事务A才进行查询,查询的结果就是最新的行,也就是不可重复读咯。而RR则一直用的事务开始时建立的ReadView。
还记得开头提到的问题吗?如今应该可以所有解决了。
其实这个在上面Undo log中已经说起了。在事务里任何对数据的修改都会写一个Undo log,而后进行数据的修改,若是出现错误或者用户须要回滚的时候能够利用Undo log的备份数据恢复到事务开始以前的状态。
这个在上面Redo log中已经说起了。在一个事务中的每一次SQL操做以后都会写入一个redo log到buffer中,在最后COMMIT的时候,必须先将该事务的全部日志写入到redo log file进行持久化(这里的写入是顺序写的),待事务的COMMIT操做完成才算完成。即便COMMIT后数据库有任何的问题,在下次重启后依然可以经过redo log的checkpoint进行恢复。也就是上面提到的crash recovery。
在事务处理的ACID属性中,一致性是最基本的属性,其它的三个属性都为了保证一致性而存在的。
首先回顾一下一致性的定义。所谓一致性,指的是数据处于一种有意义的状态,这种状态是语义上的而不是语法上的。最多见的例子是转账。例如从账户A转一笔钱到账户B上,若是账户A上的钱减小了,而账户B上的钱却没有增长,那么咱们认为此时数据处于不一致的状态。
在数据库实现的场景中,一致性能够分为数据库外部的一致性和数据库内部的一致性。前者由外部应用的编码来保证,即某个应用在执行转账的数据库操做时,必须在同一个事务内部调用对账户A和账户B的操做。若是在这个层次出现错误,这不是数据库自己可以解决的,也不属于咱们须要讨论的范围。后者由数据库来保证,即在同一个事务内部的一组操做必须所有执行成功(或者所有失败)。这就是事务处理的原子性。(上面说过了是用Undo log来保证的)
可是,原子性并不能彻底保证一致性。在多个事务并行进行的状况下,即便保证了每个事务的原子性,仍然可能致使数据不一致的结果,好比丢失更新问题。
为了保证并发状况下的一致性,引入了隔离性,即保证每个事务可以看到的数据老是一致的,就好象其它并发事务并不存在同样。用术语来讲,就是多个事务并发执行后的状态,和它们串行执行后的状态是等价的。
RU级别的操做其实就是对事务内的每一条更新语句对应的行记录加上读写锁来操做,而不把一个事务当成一个总体来加锁,因此会致使脏读。可是RC和RR可以经过MVCC来保证记录只有在最后COMMIT后才会让别的事务看到。
这个在上面的MVCC的最后说到了,在RC事务隔离级别下,每次语句执行都关闭ReadView,而后从新建立一份ReadView。而在RR下,事务开始后第一个读操做建立ReadView,一直到事务结束关闭。
这个是由于RR隔离级别使用了Next-key Lock这么个东东,也就是Gap Lock+Record Lock的方式来进行间隙锁定,具体原理本章不深刻讨论,能够参考个人另外一篇文章。