事务(Transaction)是数据库区别于文件系统的重要特性之一。
数据库引入事务的主要目的:事务会把数据库从一种抑制状态转换为另一种一致状态。在数据库提交工作时,要么所有修改都已经保存了,要么所有修改都不保存。
InnoDB中的事务完全符合ACID的特性
事务的隔离性由锁来实现。原子性,一致性,持久性通过数据库的redo log和undo log来完成,redo log称为重做日志,用来保证事务的原子性和持久性。undo log用来保证事务的一致性。
保证事务的一致性, 回滚而记录的这些东西称之为撤销日志--undo log.
undo日志的格式
INSERT操作对应的undo日志
undo no
:undo的编号:在一个事务中是从0开始递增的,也就是说只要事务没提交,每生成一条undo日志,那么该条日志的undo no就增1DELETE,UPDATE操作对应的undo日志
记录更新过程
下面演示下事务对某行记录的更新过程:
1. 初始数据行
F1~F6是某行列的名字,1~6是其对应的数据。后面三个隐含字段分别对应该行的事务号和回滚指针,假如这条数据是刚INSERT的,可以认为ID为1,其他两个字段为空。
2.事务1更改该行的各字段的值
3.事务2修改该行的值
与事务1相同,此时undo log,中有有两行记录,并且通过回滚指针连在一起。
因此,如果undo log一直不删除,则会通过当前记录的回滚指针回溯到该行创建时的初始内容,所幸的时在Innodb中存在purge线程,它会查询那些比现在最老的活动事务还早的undo log,并删除它们,从而保证undo log文件不至于无限增长。
MVCC
undo log中的行就是MVCC中的多版本.
通过read view,进行版本可见性控制
事务链表
何时创建(build view)
RR级别下,事务中的第一个SELECT请求才开始创建read view;
RC级别下,事务中每次SELECT请求都会重新创建read view;
其由两部分组成 : 一是内存中的重做日志缓冲(redo log buffer),其是易失的;二是重做日志文件(redo log file),其是持久的.
日志条目基本格式
redo_log_type (1字节) | space (压缩后可能<4字节) | page_no | redo_log_body |
日志块
Redo log的存储都是以 块(block) 为单位进行存储的,每个块的大小为512字节。同磁盘扇区大小一致,可以保证块的写入是原子操作。
块由三部分所构成,分别是 日志块头(log block header),日志块尾(log block tailer),日志本身(日志条目集合)。日志头占用12字节,日志尾占用8字节。故每个块实际存储日志的大小为492字节。
日志组
一个日志文件由多个块所构成,多个日志文件形成一个重做日志文件组(redo log group)。不过,log group是一个逻辑上的概念,真实的磁盘上不会这样存储。
重做日志存储的就是之前在log buffer中保存的块,因此也是根据块的方式进行物理存储的管理。block=512bytes。 InnoDB存储引擎运行过程中, log buffer根据一定的规则将log block刷新到磁盘:
log block 写入追加到redo log file的最后部分,当一个redo log file写满时,会写入下一个redo log file。 这种方式:round-robin.看起来是顺序的,其实不然,除了保存log buffer刷新到磁盘的log block,还保存了一些其他信息,这些信息占:2KB,即redo log file 的前2KB不保存log block的信息。2KB的信息:保存 4 * 512字节的 块。
名称 | 大小(字节) |
log file header |
512 |
checkpoint1 |
512 |
空 |
512 |
checkpoint2 |
512 |
2KB的信息只在log group的第一个redo log file里存储,其余file留空.
当有一条记录需要更新的时候, InnoDB引擎就会先把记录写到redo log里面,并更新内存,这个时候更新就算完成了。同时, InnoDB引擎会在适当的时候,将这个操作记录更新到磁盘里面,而这个更新往往是在系统比较空闲的时候做。
InnoDB的redo log是固定大小的,比如可以配置为一组4个文件,每个文件的大小是1GB,那么这块“粉板”总共就可以记录4GB的操作。从头开始写,写到末尾就又回到开头循环写,如下面这个图所示。
write pos是当前记录的位置,一边写一边后移,写到第3号文件末尾后就回到0号文件开头。checkpoint是内存中的数据快照一份到磁盘,成功后之前的记录被擦除。write pos和checkpoint之间的是“粉板”上还空着的部分,可以用来记录新的操作。如果write pos追上checkpoint,表示“粉板”满了,这时候不能再执行新的更新,得停下来先擦掉一些记录,把checkpoint推进一下。有了redo log, InnoDB就可以保证即使数据库发生异常重启,之前提交的记录都不会丢失,这个能力称为crash-safe。
InnoDB存储引擎在启动时,不管上次数据库是否正常关闭,都会尝试进行恢复。重做日志是物理日志,恢复时比较快。
checkpoint 表示已经刷新到磁盘上的LSN。
例子:redo log file 记录的LSN:13000,刷新到磁盘上的LSN:10000,数据库在10000处宕机,恢复时,只需恢复10000~13000的部分。
MySQL整体来看,其实就有两块:一块是Server层,它主要做的是MySQL功能层面的事情;还有一块是引擎层,负责存储相关的具体事宜。上面redo log是InnoDB引擎特有的日志,而Server层也有自己的日志,称为binlog。binlog与redo这两种日志有以下三点不同:
想了解更多Binlog请查看《mysql的复制及备份》
2PC
当mysql开启binlog的时候,会存在一个内部XA的问题:事务在存储引擎层(redo)commit的顺序和在binlog中提交的顺序不一致的问题。2PC即innodb对于事务的两阶段提交机制。
第一阶段:InnoDB prepare,持有prepare_commit_mutex,并写入到redo log中。将回滚段(undo)设置为Prepared状态,binlog不做任何操作。
第二阶段:将事务写入Binlog中,将redo log中的对应事务打上commit标记,并释放prepare_commit_mutex。
MySQL以binlog的写入与否作为事务是否成功的标记,innodb引擎的redo commit标记并不是这个事务成功与否的标记。
崩溃时:
扫描最后一个Binlog文件,提取其中所有的xid。
InnoDB维持了状态为Prepare的事务链表,将这些事务的xid与刚刚提取的xid做比较,若存在,则提交prepare的事务,若不存在,回滚。
update语句update T set c=c+1 where ID=2;的具体流程:
1. 执行器先找引擎取ID=2这一行。 ID是主键,引擎直接用树搜索找到这一行。如果ID=2这一行所在的数据页本来就在内存中,就直接返回给执行器;否则,需要先从磁盘读入内存,然后再返回。
2. 执行器拿到引擎给的行数据,把这个值加上1,比如原来是N,现在就是N+1,得到新的一行数据,再调用引擎接口写入这行新数据。
3. 引擎将这行新数据更新到内存中,同时将这个更新操作记录到redo log里面,此时redo log处于prepare状态。然后告知执行器执行完成了,随时可以提交事务。
4. 执行器生成这个操作的binlog,并把binlog写入磁盘。
5. 执行器调用引擎的提交事务接口,引擎把刚刚写入的redo log改成提交( commit)状态,更新完成
redo log的写入拆成了两个步骤: prepare和commit,这就是"两阶段提交"。redo log再写入biglog后提交事务,原因是:
1. 先写redo log后写binlog。假设在redo log写完, binlog还没有写完的时候, MySQL进程异常重启。由于我们前面说过的, redo log写完之后,系统即使崩溃,仍然能够把数据恢复回来,所以恢复后这一行c的值是1。但是由于binlog没写完就crash了,这时候binlog里面就没有记录这个语句。因此,之后备份日志的时候,存起来的binlog里面就没有这条语句。然后你会发现,如果需要用这个binlog来恢复临时库的话,由于这个语句的binlog丢失,这个临时库就会少了这一次更新,恢复出来的这一行c的值就是0,与原库的值不同。
2. 先写binlog后写redo log。如果在binlog写完之后crash,由于redo log还没写,崩溃恢复以后这个事务无效,所以这一行c的值是0。但是binlog里面已经记录了“把c从0改成1”这个日志。所以,在之后用binlog来恢复的时候就多了一个事务出来,恢复出来的这一行c的值就是1,与原库的值不同。
redo log用于保证crash-safe能力。 innodb_flush_log_at_trx_commit这个参数设置成1的时候,表示每次事务的redo log都直接持久化到磁盘。这个参数我建议你设置成1,这样可以保证MySQL异常重启之后数据不丢失。sync_binlog这个参数设置成1的时候,表示每次事务的binlog都持久化到磁盘。这个参数我也建议你设置成1,这样可以保证MySQL异常重启之后binlog不丢失。