MySQL事务之-2

上一篇中我们提到了MySQL的事务特性,这一片主要讲述事务的实现。

事务的隔离性由锁来实现。原子性,一致性,持久性通过数据库的redo和undo log来实现。

redo恢复提交事务修改页的操作,而undo回滚行记录到某个特定版本。因此两者记录的内容不同,redo通常是物理日志,记录的是页的物理修改操作。undo是逻辑日志,根据每行记录进行记录。

数据库为了保证提交数据的持久性,做了很多努力;回想一下double write,首先把缓冲池中的脏页使用double write技术刷新到磁盘上;想象一种情况,在一个事务的过程中,数据库down掉了,然后数据库恢复的时候会发生什么?因为最后一条事务既没有提交也没有回滚,这时候数据库状态违反了事务的原子性,因此在进行crash recover的时候,需要判断事务是需要提交呢,还是回滚?因此我们引入了redo日志和undo日志。【感觉说的不太准确】

redo日志

重做日志由两部分组成;一是内存中的重做日志缓冲(redo_log buffer);二是重做日志文件,日志文件是持久的!

在事务进行提交时,必须首先将该事务的所有日志写入重做日志文件进行持久化,然后进行comit操作。在INNODB存储引擎中,事务日志由两部分组成redo log和undo log。redo log用来保证事务的持久性,undo log用来帮助事务回滚以及MVCC功能。

redo log基本上是顺序写的,在数据库运行时不需要对redo log进行读取操作,而undo log是需要进行随机读取的。

为了确保每次日志都写入重做日志文件,在每次讲重做日志缓冲写入重做日志文件后,INNODB存储引擎都会调用一次fsync操作。【在刷新的时候虽然开启了O_DIRECT选项,但是文件的元信息并不会改变,因此需要调用fsync函数】因此重做日志缓冲先写入文件系统缓存,为了确保重做日志写入磁盘,必须进行一次fsync操作。由于fsync操作的效率取决于磁盘的性能,因此磁盘的性能决定了事务提交的性能,也就是数据库的性能。

innodb_flush_log_at_trx_commit用来控制重做日志刷新到磁盘的策略。

innodb_flush_log_at_trx_commit:
取值如下:
1: 表示事务提交时必须调用一次fsync操作。
0: 表示事务提交时不进行写入重做日志操作,这个操作仅在master thread中完成,而master thread会每1秒进行一次fsync操作。
2:表示事务提交时将重做日志写入重做日志文件,但仅写入文件系统的缓存,不进行fsync操作,在这个设置下数据库宕机并不会导致事务丢失,但是服务器宕机会导致事务丢失

二进制日志和重做日志

在MySQL数据库中还有一种二进制日志,其用来进行PIT恢复以及主从复制环境的建立。

重做日志是从INNODB存储引擎层产生的,而二进制日志是MySQL数据库的上层产生的。【因为MySQL有不同的存储引擎,为了保证各个引擎的兼容(复制的时候),因此有了二进制日志】。

二进制日志是一种逻辑日志,其记录的是对应的SQL语句,而INNODB存储引擎的重做日志是物理格式,其记录的是对于每个行的修改。

还有就是写入的时间点不同,二进制日志是在事务提交的时候一次性写入的,而重做日志在事务进行中不断的被写入,重做日志并不是随事务的提交顺序进行写入的。

重做日志的内部组成

  重做日志都是以512字节的块进行保存的,称之为重做日志块(redo log block)。磁盘的每个扇区的大小也是512字节,因为重做日志的的写入是原子性的,不需要double write。

重做日志块除了日志本身以外,还有日志头和日志尾两部分组成。重做日志头一共占用12字节,重做日志尾占用8字节,因为每个重做日志块实际可以存储大小为492字节,

如图显示了重做日志缓存的结构,可以发现重做日志缓存由每个512字节大小的日志块组成。

log  block header的组成如下:

名称 占用字节
LOG_BLOCK_HDR_NO 4
LOG_BLOCK_HDR_DATA_LEN 2
LOG_BLOCK_FIRST_REC_GROUP 2
LOG_BLOCK_CHECKPOINT_NO 4

log buffer是由log block组成,在内部log buffer就好似一个数组,因此LOG_BLOCK_HDR_NO用来标记这个数组中的位置。其是递增并且循环使用的,占用4个字节,但是由于第一位用来判断是否是flush bit,所以最大的值为2G。(这个应该最大限制应该是MySQL5.5版本的限制,重做日志最大为4G,MySQL5.6之后最大为512G)

LOG_BLOCK_HDR_DATA_LEN: 表示log block所占用的大小。当log block被写满时,该值为0x200.

LOG_BLOCK_FIRST_REC_GROUP:表示第一个日志所在的偏移量。当一个新log block写入数据时,其偏移量为log block的头部大小为12字节。事务t1重做日志为762字节,事务t2的重做日志为100字节,那么需要两个log block,第一个日志日志块的偏移量为12字节,第二个日志块为(762-492+12)=280字节。

LOG_BLOCK_CHECKPOINT_NO:表示该log block最后被写入时的检查点,占用4字节。

日志尾只有一部分组成,其值和LOG_BLOCK_HDR_NO相同。

重做日志的大小管理

在默认的情况下,在INNODB存储引擎的数据目录下面有两个名为ib_logfile0和ib_logfile1的文件。每个INNODB存储引擎至少有1个重做日志文件组,每个文件组下至少有两个文件。为了提高可靠性,用户可以设置多个镜像组,将不同的文件组放在不同的磁盘上,一次提高重做日志的高可用性。在重做日志组中,每个重做日志文件的大小是一样的,并以循环的方式写入数据。

首先写入重做日志文件1,达到文件最后时,会切换到文件2,轮转循环写入数据。

#注意: MySQL5.7中没有镜像这个设置了
mysql> show variables like "innodb_mirrored_log_groups"; Empty set (0.01 sec) mysql> set innodb_mirrored_log_groups = 2; ERROR 1193 (HY000): Unknown system variable 'innodb_mirrored_log_groups'

重做日志参数设置如下:

innodb_log_file_size: 设定重做日志的大小。(总的大小,在只有1个重做日志组的时候,每个重做日志的大小=innodb_log_file_size/innodb_log_files_in_group)
innodb_log_files_in_group: 每个重做日志组有多少个重做日志文件
innodb_log_group_home_dir: 指定重做日志的位置,默认是在datadir指定的目录下面
innodb_log_buffer_size: log buffer的大小,默认是16

重做日志文件大小设置对于INNODB存储引擎的性能有着非常大的影响。一方面重做日志文件不能设置的太大,如果设置太大,在恢复时可能需要很长时间;另一方面又不能设置的太小,否则可能导致一个事务日志需要多次切换重做日志文件,频繁发生async checkpoin,导致性能抖动。

在INNODB存储引擎中,对于不同的操作有着不同的重做日志格式。虽然各种重做日志的类型不同,但是他们有着基本的格式,如下:

redo_log_type   space page_no redo_log_body

redo_log_type: 占用1字节,表示重做日志的类型。

space:表示表空间ID。

page_no:表示页便宜量。

redo_log_body: 表示每个重做日志的数据部分,恢复时需要调用相应的函数进行解析。

undo日志

 重做日志记录了事务的行为,可以很好地通过其对页进行“重做”操作。但是事务有时候还要进行回滚操作,这时就需要undo。对数据库进行修改时,INNODB存储引擎不但会产生redo,还会产生一定量的undo。

undo存放在数据库内部一个特殊段中,这个段称为undo段。undo段位于共享表空间内。

undo是逻辑日志,因此只是将数据库逻辑地恢复到原来的样子。所有修改都被逻辑地取消了,但是数据结构和页本身在回滚之后可能大不相同。

除了回滚操作,undo的另一个作用是MVCC,即非锁定一致性读。

undo存储管理:

INNODB存储引擎对undo的管理采用段的方式。在INNODB存储引擎中有rollback segment,每个回滚段记录了1024个undo log segment。而在每个undo log segmetn段中进行undo页的申请。共享表空间偏移量为5的页记录了所有rollback segment header所在的页,这个页类型为FILE_PAGE_TYPE_SYS。

在INNODB1.1版本之前只有一个rollback segment,因此同时支持最大在线事务数为1024.从1.1版本之后最大支持128个rollback segment,其中32个回滚段作为保留用于临时表事务的非重做回滚段,剩余的96个可用的回滚段,每个回滚段支持1023个在线事务的连接。

InnoDB supports 128 rollback segments, 32 of which are reserved as non-redo rollback segments for temporary table transactions. Each transaction 
that updates a temporary table (excluding read-only transactions) is assigned two rollback segments, one redo-enabled rollback segment and one
non-redo rollback segment. Read-only transactions are only assigned non-redo rollback segments, as read-only transactions are only permitted to
modify temporary tables. This leaves 96 available rollback segments, each of which supports up to 1023 concurrent data-modifying transactions, for a total limit of
approximately 96K concurrent data-modifying transactions. The 96K limit assumes that transactions do not modify temporary tables. If all
data-modifying transactions also modify temporary tables, the total limit is approximately 32K concurrent data modifying transactions.
For more information about rollback segments that are reserved for temporary table transactions, see Temporary Table Undo Logs.

详见官方文档: https://dev.mysql.com/doc/refman/5.7/en/innodb-undo-logs.html

 

在INNODB1.1(包含)之前回滚段都存在于共享表空间中,在INNODB1.2之后,可以通过参数对回滚段做进一步的设置。

mysql> show variables like "innodb%undo%";
+--------------------------+------------+
| Variable_name            | Value      |
+--------------------------+------------+
| innodb_max_undo_log_size | 1073741824 |        #最大回滚段的大小
| innodb_undo_directory    | ./         |        #回滚段的文件位置,默认在datadir指定的目录下面
| innodb_undo_log_truncate | OFF        |        #
| innodb_undo_logs         | 128        |        #支持的回滚段大小,默认是128个
| innodb_undo_tablespaces  | 0          |        #构成回滚段文件的数量,若设置,则文件名以undo开头
+--------------------------+------------+
5 rows in set (0

innodb_undo_log_truncate:回收undo表空间,要求MySQL数据库至少有两个undo表空间,一个保持活跃的状态,另一个处于被回收的状态。
超过innodb_max_undo_log_size定义大小的表空间将会被回收。MySQL5.7才加入的!


mysql> show global variables like '%truncate%';
+--------------------------------------+-------+
| Variable_name                        | Value |
+--------------------------------------+-------+
| innodb_purge_rseg_truncate_frequency | 128   |
| innodb_undo_log_truncate             | OFF   |
+--------------------------------------+-------+
2 rows in set (0.01 sec)

undo的truncate主要由下面两个参数控制:innodb_purge_rseg_truncate_frequency,innodb_undo_log_truncate。 
1. innodb_undo_log_truncate是开关参数。 
2. innodb_purge_rseg_truncate_frequency默认128,表示purge undo轮询128次后,进行一次undo的truncate。

当设置innodb_undo_log_truncate=ON的时候, undo表空间的文件大小,如果超过了innodb_max_undo_log_size, 就会被truncate到初始大小,但有一个前提,就是表空间中的undo不再被使用。

其主要步骤如下:
1. 超过大小了之后,会被mark truncation,一次会选择一个
2. 选择的undo不能再分配新给新的事务
3. purge线程清理不再需要的rollback segment
4. 等所有的回滚段都释放了后,truncate操作,使其成为install db时的初始状态。

默认情况下, 是purge触发128次之后,进行一次rollback segment的free操作,然后如果全部free就进行一个truncate。

但mark的操作需要几个依赖条件需要满足:
1. 系统至少得有两个undo表空间,防止一个offline后,至少另外一个还能工作
2. 除了ibdata里的segment,还至少有两个segment可用
3. undo表空间的大小确实超过了设置的阈值
MySQL5.7--truncate说明

事务在undo log segment分配页并写入undo log的这个过程同样需要写入重做日志。也就是undo会产生redo log。

当事务提交时,INNODB存储引擎会做以下两件事:

  • 将undo放入到列表中,供之后的purge操作。
  • 判断undo log所在页是否可以重用,若可以分配给下个事务使用。

事务提交后并不马上删除undo log及undo log所在的页。这是因为可能还有其他事务需要通过undo log来得到行记录之前的版本。故事务将undo log放入一个链表中,是否可以删除undo log以及undo log所在的页由purge线程决定。

事务提交时,将undo页放入链表中,然后判断给undo页的使用空间是否小于3/4,若是则表明该undo页可以重用,之后,新的undo log记录在当前undo log的后面。由于存放undo log的列表都是以记录进行组织的,而undo页可能存放着不同事务的undo log,因此purge线程操作需要涉及磁盘的离散读取操作,是一个比较缓慢的过程。

TRANSACTIONS
------------
Trx id counter 12041
Purge done for trx's n:o < 0 undo n:o < 0 state: running but idle
History list length 0                      #undo 页的大小
LIST OF TRANSACTIONS FOR EACH SESSION:
---TRANSACTION 421780424370000, not started
0 lock struct(s),

 

posted @ 2018-11-24 18:40  夜间独行的浪子  阅读(354)  评论(0编辑  收藏  举报