我们来思考一下,下面这条语句的执行过程是什么样的:
start transaction;
update t_user set name = '易兮' where user_id = 666;
commit;
按照正常的思路,通常过程如下:
上面过程可以确保数据被持久化到了磁盘中。
我们将需求改一下,如下:
start transaction;
update t_user set name = '易兮' where user_id = 666;
update t_user set name = 'javacode2018' where user_id = 888;
commit;
来看一下处理过程:
上面过程我们看有什么问题
上面问题可以归纳为2点:无法确保数据可靠性、随机写导致耗时比较长。
关于上面问题,我们看一下mysql是如何优化的,mysql内部引入了一个redo log,这是一个文件,对于上面2条更新操作,mysql实现如下:
mysql内部有个redo log buffer,是内存中一块区域,我们将其理解为数组结构,向redo log文件中写数据时,会先将内容写入redo log buffer中,后续会将这个buffer中的内容写入磁盘中的redo log文件,这个个redo log buffer是整个mysql中所有连接共享的内存区域,可以被重复使用。
1.start trx=10;
2.写入rb1
3.写入rb2
4.end trx=10;
上面过程执行完毕之后,数据是这样的:
认真看一下上面过程中第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文件中的内容进行处理,处理过程如下:
上面的update之后,p1在内存中是存在的,并且p1是已经被修改过的,可以直接刷新到磁盘中。
如果上面的update之后,mysql宕机,然后重启了,p1在内存中是不存在的,此时系统会读取redo log文件中的内容进行恢复处理。
上面的过程做到了:数据最后一定会被持久化到磁盘中的页中,不会丢失,做到了可靠性。
并且内部采用了先把页的修改操作先在内存中进行操作,然后再写入了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文件中。
过程如下:
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了。
我们来分析一下上面过程可能出现的一些情况:
步骤10操作完成后,mysql宕机了
宕机之前,所有修改都位于内存中,mysql重启之后,内存修改还未同步到磁盘,对磁盘数据没有影响,所以无影响。
步骤12执行完毕之后,mysql宕机了
此时redo log prepare过程是写入redo log文件了,但是binlog写入失败了,此时mysql重启之后会读取redo log进行恢复处理,查询到trx_id=10的记录是prepare状态,会去binlog中查找trx_id=10的操作在binlog中是否存在,如果不存在,说明binlog写入失败了,此时可以将此操作回滚
步骤13执行完毕之后,mysql宕机
此时redo log prepare过程是写入redo log文件了,但是binlog写入失败了,此时mysql重启之后会读取redo log进行恢复处理,查询到trx_id=10的记录是prepare状态,会去binlog中查找trx_id=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的可靠性。
关于上面2点优秀的设计,我们平时开发的过程中也可以借鉴,下面举2个常见的案例来学习一下。