在上文的基础架构介绍中,我们明白了一条查询语句是如何工作的,接下来,我们将探究一条更新语句是如何工作的
。
先从一个表的一条更新语句来看:
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、空闲的时候刷磁盘——–这个动作叫 ”刷脏“,其实就是将数据页更新到磁盘的过程。
同时需要注意的是,InnoDB
的redo log
是固定大小的,其形式是一个循环数组,比如配置为一组4个文件,每个文件的大小都是1GB,那么redo log
就可以记录4GB的操作,从头开始写,写到末尾就又回到开头循环写。
write pos
:是当前记录的位置,一边写一边后移,写到第三号文件末尾后就回到0号文件开头。
check point
:是当前要擦除的位置,也是往后推移并且循环的,擦除记录前要把记录更新到数据文件。
图中绿色的部分:write pos和check point
之间的位置是空闲的位置,可以用来记录新的操作。如果发现write pos
和check 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 log
是InnoDB
引擎所特有的,bin log
是Mysql
的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 log
和bin 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
能力,Mysql
的server
层的日志binlog
是没有checkpoint
的,因此尽管说进程崩溃,日志被记录下来,但是由于没有记录到哪些数据是已经被写入到磁盘中的,因此在恢复的时候是无法判断哪些数据是需要恢复的
正是因为这个原因,InnoDB
所提供的redo log
才形成了一套crash-safe
的机制
5. 知道bin log吗?讲一下作用和原理
binlog
是mysql
server层所特有的日志,它记录的是原始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
的写入拆成了两个步骤prepare
和commit
,这就是两阶段提交协议
通过之前的知识就可以知道,由于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
也会直接回滚这个事务,但是它认为这个事务的日志是正常的。