- 数据是什么
从不同的角度和层次来看,我们可以将数据库中的数据看作:
A. 关系数据
B. 元组或对象
C. 存在Page中的二进制序列
因此Log中也可以记录不同的内容:
- 物理的日志(Physical Log)
A. 记录完整的Page
B. 记录Page中被修改的部分(page中的偏移,内容和长度).
优点:因为恢复时,完全不依赖原页面上的内容,所以不要求持久化了的数据保持在一个一致的状态。
比如在写一个页面到磁盘上时,系统发生故障,页面上的一部数据写入了磁盘,另一部分丢失了。
这时仍然可以恢复出正确的数据。
缺点:Log记录的内容很多,占用很大的空间。如B-Tree的分裂操作,要记录约一个完整Page的内容。
- 逻辑的日志(Logical Log)
记录在关系(表)上的一个元组操作。
A. 插入一行记录。
B. 修改一行记录。
C. 删除一行记录。
逻辑日志比起物理的日志,显得简洁的多。而且占用的空间也要小的多。
但是逻辑日志有2个缺点:
A. 部分执行
例如:表T有2个索引,在向T插入1条记录时,需要分别向2个B-Tree中插入记录。
有可能第一个B-Tree插入成功了,但是第二个B-Tree没有插入成功。在恢复或
回滚时,需要处理这些特殊情况。
B. 操作的一致性问题
一个插入操作有一个B-Tree的分裂,页A的一半数据移到了B页,A页写入了磁盘,B页没有写入磁盘。
如果这时候发生了故障,需要进行恢复,逻辑日志是很难搞定的。
逻辑的日志上的‘部分执行’的问题是比较好维护的,但是‘一致性’的问题维护起来是很复杂的。
- 物理和逻辑结合的日志(Physiological Log)
这种日志将物理和逻辑日志相结合,取其利,去其害。从而达到一个相对更好的一个状态。这种日志有2个特点:
A. 物理到page. 将操作细分到页级别。为每个页上的操作单独记日志。
比如,一个Insert分别在2个B-Tree的节点上做了插入操作,那么就分别为每一个页的操作记录一条日志。
B. Page内采用逻辑的日志。比如对一个B-Tree的页内插入一条记录时,物理上来说要修改Page Header的
内容(如,页内的记录数要加1),要插入一行数据到某个位置,要修改相邻记录里的链表指针,要修改Slot的
属性等。从逻辑上来说,就是在这个页内插入了一行记录。因此Page内的逻辑日志只记录:’这是一个
插入操作’和’这行数据的内容‘。
MySQL数据库InnoDB存储引擎的Redo Log 记录的就是这种物理和逻辑相结合的日志。
使用页内的逻辑日志,可以减少日志占用的空间。但是它毕竟还是逻辑日志,上面提到的2个问题能够避免吗?
A. 页面内的部分执行的情况可以认为不存在了。因为整个页面的操作是原子操作,在完成之前是不会写
到磁盘上的。
B. 操作一致性的问题仍然存在。如果在写一个Page到磁盘时发生了故障,可能导致Page Header的记
录数被加1了,但是数据没有刷新到磁盘上,总之页面上的数据不一致了。
好在这个问题被缩小到了一个页面的范围内,因此比较容易解决。InnoDB存储引擎中用Double Write的方法
来解决这个问题。
- Double Write
Double Write的思路很简单:
A. 在覆盖磁盘上的数据前,先将Page的内容写入到磁盘上的其他地方(InnoDB存储引擎中的doublewrite
buffer,这里的buffer不是内存空间,是持久存储上的空间).
B. 然后再将Page的内容覆盖到磁盘上原来的数据。
如果在A步骤时系统故障,原来的数据没有被覆盖,还是完整的。
如果在B步骤时系统故障,原来的数据不完整了,但是新数据已经被完整的写入了doublewrite buffer.
因此系统恢复时就可以用doublewrite buffer中的新Page来覆盖这个不完整的page.
Double write 显然会曾加磁盘的IO。直觉上IO次数增加了1倍,但是性能损失并不是很大。Peter在
innodb-double-write中说性能损失不超过5-10%。应该是因为多数情况下使用了批量写入的缘故。
A. Double write buffer是一段连续的存储空间,可以顺序写入。
B. Double write有自己的写buffer.
C. 先将多个要做doublewrite的page写入内存的buffer,然后再一起写到磁盘上。
代码在:buf0dblwr.cc
buf_flush_write_block_low()调用
buf_dblwr_write_single_page()或 buf_dblwr_add_to_batch()来实现doublewrite.
- Checksum
检测页面是否一致的功能是靠Checksum来完成的,每个页面修改完成后都会记算一个页面的checksum。
这个checksum存放在页面的尾部.每次从磁盘读一个页到内存时,都需要检测页的一致性。
函数buf_page_is_corrupted()是用来检测page的一致性的.
- InnoDB Redo Log的日志类型
InnoDB redo log的格式可以概括为:
<Space ID>+<Page NO.>+<操作类型>+<数据>.
Redo Log记录的页面操作大致可以分为以下几种类型:
A. 在页面上写入N个字节的内容,这些可以看作是物理的Log.
MLOG_1BYTE, MLOG_2BYTES, MLOG_4BYTES, MLOG_8BYTES, MLOG_WRITE_STRING
各种Page链表的指针修改,以及文件头,段页等的内容的修改都是以这种方式记录的日志。
B. 页面上的记录操作。
MLOG_REC_*, MLOG_LIST_*, MLOG_COMP_REC_*, MLOG_COMP_LIST_*
这些日志记录了对B-Tree页的INSER, DELETE, UPDATE操作和分裂合并操作。
C. 文件和Page操作
MLOG_FILE_CREATE, MLOG_FILE_RENAME, MLOG_FILE_DELETE,
MLOG_PAGE_CREATE, MLOG_INIT_FILE_PAGE, MLOG_PAGE_REORGANIZE
D. Undo Log操作
MLOG_UNDO_*
InnoDB中将undo log的操作也记入了redo log. 为什么要这样做,在前面‘恢复’已经说了.
这里只提到了部分Redo Log的类型,完整的定义在mtr0mtr.h文件中. 通过这个类型的定义,可以
很容易的找到都在哪些地方使用了。
虽说Redo Log将数据的操作细分到了页面级别。但是有些在多个页面上的操作是逻辑上不可分裂的。
比如B-Tree的分裂操作,对父节点和2个子节点的修改。当进行恢复时,要么全部恢复,要么全部不
恢复,不能只恢复其中的部分页面。InnoDB中通过mini-transaction(MTR)来保证这些不可再分
的操作的原子性。
- InnoDB Undo Log的日志类型
MySQL数据库InnoDB存储引擎的undo log采用了逻辑的日志。
InnoDB undo log的格式可以概括为:<操作类型>+<Table ID>+<数据>.
A. 从表中删除一行记录
TRX_UNDO_DEL_MARK_REC(将主键记入日志)
在删除一条记录时,并不是真正的将数据从数据库中删除,只是标记为已删除.这样做的好处是
Undo Log中不用记录整行的信息.在undo时操作也变得很简单.
B. 向表中插入一行记录
TRX_UNDO_INSERT_REC(将主键记入日志)
TRX_UNDO_UPD_DEL_REC(仅将主键记入日志) 当表中有一条被标记为删除的记录和要插入的
数据主键相同时, 实际的操作是更新这个被标记为删除的记录。
C. 更新表中的一条记录
TRX_UNDO_UPD_EXIST_REC(将主键和被更新了的字段内容记入日志)
TRX_UNDO_DEL_MARK_REC和TRX_UNDO_INSERT_REC,当更新主键字段时,实际执行的过程
是删除旧的记录然后,再插入一条新的记录。
因为undo log还要被MVCC和Purge使用,所以还有TRX_ID和DATA_ROLL_PTR等特殊的内容记录
在日志中。TRX_UNDO_INSERT_REC不需要记录这些内容.因为MVCC中不可内引用一个不存在的数据。
这也是事务将insert和update、delete的undo log分开存放的原因。事务提交后,insert的undo
占用的空间就可以立即释放了.
这些类型定义在:trx0rec.h.
记录日志的过程在:trx_undo_page_report_insert()和trx_undo_page_report_modify()中。
Undo操作在row0undo.c, row0uins.c和row0umod.c中, 入口函数是row_undo().
- 逻辑日志的一致性问题
前面说了逻辑日志的一致性问题是很复杂的,为什么undo log要用逻辑日志呢?
因为redo log使用了physiological日志和MTR,就可以保证在恢复时重做完redo log后,
数据是一致。在执行undo时,就不必考虑这个问题了。
理论上来说,如果MySQL数据库InnoDB存储引擎的buffer足够大,就不需要将数据本身持久化。将全部的redo log重新执行一遍
就可以恢复所有的数据。但是随着时间的积累,Redo Log会变的很大很大。如果每次都从第一条记
录开始恢复,恢复的过程就会很慢,从而无法被容忍。为了减少恢复的时间,就引入了Checkpoint机制。
- 脏页(dirty page)
如果一个数据页在内存中修改了,但是还没有刷新到磁盘。这个数据页就称作脏页。
- 日志顺序号(Log Sequence Number)
LSN是日志空间中每条日志的结束点,用字节偏移量来表示。在Checkpoint和恢复时使用。
- 原理
假设在某个时间点,所有的脏页都被刷新到了磁盘上.这个时间点之前的所有Redo Log就不需要重
做了。系统记录下这个时间点时redo log的结尾位置作为checkpoint. 在进行恢复时,从这个
checkpoint的位置开始即可。Checkpoint点之前的日志也就不再需要了,可以被删除掉。为了
更好的利用日志空间,InnoDB以环形缓存(circular buffer)的方式来使用日志空间。
- Sharp Checkpoint
对于繁忙的系统来说,很少会出现这样的的一个时间点。为了能创造出这样一个时间点,最简单的办
法就是,在某个时间开始停止一切更新操作,直到所有的脏页被刷新到磁盘,Checkpoint被记录。
显然对于繁忙的系统, 这种方法是不合适的。能不能在checkpoint时不停止用户的操作呢?
- Fuzzy Checkpoint
如下图所示,如果刷脏页的同时用户还在更新数据,LSN1前的某个脏页在刷到持久存储之前就有可能被
LSN1之后的某个操作给修改了。当checkpoint完成时,LSN1后的部分操作(R1,R2对应的操作)也被
持久化了。当Sharp checkpoint完成时,持久存储中存储的数据是某个确切时间点的内存数据的快照。
Fuzzy checkpoint完成时,持久存储中存储的数据不是某个确切时间点的内存数据的快照。从某种
程度上,可以说持久存储中的数据丧失了一致性。在恢复时,必须要解决这个问题。
- 幂等(Idempotence)规则
如上图所示,checkpoint 在LSN1位置,当checkpoint完成时R1,R2对应的修改也被刷到了持久存储。
恢复时要从LSN1位置开始,包括R1, R2在内。重新执行后,数据还能正确吗?
幂等规则要求无论redo log被执行了多少次,数据始终正确。
InnoDB的redo log, 物理到Page,Page内是逻辑日志。
物理日志,天然支持幂等规则. 但是逻辑日志 需要特殊处理,才能支持满足幂等规则。
- 数据页的最新(最大)LSN
为了满足幂等规则,InnoDB中每个数据页上都记录有一个LSN。每次更新数据页时,将LSN修改为
当前操作的redo log的LSN。在恢复时,如果数据页的LSN大于等于当前redo log的LSN,则跳过此
日志。
- 异步Checkpoint
实现了幂等规则后,脏页就可以在任何时间,以任何顺序写入持久存储了。InnoDB的buffer pool有
一套单独的机制来刷脏页。因此很多情况下checkpoint时,并不写脏页到存储。只是将所有脏页的
最小的LSN记做checkpoint.
checkpoint的实现在log0log.c.
log_checkpoint()实现异步checkpoint.
- 同步Checkpoint
InnoDB的buffer pool通过LRU的算法来决定哪些脏页应该被写入持久存储。如果包含最小LSN的
页面频繁的被更新,它就不会被刷到存储上。这样就可能导致checkpoint点很长一段时间无法前进,
甚至导致日志空间被占满。这时就要按照LSN由最小到大的顺序写一部分脏页到持久存储。
log_checkpoint_margin().
log_calc_max_ages()用来计算,‘判断是否要执行同步checkpoint’用到的参数.
05 – 缓存池(Buffer Pool)
学习到这里,我更倾向于说这是一个”Redo+Undo+Buffer”的模式。为了提搞IO性能,脏页缓存在buffer中,
Redo log也要先缓存在内存中,doublewrite也有内存buffer. Buffer pool在这个模式中是至关重要的。
- 页分类
Buffer pool内的页分为三种:
A. 未被使用的页(空白的buffer),没有映射到一个数据文件中页。
B. 净页,映射到了一个数据文件页,而且没有被修改过。内容和数据文件的页一样。
C. 脏页,映射到了一个数据文件页,并且数据被修改过。内容和数据文件的页不一样。
- LRU
InnoDB维护了两个LRU列表。当空间不足时,用来决定哪些脏页应该被首先写入磁盘,哪些净页应该被释放掉。
A. buffer_pool->LRU,普通LRU链表,记录所有数据缓冲页。
B. buffer_pool->unzip_LRU,是压缩页(row_format=compressed)解压后数据缓冲页LRU链表。
LUR链表中的页面按最近一次的访问的时间顺序排列,头部是最后一次被访问的页面,尾部是最早一次被
访问的页面。无论是读还是写一个页面上的数据,都要先获取这个页面。因此可以在获取页面时,维护
LRU链表.当获取一个页面后,将其放到LRU链表的头部即可。
buf_page_get_gen()和buf_page_get_zip()用来获取一个页面,他们调用
buf_unzip_LRU_add_block()和buf_page_set_accessed_make_young()来维护LRU链表。
- flush_list
同步checkpoint时,需要根据数据页修改的先后顺序来将脏页写入持久存储。因此除了LRU链表,
buffer pool中还有一个按脏页修改先后顺序排列的链表,叫flush_list.当需要同步checkpoint时,
根据flush_list中页的顺序刷数据到持久存储。
A. 一个页只在flush_list中出现1次,因为一个页面只需要写一次。
B. 按页面最早一次被修改的顺序排列。
06 – Mini-Transaction(MTR)
前面提到Redo Log将数据的操作细分到了页面级别。但是有些在多个页面上的操作是逻辑上不可分裂的。
InnoDB中用Mini-Transaction来表示这些不可再细分的逻辑操作。
- MTR的一致性
为了满足MTR的一致性,MTR做了如下的设计:
A. MTR的所有日志被封装在一起,当MTR提交时一起写入redo log buffer.
这样做有2个好处:
* 减少并发MTR对redo log buffer 的竞争。
* 连续的存储在一起,恢复时的处理过程更简单。
B. InnoDB在redo log的层面,将一个MTR中的所有日志作为Redo log的最小单元。在恢复时,一个MTR
中的所有日志必须是完整的才能进行恢复。
- MTR日志的封装
为了在日志文件中区分不同的MTR,MTR将MLOG_SINGLE_REC_FLAG或MLOG_MULTI_REC_END写入
redo log(mtr_log_reserve_and_write()).
A. 如果MTR的日志中只有一行记录,在日志的开始处添加MLOG_SINGLE_REC_FLAG,表示MTR中只有
一条记录。
B. 如果MTR的日志中有多行记录,在日志的结尾处添加一个类型为MLOG_MULTI_REC_END的日志,
代表MTR的日志到此结束.
- MTR的LSN
A. 因为在将日志写入redo log buffer时,才能获得LSN。所以修改数据时,并没有修改页上的LSN。
需要在MTR获得LSN后统一修改。
B. 一个MTR只有一个LSN. 一个MTR内修改的所有页的LSN相同。这样checkpoint就不会出现在MTR的中间。
C. 在获得LSN后,如果被MTR修改的脏页不在buffer pool的flush_list里,就会被添加进去。
看mtr_memo_slot_note_modification()和buf_flush_note_modification().
- 页级锁
提交时才写日志到redo log的做法,决定了MTR要使用页级锁。
A. 一个页面不能同时被多个活动的MTR修改。
B. MTR中数据页的锁,直到MTR提交时(日志写入redo log buffer)后才释放。
锁对象存储在mtr的memo中。调用mtr_s_lock和mtr_x_lock来加锁时,锁对象被保存到memo中。
解锁在mtr_memo_slot_release()中完成。
- MTR的ROLLBACK
看完MTR的代码发现mtr没有记录undo日志,也不能rollback. MTR都是很小的操作单元,而且每个MTR
都有明确的操作目标,因此比较容易保证其正确性。
A. 因为页面操作是在内存中完成,并且页面有固定的格式,因此很多的页面操作是不会失败的。
InnoDB存储引擎中的很多写页面的函数都没有返回值.
B. 在对任何页面操作前,先要检查是否可能发生错误。如果可能发生错误就不能往下执行。
如,当插入一行记录到B-Tree的节点时,首先检查页面有足够的空间。
C. 使用更大粒度的锁(如B-Tree的锁),并且按照一定的顺序加锁。这样才能不导致死锁问题。
以上是自己看代码后的大概印象,不一定说到了正点上。MTR模块的代码虽简单,但是MTR在其他模块大量的
使用。要透彻的理解MTR,估计还得要看其他模块的代码,整理出来大部分MTR操作过程才行.
06 – 参考
A. Database Systems: The Complete Book (2nd Edition)
B. Transaction Processing: Concepts and Techniques
C. how-innodb-performs-a-checkpoint
D. InnoDB fuzzy checkpoints
E. Heikki Tuuri Innodb answers – Part I
F. Heikki Tuuri Innodb answers – Part II