MySQL事务日志
- 事务的隔离性由锁机制实现
- 事务的原子性、一致性、持久性由事务的redo 日志和undo日志实现
- redo 日志:被称为重做日志,提供再写入操作,恢复提交事务修改的页操作,用来保证事务的持久性。记录的是“物理级别”的页修改操作,比如页号xxx、偏移量yyy写入了’zzz’数据。主要为了保证数据的可靠性。
- undo 日志:被称为回滚日志,回滚行记录到某个特定版本,用来保证事务的原子性、一致性。记录的是逻辑操作日志,是与实际操作相反的一条记录(列如实际操作是insert,那么记录的就是delete),主要用于事务的回滚和一致性非锁定读。
redo日志
为什么需要redo日志
InnoDB是以页为单位来管理存储空间的。在真正访问数据页之前,需要将物理磁盘上的页缓存到内存中的buffer pool之后才可以访问。所有的变更都必须先更新buffer pool中的数据,然后缓冲池中的脏页会以一定的频率被刷入磁盘(checkpoint机制),通过缓冲池来优化cpu和磁盘之间的鸿沟,这样可以保证效率不会下降的太快。
而脏页刷入磁盘并不是每次变更都会触发的,而是master线程隔一段时间处理一次,所以就可能出现在两次刷盘的间隔中,服务器就宕机了,那么写入缓冲池的数据就丢失了,但是事务是具有持久性的,事务提交的数据变更操作即使是服务器宕机也不应该丢失,所以需要redo日志记录事务的变更操作。
当然也可以不使用redo日志,在每次事务提交时把该事务修改的页刷新到磁盘中,这是一种简单粗暴的做法会有以下问题出现:
- 修改量和刷新磁盘工作量不成比例:有的时候一个事务可能仅仅只是更新某一条数据的某一个字段,但是刷盘时InnoDB还是以页为单位来进行磁盘IO的,也就是说哪怕只更新了一个很小的数据,刷盘时也是需要将完整的一个页从内存刷新到磁盘中,为了可能只有一两个字节的修改就刷新16KB的数据到磁盘上显然有些小题大作
- 随机IO刷新较慢:一个事务可能包含多个语句,一条语句也可能修改多个数据页中的数据,如果这些数据页并不相邻的话,那么将这个事务修改的数据刷新到磁盘上就需要进行很多的随机IO,而相对顺序IO来说,随机IO就要慢很多
而相比之下另外一种解决思路效率就比较高了:我们只是想让已经提交的事务对数据库中数据所做的修改永久生效,即使系统崩溃,在重启后也能恢复这种修改。所以我们并不需要在每次事务提交时就把数据页刷新到磁盘上,只要记录一下修改了哪些东西就好了。InnoDB采用了WAL技术(Write-Ahead Logging),这种技术的思想就是先写日志,再写磁盘,只有日志写入成功,才算事务提交成功,当出现服务器宕机一些数据未刷新到磁盘时,就可以通过写入的日志来恢复,保证事务的持久性。
redo日志的好处、特点
1.好处
- redo日志降低了刷盘频率
- redo日志占用的空间非常小
2.特点
- redo日志是顺序写入磁盘的(执行事务的过程中,每执行一条语句,就可能产生若干条redo日志,这些日志是按照产生的顺序写入磁盘的,也就是顺序IO)
- 事务执行过程中,redo日志不断记录
日志类型 | 产生位置 | 记录时间 |
---|---|---|
redo log | 存储引擎层 | 事务执行过程中,不断的记录 |
bin log | 数据层 | 事务提交后,一次性写入 |
redo log的结构
redo log由两个部分组成
- 重做日志的缓冲(redo log buffer):保存在内存中,是服务器启动时向操作系统申请的一片连续内存空间,容易丢失,可以通过innodb_log_buffer_size来设置大小,默认时16M,最大是4096M,最小是1M。这片空间被划分成若干个连续的redo log block,每个redo log block占用512字节。
- 重做日志文件(redo log file):保存在硬盘中,是持久的,可以通过innodb_log_file_size(默认值为48M)来设置单个redo日志文件的大小,再通过innodb_log_file_in_group来设置redo日志的个数(默认是2个,最大是100),innodb_log_file_size*innodb_log_file_in_group的最大值不能超过512G
redo的整体流程
- 将原始数据从磁盘读取到内存中,修改数据的内存拷贝
- 生成一条重做日志并写入redo log buffer,记录的是数据被修改后的值
- 当事务提交时,将redo log buffer中的内容刷新到redo log file,对redo log file 采用追加写的方式
- 定期将内存中修改的数据刷新到磁盘中
redo log buffer刷新到redo log file的策略
InnoDB针对redo log buffer刷新到redo log file提供了三种刷新策略,可以通过innodb_flush_at_trx_commmit参数来设置:
- 设置为0:表示每次事务提交时不进行刷盘操作。(系统默认master线程每隔1s进行一次重做日志的同步)
- 设置为1:也是默认值,表示每次事务提交时都将进行同步。(系统master线程依然会每隔1s进行一次重做日志的同步)
- 设置为2:表示每次事务提交时都只把redo log buffer内容写入page cache(现代操作系统针对文件写入效率做的优化),不进行同步,由操作系统决定什么时候同步到磁盘,如果系统宕机则可能丢失数据(系统master线程依然会每隔1s进行一次重做日志的同步)
undo日志
undo日志是对事务原子性的保证。所以在事务更新数据之前的操作就是写入一个undo log。事务的操作需要保证要么全部完成,要么什么也不做,但是有时候事务执行到一半时会出现一些情况:
-
情况一:事务执行过程中可能出现一些错误,比如服务器错误、操作系统错误,甚至是突然断电导致的错误。
-
情况二:程序员手动输入rollback结束当前事务的执行
在上述情况下,我们需要将数据回滚到事务执行前的样子,这样就可以保证事务要么全部成功,要么什么也不做,符合事务的原子性要求。
当事务更新数据时,需要将对数据的操作记录下来,比如: -
insert:插入一条记录时,至少要记录这条记录的主键,之后回滚时只需要把这个主键对应的记录删除即可
-
delete:至少需要把这条记录的所有内容记下来,回滚时再把这些内容组成一条记录插入到表中
-
update:至少要把修改前的旧值都记录下来,回滚时把这条记录更新为旧值即可。
undo日志的作用
- 回滚数据:是从逻辑上执行事务更新数据的逆向操作,完成数据的恢复,并不是直接物理恢复
- MVCC:当用户读取一行记录时,若该记录已经被其他事务占用,当前事务可以通过undo日志读取之前的行版本信息,以此实现非锁定读取
undo的类型
在InnoDB中,undo log可以分为:
- insert undo log:指在insert操作时产生的undo log。因为insert操作的记录,只对当前事务可见,对其他事务不可见,所以这种类型的undo log在事务提交后就可以删除了。
- update undo log:指事务在update或delete时产生的undo log。这种undo log可能需要提供MVCC机制,用于让其他事务读取之前的行版本信息,所以不能在事务提交后删除。需要交由特定的线程去处理。
undo log的生命周期
undo log的生成过程
对于InnoDB来说,每个行记录除了记录本身的数据之外,还有几个隐藏列:
- DB_ROW_ID:如果表没有显式的指定主键,且没有唯一约束的列,那么InnoDB会自动为表添加一个row_id的隐藏列作为主键
- DB_TRX_ID:每个事务都会分配一个事务id,当事务对某条数据进行更新操作时,就会将这个事务的事务ID写入其中
- DB_ROLL_PTR:回滚指针确实是指向undo日志的指针。当需要回滚某个事务时,InnoDB会根据这些回滚指针来找到相应的undo日志,然后执行相反的操作,从而还原数据到之前的状态
举例,向表A指向三条语句时:
- insert into A(id, name) values(1, ‘a’):首先,会生成一条 insert 类型的undo日志(undo log1)。该undo日志记录了undo日志的序号、插入数据的主键列和值。同时,在插入的数据中,DB_TRX_ID 的位置记录了插入当前数据的事务的事务ID,而 DB_ROLL_PTR 的位置指向了这条新产生的 insert undo日志。
- update A set name = ‘bbb’ where id = 1:这会产生一条 update 类型的undo日志(undo log2)。对于update操作,会记录被更新字段的旧值。被修改的记录的 DB_TRX_ID 位置记录了更新当前数据的事务的事务ID,而 DB_ROLL_PTR 的位置指向了这条新产生的 update undo日志。此外,update undo日志还记录了之前的 DB_ROLL_PTR 的值(delete 操作类似)。
- update A set id = 2 where id = 1:首先,将原来数据的 delete_mask 设为1。在此之前,先记录一条 update 类型的undo日志(undo log3)。然后,在后面插入一条数据,新的数据也会产生一条 insert 类型的undo日志(undo log4)。
undo log是如何回滚的
假设对上述三条语句进行回滚
- 根据最后生成的insert undo log undo log4 将id = 2 的数据删除
- 再根据undo log3 将id=1的数据的delete_mask还原成0
- 根据undo log2 将id=1的数据的name还原成’a’
- 根据undo log1 将id=1的数据删除
undo log的删除
- insert undo log:insert操作的记录只有当前事务可见,所以在事务提交后,insert undo log即可删除
- update undo log:可能需要提供MVCC机制,因此事务提交时不能直接删除。需要交由特定的线程去处理。
小结
完整的梳理一下事务进行更新操作时,数据库完整的流程:
- 发起更新操作:这是用户或应用程序发起的数据库操作,例如插入、更新或删除数据。
- 判断Buffer Pool是否有数据,没有则加载:Buffer Pool是内存中的缓存区域,用于存储数据库表的数据页。如果需要更新的数据不在Buffer Pool中,数据库会从磁盘加载相应的数据页到Buffer Pool中,以便进行后续的操作。
- 记录Undo Log:在执行更新之前,数据库会先记录一条Undo Log。Undo Log用于回滚操作,以确保在事务回滚时可以还原数据到之前的状态。它包含了旧值、事务ID等信息。
- 更新Buffer Pool中的数据:现在数据库已经有了需要更新的数据,它会在Buffer Pool中直接修改相应的数据页。
- 向Redo Log Buffer写入Redo Log:Redo Log用于持久化事务的修改。在更新Buffer Pool中的数据后,数据库会将相应的Redo Log记录写入Redo Log Buffer。
- 写Redo Log到Redo Log文件:Redo Log Buffer中的Redo Log记录会定期刷新到磁盘上的Redo Log文件。这样即使数据库崩溃,也可以通过Redo Log还原数据。
- 生成Bin Log:Bin Log(二进制日志)用于复制、备份和恢复。数据库会生成Bin Log,记录所有的数据更改操作,以便其他服务器可以复制这些操作。
- 固定频率将Buffer Pool刷新到磁盘:为了保证数据的持久性,数据库会定期将Buffer Pool中的数据刷新到磁盘上的数据文件。