atomicity,原子性
事务是不可分割的工作单位,事务中的操作语句要么全部完成,要么全不完成。
如果事务中的操作都是只读的,保持原子性是十分简单的,因为只读操作不会对数据做改变。
如果事务中存在改变数据的操作,如插入、更新和删除,如果操作过程中出现失败,则要对已更改的部分进行恢复。
consistency,一致性
事务开始和解书,数据库的完整性约束没有被破坏,如对唯一索引的操作事务成功提交或失败回滚后,不会对唯一索引的唯一性约束进行破坏。
isolation,隔离性
通过锁实现,目的是使不同事务间操作数据时互不影响。
durability,持久性
事务提交后对数据库的影响是永久的,即使宕机也能够被恢复。
事务的原子性和持久性由redo log保证,一致性由undo log保证。这两个log不是互逆过程,redo log恢复的是提交完成的事务对数据页进行的修改,而undo log则是回滚到之前的某个版本或实现MVCC。
最简单的一种事务,实际生产中使用最为频繁,所有的操作都处于同一层次。由begin
开始,至commit
或rollback
结束。扁平事务的执行有三种不同的结果,
扁平事务的主要限制是不能提交或回滚事务的某一部分,或分几个步骤进行提交。当运行出错时,每次都要回滚到最开始的位置。
支持扁平事务的同时,允许事务执行过程中回滚到同一个事务中较早的一个状态。避免了回滚到事务开头造成过大的开销,而是通过保存点通知系统记住事务当前的状态。扁平事务在事务开始时会隐式创建一个保存点,所以当事务失败时直接回滚至开头部分。
使用savepoint
函数创建保存点,如下图,
上图是在事务中使用保存点,灰色部分是由rollback
导致部分回滚。
保存点在事务内部是递增的,rollback
回到保存点2的状态后,理论上下一个保存点的编号应该是3。但是实际情况下,新的保存点的编号为5,意味着rollback不会影响保存点的计数。
保存点模式的变种,带有保存点的扁平事务执行过程中如果遇到系统崩溃,所有的保存点都将消失,因为保存点是易失的。链事务的思想是,在提交一个事务时,释放不需要的数据对象。提交事务操作和下一个实务操作合并为一个原子操作,
链事务与带保存点的扁平事务不同的是,带有保存点的扁平事务可以回滚到任意正确的保存点。而链事务中回滚仅限于当前事务,即只能恢复到最近一个保存点。
是一个层次结构,由顶层事务控制着各个层次的事务,顶层事务之下嵌套的事务被称为子事务,
分为外部分布式事务和内部分布式事务,后面笔记中会讲解。
重做日志实现实物的持久性。由两部分组成
InnoDB引擎通过Force log at commit
机制实现事务的持久性,当事务提交时,必须先将事务的所有redo log写入到redo log file中进行持久化。redo log 基本上都是顺序写的。
每次将redo log buffer中的内容写入 redo log file的后,InnoDB引擎都会执行一次fsync
操作。fsync的效率取决于磁盘性能,InnoDB支持用户设置innodb_flush_log_at_trx_commit
来控制 redo log buffer中的内容刷新到磁盘的策略,该参数可取0、1和2,这三个值,
值 | 说明 |
---|---|
0 | 事务不进行写入文件操作,这个操作只在主线程中进行,主线程中每秒会调用一次fsync操作,即 log buffer 的刷写操作和事务提交操作没有关系。在这种情况下,MySQL性能最好,但如果 mysqld 进程崩溃,通常会导致最后 1s 的日志丢失 |
1 | 事务提交时必须调用一次fsync操作。这是最安全的配置,但由于每次事务都需要进行磁盘I/O,所以也最慢。 |
2 | 事务提交时仅将 redo log写入redo log buffer,不进行fsync操作。日志文件会每秒刷写一次到磁盘。这时如果 mysqld 进程崩溃,由于日志已经写入到系统缓存,所以并不会丢失数据;在操作系统崩溃的情况下,通常会导致最后 1s 的日志丢失。 |
2是对0和1两种方式的折中,将参数设置为0或2可以提高提交的性能,但是使事务丧失了ACID的特性(持久性可能不会被满足)。
InnoDB中,redo log以512字节的块进行存储。
每个redo log block分为log block head
、log block body
和log block tail
,如下,
log group是一个逻辑上的概念,并没有实际的物理文件表示 log group。
log group由多个 redo log file组成,每个log group中的日志文件大小是相同的。
redo log file中存储的就是之前在log buffer中存储的log block。
上图是有两个 redo log file的 log group,每个log file开头有2KB的信息,其余为 log block存储的日志内容。
InnoDB的存储管理是页级别的,其 redo log的格式也是页级别的。不同的操作有不同的redo log格式,但是都有通用的头部,
log sequence number,日志序列号,表示写入到 redo log中的字节总量。
该值用于判断页是否需要恢复操作。具体的原理是,redo log file和页中各记录一个LSN。
在同一个事务中修改数据操作时,将修改结果更新到内存后,会在redo log添加一行记录记录“需要在哪个数据页上做什么修改”,并将该记录状态置为prepare
。
等到commit提交事务后,会将此次事务中在redo log添加的记录的状态都置为commit
状态。
之后将修改落盘时,会将redo log中状态为commit的记录的修改都写入磁盘。整个过程如下,
图片摘自简书博客
首先明确一个问题,有了redo log,为什么还需要binlog呢?
write pos
表示日志当前记录的位置,当ib_logfile_4写满后,会从ib_logfile_1从头开始记录;check point
表示将日志记录的修改写进磁盘,完成数据落盘,数据落盘后checkpoint会将日志上的相关记录擦除掉基于上述两个问题,引入bin log,
max_binlog_size
设置每个bin log文件的大小,当文件大小大于给定值后,日志会发生滚动,之后的日志记录到新的文件上。
bin log和 redo log要么都成功写入,要么一起失败。如果写入bin log时,事务会回滚。如果在将redo log中的状态改为commit
的过程失败,也会回滚,且bin log中也会删除该事务的记录。
redo log记录了事务的行为,可以通过redo log对页进行重做操作。事务有时需要进行回滚,此时就需要undo log。undo log的功能有两个,
rollback
命令请求回滚。此时可以使用undo信息将数据库修改回之前的样子undo log是采用段(segment)的方式来记录的,每个undo操作在记录的时候占用一个undo log segment,该段位于共享表空间内。innodb 存储引擎对 undo 的管理采用段的方式。rollback segment称为回滚段,每个回滚段中有1024个 undo log segment
undo是逻辑日志,不是物理日志。使用undo日志恢复的过程实际上就是将修改以逻辑的形式恢复,
修改方式 | 逻辑回滚方式 |
---|---|
insert | delete |
delete | insert |
update | update |
undo回滚后,数据结构和页在变化后可能差异很大。因为在多并发的环境中,可能会有成百上千个并发事务。一个事务对数据进行修改时,其他事务也会对页中的记录进行修改,所以不能将页直接回滚到事务开始时的样子,因为会影响其他事务的工作。所以undo不会对页进行过多修改,主要是对数据结构上的修改。
InnoDB中,undo log有两种,
delete和update操作并不直接删除原有数据,如下面的SQL语句,
delete from t where a=1;
假设在表t中,a字段是主键,b字段上存在普通索引。
如果事务不是只读事务,即涉及到了数据的修改,默认情况下会在 commit 的时候调用 fsync() 将日志刷到磁盘,保证事务的持久性。
但是一次刷一个事务的日志性能较低,特别是事务集中在某一时刻时事务量非常大的时候。innodb提供了 group commit 功能,可以将多个事务的事务日志通过一次fsync()刷到磁盘中。
因为事务在提交的时候不仅会记录事务日志,还会记录二进制日志。二进制日志是MySQL的上层日志,先于存储引擎的事务日志被写入。
commit prepare
阶段;prepare
阶段后,立即写内存中的二进制日志,写完内存中的二进制日志后就相当于确定了commit操作;sync_binlog
和 innodb_flush_log_at_trx_commit
控制。为保证二进制日志和事务日志的一致性,在提交后的prepare
阶段会启用一个prepare_commit_mutex锁保证顺序性和一致性。但这样会导致开启二进制日志后group commmit失效,特别是在主从复制结构中,几乎都会开启二进制日志。MySQL5.6 中进行了改进。提交事务时,在存储引擎层的上一层结构中会将事务按序放入一个队列,队列中的第一个事务称为 leader,其他事务称为 follower,leader 控制着 follower 的行为。虽然顺序还是一样先刷二进制,再刷事务日志,但是机制完全改变了:删除了原来的prepare_commit_mutex 行为,也能保证即使开启了二进制日志,group commit 也是有效的。
在flush阶段写入二进制日志到内存中,不是写完就进入sync阶段的,而是要等待一定的时间,多积累几个事务的 binlog 一起进入 sync 阶段。等待时间由变量binlog_max_flush_queue_time
决定,默认值为 0,表示不等待直接进入 sync。设置该变量为一个大于0的值的好处是group中的事务多了,性能会好一些,但是这样会导致事务的响应时间变慢,所以建议不要修改该变量的值,除非事务量非常多并且不断的在写入和更新。
进入到 sync 阶段,会将 binlog 从内存中刷入到磁盘,刷入的数量和单独的二进制日志刷盘一样,由变量sync_binlog
控制。
当有一组事务在进行 commit 阶段时,其他新事务可以进行 flush 阶段,它们本就不会相互阻塞,所以 group commit 会不断生效。当然,group commit 的性能和队列中的事务数量有关,如果每次队列中只有1个事务,那么 group commit 和单独的 commit 没什么区别,当队列中事务越来越多时,即提交事务越多越快时,group commit 的效果越明显。