事务
事务ACID特性
原子性(atomicity) :事务最小工作单元,要么全成功,要么全失败 。
一致性(consistency): 事务开始和结束后,数据库的完整性不会被破坏 。
隔离性(isolation) :不同事务之间互不影响,四种隔离级别为RU(读未提交)、RC(读已提交)、RR(可重复读)、SERIALIZABLE (串行化)。
持久性(durability) :事务提交后,对数据的修改是永久性的,即使系统故障也不会丢失
事务隔离级别
未提交读(READ UNCOMMITTED/RU)
如果一个事务读到了另一个未提交事务修改过的数据,那么这种 隔离级别 就称之为 未提交读 。
问题: 脏读(一个事务读取到另一个事务未提交的数据)
已提交读(READ COMMITTED/RC)
如果一个事务只能读到另一个已经提交的事务修改过的数据,并且其他事务每对该数据进行一次修改并提交后,该事务都能查询得到最新值
问题:不可重复读(一个事务因读取到另一个事务已提交的update。导致对同一条记录读取两次以上的结果不一致)
可重复读(REPEATABLE READ/RR)
一个事务第一次读过某条记录后,即使其他事务修改了该记录的值并且提交,该事务之后再读该条记录时,读到的仍是第一次读到的值,而不是每次都读到不同的数据。
问题:幻读
串行化(SERIALIZABLE)
事务实现方案
LBCC
基于锁的并发控制(Lock-Based Concurrent Control,简写 LBCC)。通过对读写操作加不同的锁,以及对释放锁的时机进行不同的控制,就可以实现四种隔离级别。
传统的锁有两种:
1、读操作通常加共享锁(Share locks,S锁,又叫读锁),加了共享锁的记录,其他事务也可以读,但不能写;
2、写操作加排它锁(Exclusive locks,X锁,又叫写锁),加了排它锁的记录,其他事务既不能读,也不能写;
另外,对于锁的粒度,又分为行锁和表锁,行锁只锁某行记录,对其他行的操作不受影响,表锁会锁住整张表,所有对这个表的操作都受影响。
LBCC锁执行过程:
MVCC
MVCC(Multi-Version Concurrency Control)即多版本并发控。MVCC使得大部分支持行锁的事务引擎,不再单纯的使用行锁来进行数据库的并发控制,取而代之的是把数据库的行锁与行的多个版本结合起来,只需要很小的开销,就可以实现非锁定读,从而大大提高数据库系统的并发性能。
Innodb MVCC实现
InnoDB的MVCC是通过在每行记录后面保存两个隐藏的列来实现的。这两个列,一个保存了行的事务ID(DB_TRX_ID),一个保存了行的回滚指针(DB_ROLL_PTR)。
每开始一个新的事务,都会自动递增产生一个新的事务id。事务开始时刻的会把事务id放到当前事务影响的行中,当查询时需要用当前事务id和每行记录的事务id进行比较。
MVCC 在mysql 中的实现依赖的是 undo log 与 read view 。
undo log
根据行为的不同,undo log分为两种: insert undo log 和 update undo log。
insert undo log
insert undo log 是在 insert 操作中产生的 undo log。
因为 insert 操作的记录只对事务本身可见,对于其它事务此记录是不可见的,所以 insert undo log 可以在事务提交后直接删除而不需要进行 purge 操作。
update undo log
undate undo log 是 update 或 delete 操作中产生的 undo log。
第一次修改:
第二次修改:
因为会对已经存在的记录产生影响,为了提供 MVCC机制,因此 update undo log 不能在事务提交时 就进行删除,而是将事务提交时放到入 history list 上,等待 purge 线程进行最后的删除操作。
ReadView
对于使用 READ UNCOMMITTED 隔离级别的事务来说,查询数据时直接读取记录的最新版本就好了。
对于使用SERIALIZABLE 隔离级别的事务来说,使用加锁的方式来访问记录。
对于使用 READ COMMITTED 和 REPEATABLE READ 隔离级别的事务来说,就需要用到我们上边所说的版本链 了:
ReadView的创建:
当执行Select操作时创建,把当前数据库中活跃的事务id放到一个集合中,命名为m_ids。
然后就可以通过ReadView判断版本的可见性。
数据版本可见判断方法:
1、如果被访问版本的 trx_id 属性值小于 m_ids 列表中最小的事务id,表明生成该版本的事务在生成ReadView前已经提交,所以该版本可以被当前事务访问。
2、如果被访问版本的 trx_id 属性值大于 m_ids 列表中最大的事务id,表明生成该版本的事务在生成ReadView后才生成,所以该版本不可以被当前事务访问。
3、如果被访问版本的 trx_id 属性值在 m_ids 列表中最大的事务id和最小事务id之间,那就需要判断一下 trx_id 属性值是不是在 m_ids 列表中,如果在,说明创建 ReadView 时生成该版本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建 ReadView 时生成该版本的事务已经被提交,该版本可以被访问。
通过readView可以实现RC和RR事务隔离级别:
RC:读已提交
当前事务中执行Select操作,会生成一个ReadView,判断版本可见性,会得到一个结果。如果是RC级别每次执行Select操作时都生成一个ReadView。就可以实现RC。
RR:可重复读
在当前事务中执行select操作时会生成一个ReadView,以后每次查询时都使用同一个ReadView。可以保证每次查询得到的结果是相同的。
MVCC下的读操作
在MVCC并发控制中,读操作可以分成两类:快照读 (snapshot read)与当前读 (current read)。
快照读:读取的是记录的可见版本 (有可能是历史版本),不用加锁。
当前读:读取的是记录的最新版本,并且当前读返回的记录,都会加上锁,保证其他事务不会并发修改这条记录。
在一个支持MVCC并发控制的系统中,哪些读操作是快照读?哪些操作又是当前读呢?
以MySQL InnoDB为例:
快照读:简单的select操作,属于快照读,不加锁。(当然,也有例外,下面会分析) 不加读锁读历史版本
当前读:特殊的读操作,插入/更新/删除操作,属于当前读,需要加锁。 加行写锁 读当前版本.
select * from table where ? lock in share mode;
select * from table where ? for update;
insert into table values (…);
update table set ? where ?;
delete from table where ?;
以上所有以上的语句,都属于当前读,读取记录的最新版本。并且,读取之后,还需要保证其他并发事务不能修改当前记录,对读取记录加锁。其中,除了第一条语句,对读取记录加S锁 (共享锁)外,其他的操作,都加的是X锁 (排它锁)。
事务回滚和数据恢复
事务的隔离性由多版本控制机制和锁实现,而原子性,持久性和一致性主要是通过redo log、undo log和Force Log at Commit机制机制来完成的。
undo log 用于对事务的影响进行撤销,也可以用于多版本控制。
redo log 每次数据库的SQL操作导致的数据变化它都会记录一下,具体来说,redo log是物理日志,记录的是数据库页的物理修改操作。如果数据发生了丢失,数据库可以根据redo log进行数据恢复。
InnoDB通过Force Log at Commit机制实现事务的持久性,即当事务COMMIT时,必须先将该事务的所有日志都写入到redo log文件进行持久化之后,COMMIT操作才算完成。
当事务的各种SQL操作执行时,即会在缓冲区中修改数据,也会将对应的redo log写入它所属的缓存。当事务执行COMMIT时,与该事务相关的redo log缓冲必须都全部刷新到磁盘中之后COMMIT才算执行成功。
数据库日志和数据落盘机制,如下图所示:
redo log写入磁盘时,必须进行一次操作系统的fsync操作,防止redo log只是写入了操作系统的磁盘缓存中。参数innodb_flush_log_at_trx_commit可以控制redo log日志刷新到磁盘的策略
数据库崩溃重启后需要从redo log中把未落盘的脏页数据恢复出来,重新写入磁盘,保证用户的数据不丢失。当然,在崩溃恢复中还需要回滚没有提交的事务。由于回滚操作需要undo log的支持,undo log的完整性和可靠性需要redo日志来保证,所以崩溃恢复先做redo恢复数据,然后做undo回滚。
在事务执行的过程中,除了记录redo log,还会记录一定量的undo log。undo log记录了数据在每个操作前的状态,如果事务执行过程中需要回滚,就可以根据undo log进行回滚操作。undo log的存储不同于redo log,它存放在数据库内部的一个特殊的段(segment)中,这个段称为回滚段。回滚段位于共享表空间中。undo段中的以undo page为更小的组织单位。undo page和存储数据库数据和索引的页类似。因为redo log是物理日志,记录的是数据库页的物理修改操作。所以undo log(也看成数据库数据)的写入也会产生redo log,也就是undo log的产生会伴随着redo log的产生,这是因为undo log也需要持久性的保护。如上图所示,表空间中有回滚段和叶节点段和非叶节点段,而三者都有对应的页结构。
我们再来总结一下数据库事务的整个流程,如下图所示:
事务进行过程中,每次DML sql语句执行,都会记录undo log和redo log,然后更新数据形成脏页,然后redo log按照时间或者空间等条件进行落盘,undo log和脏页按照checkpoint进行落盘,落盘后相应的redo log就可以删除了。此时,事务还未COMMIT,如果发生崩溃,则首先检查checkpoint记录,使用相应的redo log进行数据和undo log的恢复,然后查看undo log的状态发现事务尚未提交,然后就使用undo log进行事务回滚。事务执行COMMIT操作时,会将本事务相关的所有redo log都进行落盘,只有所有redo log落盘成功,才算COMMIT成功。然后内存中的数据脏页继续按照checkpoint进行落盘。如果此时发生了崩溃,则只使用redo log恢复数据。