预备知识
- 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的修改还未写⼊磁盘,最终导致userid=666的记录被修改成功了,userid=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后,⽣成⼀个全局的事务编号trxid,⽐如trxid=10
- userid=666这个记录我们就叫r1,userid=888这个记录叫r2
- 找到r1记录所在的数据页p1,将其从磁盘中加载到内存中
- 在内存中找到r1在p1中的位置,然后对p1进⾏修改(这个过程可以描述为:将p1中的posstart1到posstart2位置的值改为v1),这个过程我们记为rb1(内部包含事务编号trx_id),将rb1放⼊redo log buffer数组中,此时p1的信息在内存中被修改了,和磁盘中p1的数据不⼀样了
- 找到r2记录所在的数据页p2,将其从磁盘中加载到内存中
- 在内存中找到r2在p2中的位置,然后对p2进⾏修改(这个过程可以描述为:将p2中的posstart1到posstart2位置的值改为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进⾏修改,然后将其写到磁盘中上⾯的update之后,p1在内存中是存在的,并且p1是已经被修改过的,可以直接刷新到磁盘中。
如果上⾯的update之后,mysql宕机,然后重启了,p1在内存中是不存在的,此时系统会读取redo log⽂件中的内容进⾏恢复处理。
6. 将redo log⽂件中trx_id=10的占有的空间标记为已处理,这块空间会被释放出来可以重复利⽤了
7. 如果第2步读取到的trx_id对应的内容没有end,表⽰这个事务执⾏到⼀半失败了(可能是第9步骤写到⼀半宕机了),此时这个记录是⽆效的,可以直接跳过不⽤处理上⾯的过程做到了:数据最后⼀定会被持久化到磁盘中的页中,不会丢失,做到了可靠性。并且内部采⽤了先把页的修改操作先在内存中进⾏操作,然后再写⼊了redo log⽂件,此处redo log是按顺序写的,使⽤到了io的顺序写,效率会⾮常⾼,相对于⽤户来说响应会更快。
对于将数据页的变更持久化到磁盘中,此处又采⽤了异步的⽅式去读取redo log的内容,然后将页的变更刷到磁盘中,这块的设计也⾮常好,异步刷盘操作!但是有⼀种情况,当⼀个事务commit的时候,刚好发现redo log不够了,此时会先停下来处理redo log中的内容,然后在进⾏后续的操作,遇到这种情况时,整个事物响应会稍微慢⼀些。
mysql中还有⼀个binlog,在事务操作过程中也会写binlog,先说⼀下binlog的作⽤,binlog中详细记录了对数据库做了什么操作,算是对数据库操作的⼀个流⽔,这个流⽔也是相当重要的,主从同步就是使⽤binlog来实现的,从库读取主库中binlog的信息,然后在从库中执⾏,最后,从库就和主库信息保持同步⼀致了。还有⼀些其他系统也可以使⽤binlog的功能,⽐如可以通过binlog来实现bi系统中etl的功能,将业务数据抽取到数据仓库,阿⾥提供了⼀个java版本的项⽬:canal,这个项⽬可以模拟从库从主库读取binlog的功能,也就是说可以通过java程序来监控数据库详细变化的流⽔,这个⼤家可以脑洞⼤开⼀下,可以做很多事情的,有兴趣的朋友可以去研究⼀下;所以binlog对mysql来说也是相当重要的,我们来看⼀下系统如何确保redo log 和binlog在⼀致性的,都写⼊成功的。
还是以update为例:
start transaction;
update t_user set name = '路⼈甲Java' where user_id = 666;
update t_user set name = 'javacode2018' where user_id = 888;
commit;
⼀个事务中可能有很多操作,这些操作会写很多binlog⽇志,为了加快写的速度,mysql先把整个过程中产⽣的binlog⽇志先写到内存中的binlog cache缓存中,后⾯再将binlog cache中内容⼀次性持久化到binlog⽂件中。⼀个事务的binlog 是不能被拆开的,因此不论这个事务多⼤,也要确保⼀次性写⼊。这就涉及到了 binlog cache 的保存问题。系统给 binlog cache 分配了⼀⽚内存,每个线程⼀个,参数 binlogcachesize ⽤于控制单个线程内 binlog cache 所占内存的⼤⼩。如果超过了这个参数规定的⼤⼩,就要暂存到磁盘。过程如下:
- mysql收到start transaction后,⽣成⼀个全局的事务编号trxid,⽐如trxid=10
- userid=666这个记录我们就叫r1,userid=888这个记录叫r2
- 找到r1记录所在的数据页p1,将其从磁盘中加载到内存中
- 在内存中对p1进⾏修改
- 将p1修改操作记录到redo log buffer中
- 将p1修改记录流⽔记录到binlog cache中
- 找到r2记录所在的数据页p2,将其从磁盘中加载到内存中
- 在内存中对p2进⾏修改
- 将p2修改操作记录到redo log buffer中
- 将p2修改记录流⽔记录到binlog cache中
- mysql收到commit指令
- 将redo log buffer携带trx_id=10写⼊到redo log⽂件,持久化到磁盘,这步操作叫做redo log prepare,内容如下:
1.start trx=10; 2.写⼊rb1 3.写⼊rb2 4.prepare trx=10;注意上⾯是prepare了,不是之前说的end了。
- 将binlog cache携带trx_id=10写⼊到binlog⽂件,持久化到磁盘
- 向redo log中写⼊⼀条数据:end trx=10; 表⽰redo log中这个事务完成了,这步操作叫做redo log commit
- 返回给客户端更新成功,分析⼀下上⾯过程可能出现的⼀些情况:
步骤10操作完成后,mysql宕机了宕机之前,所有修改都位于内存中,mysql重启之后,内存修改还未同步到磁盘,对磁盘数据没有影响,所以⽆影响。步骤12执⾏完毕之后,mysql宕机了此时redo log prepare过程是写⼊redo log⽂件了,但是binlog写⼊失败了,此时mysql重启之后会读取redo log进⾏恢复处理,查询到trxid=10的记录是prepare状态,会去binlog中查找trxid=10的操作在binlog中是否存在,如果不存在,说明binlog写⼊失败了,此时可以将此操作回滚步骤13执⾏完毕之后,mysql宕机此时redo log prepare过程是写⼊redo log⽂件了,但是binlog写⼊失败了,此时mysql重启之后会读取redo log进⾏恢复处理,查询到trxid=10的记录是prepare状态,会去binlog中查找trxid=10的操作在binlog是存在的,然后接着执⾏上⾯的步骤14和15;
做⼀个总结
上⾯的过程设计⽐较好的地⽅,有2点⽇志先⾏,io顺序写,异步操作,做到了⾼效操作对数据页,先在内存中修改,然后使⽤io顺序写的⽅式持久化到redo log⽂件;
然后异步去处理redo log,将数据页的修改持久化到磁盘中,效率⾮常⾼,整个过程,其实就是 MySQL ⾥经常说到的 WAL 技术,WAL 的全称是 Write-Ahead Logging,它的关键点就是先写⽇志,再写磁盘;
两阶段提交确保redo log和binlog⼀致性
为了确保redo log和binlog⼀致性,此处使⽤了⼆阶段提交技术,redo log 和binlog的写分了3步⾛:
- 携带trx_id,redo log prepare到磁盘
- 携带trx_id,binlog写⼊磁盘
- 携带trx_id,redo log commit到磁盘
- 上⾯3步骤,可以确保同⼀个trx_id关联的redo log 和binlog的可靠