Mysql的日志系统


在上文的基础架构介绍中,我们明白了一条查询语句是如何工作的,接下来,我们将探究一条更新语句是如何工作的

先从一个表的一条更新语句来看:

create table T(ID int primary key,c int)

如果要将ID = 2这一行的值加1,SQL语句如下:

update T
set ID = ID+1
where T.ID = 2;

接下来我们思考更新语句会如何工作

首先可以明确的是,更新语句同样会将上面的流程走一遍

  • 连接器:负责客户端与主机之间的连接
  • 查询缓存:当一个表上有更新的时候,这时候表T上的查询缓存就会被清空,跟这个表有关的查询缓存都会失效。
  • 分析器:分析器会通过语法解析和词法解析,知道了用户将要执行一条更新语句
  • 优化器:优化器会拿到分析之后的结果,决定走ID这个索引
  • 执行器:负责执行操作,找到这一行,然后更新

与查询流程不一样的是,更新操作还涉及到两个重要的日志模块,redo log(重做日志)binlog(归档日志)

1. 重要的日志模块:redo log

这里作者举了一个比较生动的例子

将Mysql比作是一个酒店,用户就是一条条的更新语句,用户在数据库中的操作就是赊账和还账,用户的每次操作掌柜都要把这些操作记到账本里面去,但是如果账本挤不下了,怎么办呢?

  • 一种做法是直接把账本翻出来,算好之后再记进去
  • 一种是先拿一块临时黑板,将账记下来,省得去找了,等人没那么多的时候一个个算账

明显的,第二种方案是比较好的,因为这时候数据库的压力已经很大了,如果还去做遍历查询,只会导致前台的请求挤压越来越多,最终导致严重的性能下降。

对于Mysql而言,如果每一次的更新操作都需要写入磁盘,然后磁盘也要找到对应的那条数据,然后再更新,整个过程的I/O成本、查找成本都会很高。为了解决这个问题,第二种方案的类似思路就被设计出来了。

在我们的例子中,黑板和账本的配合过程就是WAL(Writing-Ahead Logging)技术,它的关键点就是先写日志,再写磁盘,对应的例子就是先写黑板,等压力没有那么大的时候我再写账本。


具体来说,当有一条记录需要更新的时候,InnoDB引擎就会先把记录写到redo log里面去,并更新内存,这个时候更新就算完成了。同时,InnoDB引擎就会在适当的时候,将这个操作记录更新到磁盘中去。

  • redo log是一个特定的区域,写入操作是顺序写,非常快,而将数据刷新到磁盘是随机IO,也就是要先查询再写入,这个过程查询时间耗费性能。所以要先写入redo log
  • 更新内存——–这个动作是将当前更新内容更新到内存中,如果更新内容在内存中不存在就会涉及到先读入内存,在更新的操作。后面引入了change buffer时,你会发现存在与不存在内存都不用读磁盘,用change buffer解决,减少了读磁盘IO的操作,提高了性能 3、空闲的时候刷磁盘——–这个动作叫 ”刷脏“,其实就是将数据页更新到磁盘的过程。

同时需要注意的是,InnoDBredo log是固定大小的,其形式是一个循环数组,比如配置为一组4个文件,每个文件的大小都是1GB,那么redo log就可以记录4GB的操作,从头开始写,写到末尾就又回到开头循环写。

write pos:是当前记录的位置,一边写一边后移,写到第三号文件末尾后就回到0号文件开头。

check point:是当前要擦除的位置,也是往后推移并且循环的,擦除记录前要把记录更新到数据文件。

图中绿色的部分:write pos和check point之间的位置是空闲的位置,可以用来记录新的操作。如果发现write poscheck point相遇,这时候就不能再执行新的更新了,这时候就需要停下来,check point向前推进一部分距离,才能继续更新。

其实这就是一个循环队列。

有了redo log,InnoDB就可以保证即使数据库发生异常重启,之前所提交的记录都不会丢失,这个能力称为crash-safe

2. 重要的日志模块:binlog

Mysql从整体来讲,其实就是两块

  • server层:完成Mysql功能层面上的事情。
  • 引擎层:负责存储的相关事宜。

redo log InnoDB引擎所特有的日志,Server层也有自己的日志,称之为binlog

MySql自带的引擎是MyiSAM,但是MyISAM没有crash-safe的能力,binlog日志只能用于归档,而InnoDB是另外一个公司以插件的形式引入Mysql 的。

因为binlog中没有checkPoint机制,因此没有仅依靠binlog没有crash-safe能力

不考虑mysql现有的实现,假如现在重新设计mysql,只用一个binlog是否可以实现cash_safe能力呢?答案是可以的,只不过binlog中也要加入checkpoint,数据库故障重启后,binlog checkpoint之后的sql都重放一遍。但是这样做让binlog耦合的功能太多。

这一个观点非常重要!因为在学习数据库原理的时候,我们遇到过的那个update log record,它实际上就是这个binlog,数据库原理在实现这个binlog的是实现了checkPoint的,因此可以实现crash-safe

binlog和redolog本质上都是日志记录,它会将mysql的操作记录下来,它们有以下的不同

  • redo logInnoDB引擎所特有的,bin logMysql的Server层实现的,所有引擎都可以使用
  • redo log是物理日志,记录的是在某个数据页上做了什么修改,binlog是逻辑日志,记录的是这个语句的原始逻辑,比如给ID = 2这一行字段+1
  • redo log是循环写的,空间固定会用完,而bin log是可以追加写入的,追加写是指bin log文件写到一定程度的大小后会切换到下一个,但是它不会记录哪些操作已经被写入到持久化设备中。

执行update语句时的内部流程

  • 执行器先找引擎取ID = 2这一行,ID是主键,引擎直接用树搜索找到这一行,如果ID = 2这一行所在的数据本来就在内存中,就直接返回给执行器,否则,需要先从磁盘中调入内存,然后再返回。
  • 执行器拿到引擎给的行数据,把这个值加上1,得到新的一行数据,将这一行交还给引擎,调用接口写入这行新的数据
  • 引擎将这行新数据更新到内存中,同时将这个更新记录更新到redo log里面,此时redo log处于prepare状态,然后告知执行器执行完毕了,随时可以提交事务。
  • 执行器生成这个操作的bin log,并把bin log写入磁盘
  • 执行器调用引擎的提交事务的接口,引擎把刚刚写入的redo log改成提交commit状态,更新完成。

其中,浅色的框代表的是在InnoDB内部执行的,而深色框代表的是在执行器内部执行的。

值得注意的是,redo log的写入拆成了两个步骤:

  • prepare
  • commit

分为了这两个阶段,这就是两阶段提交

3. 两阶段提交

两阶段提交指的是redo log在写入的时候,需要先经过写入,然后等待某些事件发生,最后集中提交事务的这个过程。这样做的目的是为了让两份日志的逻辑保持一致

bin log会记录所有逻辑操作,并且是采用追加写的形式,如果说一个系统具有恢复半个月内的所有数据库状态的能力,那么也就意味着这个系统保存着这半个月所有的bin log,同时系统会定期做整库备份。

当需要恢复到指定的某一秒的时候,需要找回数据的时候,可以这样做:

  • 首先,找到最近的一次全量备份,从这个备份恢复到临时库
  • 然后从备份的时间点开始,将备份的bin log依次取出来,重放到指定的时刻。

为什么需要两阶段提交?

我们以下面的语句为例:

update T
set ID = ID+1
where T.ID = 2;

原本字段c的值是0,假设在update的语句过程中,写完第一个log还没来得及写入第二个log的时候,这时候抛出exception,程序crash掉了:

由于redo logbin log是两个独立的逻辑,如果不用两阶段提交的话,这时候会有两种情况

  • 先写redo log后写bin log

假设redo log写完的时候,但是bin log还没有写完,Mysql进程crash掉了,redo log写完之后,系统即使崩溃,仍然能够把数据给恢复回来,恢复后这一行的数据还是1

但是由于bin log没有写完就crash掉了,这时候binlog就没有记录这个语句,因此之后备份日志的时候,存起来的bin log里面就没有这条记录了,如果需要用这个bin log来恢复临时数据的话,这时候数据库的状态就和库的状态不同了。

这是因为数据库备份的时候使用的是binlog如果binlog少了一个记录,那么通过备份记录是恢复出来的数据库的状态就和数据状态不一致。

  • 先写bin log后写redo log

如果在 binlog 写完之后 crash,由于 redo log 还没写,崩溃恢复以后这个事务无效,所以这一行 c 的值是 0。但是 binlog 里面已经记录了“把 c 从 0 改成 1”这个日志。所以,在之后用binlog来恢复的时候就多了一个事务出来,恢复出来的这一行 c 的值就是 1,与原库的值不同。

本质上是因为 redo log 负责事务; binlog负责归档恢复; 各司其职,相互配合,才提供(保证)了现有功能的完整性;

4. 知道redo log吗?讲一下作用和原理

对于Mysql而言,redo log是属于InnoDB所特有的,其作用可以概括为两个:①提升mysql读写的效率效率②保存数据库状态,提供crash-safe能力。其执行原理是这样的,首先redo log它在磁盘上是顺序写的,因此它在磁盘上的读写速度相对于普通的磁盘随机IO要快得多,就好像我要写入一个值,redo log的写入是通过指针寻址,是O(1)的复杂度,而普通的磁盘随机IO在最差情况下可能达到O(n)的复杂度

正是因为这样的特性,在记录数据库操作的时候,并不直接操作在磁盘中记录的数据,而是先顺序写下日志,等到合适的时机,引擎会触发刷脏,所谓刷脏就是将日志中记录的操作在磁盘中进行记录,这个过程中有两个关键指针,第一个指针是check point,它代表着当前的日志已经被记录到磁盘中了,第二个指针是write pos,它代表着刚写入的日志位置,它可能还没有写入到磁盘中,所以说,已经被写入到磁盘中的记录就可以被remove()

这两个指针除了记录哪里是可以删除的日志,还有一个很重要的功能就是crash-safe能力,Mysqlserver层的日志binlog是没有checkpoint的,因此尽管说进程崩溃,日志被记录下来,但是由于没有记录到哪些数据是已经被写入到磁盘中的,因此在恢复的时候是无法判断哪些数据是需要恢复的

正是因为这个原因,InnoDB所提供的redo log才形成了一套crash-safe的机制

5. 知道bin log吗?讲一下作用和原理

binlogmysqlserver层所特有的日志,它记录的是原始sql的逻辑,由于记录的是sql执行的逻辑,因此也通常叫做逻辑日志,而相对的,redo log也叫做物理日志。bin log由于采用的是追加写的方式,没有像redo log那样有容量限制和循环刷新的机制,因此从理论上来说,可以保存数据库一年甚至更久的时间的状态。

6. 两阶段提交了解过吗?是怎么作用的?

首先要理解为什么需要有两阶段协议,两阶段提交协议的场景描述如下:

mysql中,存在着逻辑日志bin log和物理日志redo log,这是两个独立的日志,因此在写入的时候也是独立的,因此存在原子性问题,无论是先写binlog还是redolog,一旦在间隙发生crash,都将会导致后期根据日志系统恢复的时候,产生二义性

redo log在事务执行的过程中会不断写入

而bin log只有在事务commit的时候才会写入

这个二义性通常是在主从复制的时候产生的

  • binlog丢失,而redolog完整的时候,虽然这时候主库的数据能够恢复,但是从库读取到的binlog是不完整的,因此在这样的情况下,就会导致主库和从库的数据不一致
  • redolog丢失,而binlog完整的时候,同样就会导致主库的数据太旧了,而从库读取的数据太新了,最终又导致数据的不一致

为了解决两份日志的逻辑一致的问题,InnoDB使用两阶段提交协议,其原理就是将redo log的写入拆成了两个步骤preparecommit,这就是两阶段提交协议

通过之前的知识就可以知道,由于redo log是不断写入的,而bin log是在事务提交的时候一次性提交的,因此,我们只需要确保bin log在提交成功后,redo log直接设置为提交状态即可

使用两阶段提交协议后,写入bin log发生异常也不会有影响,Mysql根据redo log日志恢复数据的时候,发生redo log还处于prepare状态,并且没有对应的bin log,就会回滚该事务

如果redo log设置commit阶段发生异常,会不会回滚事务呢?

它不会回滚事务,如图所示,如果这个redo log处于commit阶段的话会直接提交,否则会检查是否存在bin log,如果存在bin log也会直接回滚这个事务,但是它认为这个事务的日志是正常的。


文章作者: 穿山甲
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 穿山甲 !
  目录