这是Mysql系列第27篇。
本篇文章我们先来看一下mysql是如何确保数据不丢失的,通过本文我们可以了解mysql内部确保数据不丢失的原理,学习里面优秀的设计要点,然后我们再借鉴这些优秀的设计要点进行实践应用,加深理解。
预备知识
-
mysql内部是使用b+树的结构将数据存储在磁盘中,b+树中节点对应mysql中的页,mysql和磁盘交互的最小单位为页,页默认情况下为16kb,表中的数据记录存储在b+树的叶子节点中,当我们需要修改、删除、插入数据时,都需要按照页来对磁盘进行操作。
-
磁盘顺序写比随机写效率要高很多,通常我们使用的是机械硬盘,机械硬盘写数据的时候涉及磁盘寻道、磁盘旋转寻址、数据写入的时间,耗时比较长,如果是顺序写,省去了寻道和磁盘旋转的时间,效率会高几个数量级。
-
内存中数据读写操作比磁盘中数据读写操作速度高好多个数量级。
mysql确保数据不丢失原理分析
我们来思考一下,下面这条语句的执行过程是什么样的:
start transaction;
update t_user set name = '路人甲Java' where user_id = 666;
commit;
按照正常的思路,通常过程如下:
-
找到user_id=666这条记录所在的页p1,将p1从磁盘加载到内存中
-
在内存中对p1中user_id=666这条记录信息进行修改
-
mysql收到commit指令
-
将p1页写入磁盘
-
给客户端返回更新成功
上面过程可以确保数据被持久化到了磁盘中。
我们将需求改一下,如下:
start transaction;
update t_user set name = '路人甲Java' where user_id = 666;
update t_user set name = 'javacode2018' where user_id = 888;
commit;
来看一下处理过程:
-
找到user_id=666这条记录所在的页p1,将p1从磁盘加载到内存中
-
在内存中对p1中user_id=666这条记录信息进行修改
-
找到user_id=888这条记录所在的页p2,将p2从磁盘加载到内存中
-
在内存中对p2中user_id=888这条记录信息进行修改
-
mysql收到commit指令
-
将p1页写入磁盘
-
将p2页写入磁盘
-
给客户端返回更新成功
上面过程我们看有什么问题
-
假如6成功之后,mysql宕机了,此时p1修改已写入磁盘,但是p2的修改还未写入磁盘,最终导致user_id=666的记录被修改成功了,user_id=888的数据被修改失败了,数据是有问题的
-
上面p1和p2可能位于磁盘的不同位置,涉及到磁盘随机写的问题,导致整个过程耗时也比较长
上面问题可以归纳为2点:无法确保数据可靠性、随机写导致耗时比较长。
关于上面问题,我们看一下mysql是如何优化的,mysql内部引入了一个redo log,这是一个文件,对于上面2条更新操作,mysql实现如下:
mysql内部有个redo log buffer,是内存中一块区域,我们将其理解为数组结构,向redo log文件中写数据时,会先将内容写入redo log buffer中,后续会将这个buffer中的内容写入磁盘中的redo log文件,这个个redo log buffer是整个mysql中所有连接共享的内存区域,可以被重复使用。
-
mysql收到start transaction后,生成一个全局的事务编号trx_id,比如trx_id=10
-
user_id=666这个记录我们就叫r1,user_id=888这个记录叫r2
-
找到r1记录所在的数据页p1,将其从磁盘中加载到内存中
-
在内存中找到r1在p1中的位置,然后对p1进行修改(这个过程可以描述为:将p1中的pos_start1到pos_start2位置的值改为v1),这个过程我们记为rb1(内部包含事务编号trx_id),将rb1放入redo log buffer数组中,此时p1的信息在内存中被修改了,和磁盘中p1的数据不一样了
-
找到r2记录所在的数据页p2,将其从磁盘中加载到内存中
-
在内存中找到r2在p2中的位置,然后对p2进行修改(这个过程可以描述为:将p2中的pos_start1到pos_start2位置的值改为v2),这个过程我们记为rb2(内部包含事务编号trx_id),将rb2放入redo log buffer数组中,此时p2的信息在内存中被修改了,和磁盘中p2的数据不一样了
-
此时redo log buffer数组中有2条记录[rb1,rb2]
-
mysql收到commit指令
-
将redo log buffer数组中内容写入到redo log文件中,写入的内容:
1.start trx=10; 2.写入rb1 3.写入rb2 4.end trx=10;
-
返回给客户端更新成功。
上面过程执行完毕之后,数据是这样的:
-
内存中p1、p2页被修改了,还未同步到磁盘中,此时内存中数据页和磁盘中数据页是不一致的,此时内存中数据页我们称为脏页
-
对p1、p2页修改被持久到磁盘中的redolog文件中了,不会丢失
认真看一下上面过程中第9步骤,一个成功的事务记录在redo log中是有start和end的,redo log文件中如果一个trx_id对应start和end成对出现,说明这个事务执行成功了,如果只有start没有end说明是有问题的。
那么对p1、p2页的修改什么时候会同步到磁盘中呢?
redo log是mysql中所有连接共享的文件,对mysql执行insert、delete和上面update的过程类似,都是先在内存中修改页数据,然后将修改过程持久化到redo log所在的磁盘文件中,然后返回成功。redo log文件是有大小的,需要重复利用的(redo log有多个,多个之间采用环形结构结合几个变量来做到重复利用,这块知识不做说明,有兴趣的可以去网上找一下),当redo log满了,或者系统比较闲的时候,会对redo log文件中的内容进行处理,处理过程如下:
-
读取redo log信息,读取一个完整的trx_id对应的信息,然后进行处理
-
比如读取到了trx_id=10的完整内容,包含了start end,表示这个事务操作是成功的,然后继续向下
-
判断p1在内存中是否存在,如果存在,则直接将p1信息写到p1所在的磁盘中;如果p1在内存中不存在,则将p1从磁盘加载到内存,通过redo log中的信息在内存中对p1进行修改,然