一文看懂 redo log 与undo log

《朱子语类》里面有句话:“知其然知其所以然!”,笔下回曰:“然也!” 。

 前两天看到一张图,应该是大学毕业不久后保存的,很现实,当然,也有点黑色幽默的感觉,一起来看一看。

感慨什么的,就不吐槽了,过去的生活中或许会有遗憾,未来的日子里都会充满期待与幻想,不过么,数风流人物,还看今朝!

干了这碗鸡汤,咱们继续。MySQL数据库,大家很早应该就接触到了,毕竟数据存储,是一切业务必不可少的阶段。而MySQL的写入操作,都离不开事务这个东东,当然也不离开redo log这个“中间商”。

数据存储自然是持久的,大家用最朴实的思想想一下,也明白,数据最终一定是存储到了磁盘里面。

                

笔下要把“个人精通java字符串的编写!”这句话写到磁盘的文件中,首先是在JVM堆中创建一个字符串,然后写入到“简历.txt”中,历程如下:

                

先从应用程序缓存到OS缓存,再到磁盘,而且,把这句话写入文件中还是非常快的。但是,如果磁盘中已经有了数万乃至数十万的文件数据,用户突然对其中的某一条或者某几条数据进行写操作,那样速度还会很快么?定位数据的位置,分配空间之类的估计就会话费不少的时间。 从度娘那里截了个图,大家可以瞅瞅,有兴趣的话,可以深入下,这里就不重点说了。

对于MySQL来说,每次对数据库file文件的修改都是一次事务,当然直接一句insert 或 update后台就默认帮你提交了,不需要你关注那么多细节,而且这种简单的事务几乎不存在事务并发问题,毕竟耗时短,提交快,而且总有一个先后(下面说为什么有先后)。

那么复杂的事务呢?MySQL是如何做到高速读写,不仅实现事务的提交回滚,而且可以保证数据的一致性呢?我们模拟个事务并发来看看。

首先建立一张简单的数据表:


CREATE TABLE `student`  (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键id',
  `stu_name` varchar(10) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '学生姓名',
  `stu_code` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '学生编号',
  `school_code` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '学校编码',
  `create_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  PRIMARY KEY (`id`) USING BTREE,
  INDEX `idx_code`(`stu_code`) USING BTREE COMMENT '学生编码索引'
) ENGINE = InnoDB AUTO_INCREMENT = 22 CHARACTER SET = latin1 COLLATE = latin1_swedish_ci COMMENT = '学生表' ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of student
-- ----------------------------
INSERT INTO `student` VALUES (23, '张三1', 'ce1482d6b2d211ebaa2abce92fdc3ad6', '20', '2021-05-12 11:33:24');
INSERT INTO `student` VALUES (24, '张三2', 'ce14a163b2d211ebaa2abce92fdc3ad6', '20', '2021-05-12 11:33:24');
INSERT INTO `student` VALUES (25, '张三3', 'ce14b888b2d211ebaa2abce92fdc3ad6', '20', '2021-05-12 11:33:24');
INSERT INTO `student` VALUES (26, '张三4', 'ce14ebb6b2d211ebaa2abce92fdc3ad6', '20', '2021-05-12 11:33:24');
INSERT INTO `student` VALUES (27, '张三5', 'ce151bc1b2d211ebaa2abce92fdc3ad6', '20', '2021-05-12 11:33:24');
INSERT INTO `student` VALUES (28, '张三6', 'ce1538d0b2d211ebaa2abce92fdc3ad6', '20', '2021-05-12 11:33:24');
INSERT INTO `student` VALUES (29, '张三7', 'ce155866b2d211ebaa2abce92fdc3ad6', '20', '2021-05-12 11:33:24');
INSERT INTO `student` VALUES (30, '张三8', 'ce1578beb2d211ebaa2abce92fdc3ad6', '20', '2021-05-12 11:33:24');
INSERT INTO `student` VALUES (31, '张三9', 'ce1591b5b2d211ebaa2abce92fdc3ad6', '20', '2021-05-12 11:33:24');
INSERT INTO `student` VALUES (32, '张三10', 'ce15ac92b2d211ebaa2abce92fdc3ad6', '20', '2021-05-12 11:33:24');
INSERT INTO `student` VALUES (33, '张三11', 'ce15c86ab2d211ebaa2abce92fdc3ad6', '20', '2021-05-12 11:33:24');
INSERT INTO `student` VALUES (34, '张三12', 'ce15e20eb2d211ebaa2abce92fdc3ad6', '20', '2021-05-12 11:33:24');
INSERT INTO `student` VALUES (35, '张三13', 'ce15fa18b2d211ebaa2abce92fdc3ad6', '20', '2021-05-12 11:33:24');
INSERT INTO `student` VALUES (36, '张三14', 'ce1612cab2d211ebaa2abce92fdc3ad6', '20', '2021-05-12 11:33:24');
INSERT INTO `student` VALUES (37, '张三15', 'ce1628e3b2d211ebaa2abce92fdc3ad6', '20', '2021-05-12 11:33:24');
INSERT INTO `student` VALUES (38, '张三16', 'ce163cedb2d211ebaa2abce92fdc3ad6', '20', '2021-05-12 11:33:24');
INSERT INTO `student` VALUES (39, '张三17', 'ce16524bb2d211ebaa2abce92fdc3ad6', '20', '2021-05-12 11:33:24');
INSERT INTO `student` VALUES (40, '张三18', 'ce166a5fb2d211ebaa2abce92fdc3ad6', '20', '2021-05-12 11:33:24');
INSERT INTO `student` VALUES (41, '张三19', 'ce1680fbb2d211ebaa2abce92fdc3ad6', '20', '2021-05-12 11:33:24');
INSERT INTO `student` VALUES (42, '张三20', 'ce169964b2d211ebaa2abce92fdc3ad6', '20', '2021-05-12 11:33:24');
INSERT INTO `student` VALUES (43, '张三21', 'ce16b0b9b2d211ebaa2abce92fdc3ad6', '20', '2021-05-12 11:33:24');
INSERT INTO `student` VALUES (44, '张三22', 'ce16c723b2d211ebaa2abce92fdc3ad6', '20', '2021-05-12 11:33:24');
INSERT INTO `student` VALUES (45, '张三23', 'ce16decab2d211ebaa2abce92fdc3ad6', '20', '2021-05-12 11:33:24');
INSERT INTO `student` VALUES (46, '张三24', 'ce170106b2d211ebaa2abce92fdc3ad6', '20', '2021-05-12 11:33:24');
INSERT INTO `student` VALUES (47, '张三25', 'ce171d10b2d211ebaa2abce92fdc3ad6', '20', '2021-05-12 11:33:24');
INSERT INTO `student` VALUES (48, '张三26', 'ce173b93b2d211ebaa2abce92fdc3ad6', '20', '2021-05-12 11:33:24');
INSERT INTO `student` VALUES (49, '张三27', 'ce1759e3b2d211ebaa2abce92fdc3ad6', '20', '2021-05-12 11:33:24');
INSERT INTO `student` VALUES (50, '张三28', 'ce177224b2d211ebaa2abce92fdc3ad6', '20', '2021-05-12 11:33:24');

 启动事务一(操作的数据id是34):

start transaction;

select * from student where stu_code = 'ce15e20eb2d211ebaa2abce92fdc3ad6';
update student set school_code = 30 where stu_code ='ce15e20eb2d211ebaa2abce92fdc3ad6';
-- update student set school_code = 40 where stu_code ='ce15e20eb2d211ebaa2abce92fdc3ad6';
select * from student where stu_code = 'ce15e20eb2d211ebaa2abce92fdc3ad6';
commit;

可以看到最后查询的 school_code = 30 ,再启动事务2: 

start transaction;

select * from student where stu_code = 'ce15e20eb2d211ebaa2abce92fdc3ad6';
update student set school_code = 50 where stu_code = 'ce15e20eb2d211ebaa2abce92fdc3ad6';

我们可以看到school_code = 20,而且,修改的操作陷入了等待,也就是一直在执行中。也就是说发生了事务并发,触发了行锁,事务2进入了锁等待。

此时对于数据库来说school_code = 20,对于事务1来说,school_code = 30。如果事务一提交,那么事务二修改完成后,再查询,school_code = 50

在启动一个事务三,如果仅仅事务一提交,查询的话结果也是school_code = 30;

start transaction;
select * from student where stu_code = 'ce15e20eb2d211ebaa2abce92fdc3ad6';

说到这里,就不得不说下MySQL的一个机制了,MVCC(Multi-Version Concurrency Control ),也就是多版本并发控制。

事物的隔离级别分为Read uncommitted,Read committed,Repeatable read,Seralizable这四种,Mysql在可重复读隔离级别下保证事务较高的隔离性,其中必不可少的机制就是MVCC!其中一个重要部分就是undo log!

                

undo log实际上是就是一个日志版本链,比如,某一行数据被多个事务进行了修改,那么每次修改都会记录一个undo log 回滚日志,如果某次修改最终rollback了,那么就根据undo log日志,进行数据回滚。每条undo log 有三个关键字段,row_id、trx_id和roll_pointer,也就是该条数据主键id、事务的id和这条数据上一个版本的指针,通过事务id将undo log 串联成一个历史记录版本链,那么,事务的任何SQL查询结果从对应版本链里的最新数据开始逐条对比从而获取最终的快照结果。

根据上面的两个事务,衍生一个undo log版本链。

通俗的说下,就是在当前事务进行查询的时候,从当前记录向上查找,直到找到当前数据的提交的那条undo log,在这段记录中,如果没有本事务对该条数据的操作,那么就返回已经提交的数据,如果有当前事务对这条数据的修改,那么久返回当前事务修改的数据。

对着上面的栗子来看下:

就看事务二,第一次查询的时候,已提交的数据,也就是数据库的数据,school_code=20,查询的结果也就是20。

第二次查询的时候,事务一提交了,数据库的数据是school_code=40,但是当前事务对school_code=40进行了修改,改成了50,那就需要根据当前事务修改后的数据为主,故返回50,是不是挺清晰的?就是MVCC和MySQL的锁保证了事务的正确执行,是不是挺牛掰?

                 

undo log 是事务数据查询,回滚等用到的,那么,redo log是干啥的?

        MySQL数据库最终是以文件的形式存储在磁盘中,为了保证数据的持久性,对数据进行新增修改的时候,肯定要把数据写入到磁盘中的。而对磁盘数据进行修改的时候,也就是我们操作数据库的时候,肯定是随机修改其中的数据,这样的话,修改速度肯定满足不了我们的要求。

        随机写性能比较差,肯定有优化方法呀!比如,计算机,CPU读写速度肯定都远远超过磁盘,如果每次操作都是直接对磁盘进行操作,无疑会拖慢CPU的读写速率,于是有了CPU缓存,内存条等中间存储设备!

        MySQL也是采取类似的方法,对读写速率进行了优化,不过更为复杂,而且为了保证MySQL中数据的持久性和一致性,数据每次或者每隔很短的时间就要写入磁盘。而redo log就在其中扮演了一个十分重要的角色!(redo log 是innerDB独有的,下文的逻辑默认MySQL使用了innerDB引擎哈)

                

        当MySQL事务提交时,首先会把数据写入缓存,也就是redo log buffer,调用了一个函数叫做WriteRedoLog。这个是每次事务提交都必须要写入的。

        接着,肯定是要把应用级缓存写入到操作系统缓存呀,也就是要发起系统调用写文件write。当然这个什么时候写就看配置了,可以每次事务完成都写入到操作系统级别的缓存,也可以每隔一段时间写一次。不过这个时候文件还是没有进入磁盘的,如果发生宕机或者断电之类的,数据自然也就丢失了哦。

                

最后,再由操作系统将操作系统中的缓存(OS cache)数据,最终fsync到磁盘上,也就是顺序写到redo log,速度自然超过随机写一大截!这个时候,停电宕机什么也不会影响到数据了。

        

 当然,这个时候也仅仅是将数据也到了redo 日志中,并未真正的写到数据库文件中,后面还有操作将redo log file同步到数据库,不过这一步已经影响不到数据的安全性了,不多说。

为了保证数据的持久性,也就是需要三步,如下图:

                

 如果在写入OS cache之前,应用程序崩溃了,数据就丢失;

如果在写入redo log file 之前,机器宕机或者断电之类的,数据也丢失了。

所以,MySQL默认的是每次事务提交,都会写入到redo log file。

redo log file的写入虽然是顺序写,但是每次事务提交都写入磁盘,对性能的影响仍然是很大的。每隔1-2s批量写一次行不行?

行!

        MySQL里面有个参数,innodb_flush_log_at_trx_commit,就是控制事务提交的时候,刷新数据的策略。

默认innodb_flush_log_at_trx_commit=1, 强一致性,也就是每次事务提交,既要刷新到OS cache,也要写入到redo log中去,不用担心数据丢失。

        如果要性能最佳的话,设置innodb_flush_log_at_trx_commit=0,提交后,只是写入到redo log buffer,每隔1s,才将redo log buffer 中的数据批量写入OS cache,同时写入redo log。单次写改成批量写,速度肯定快很多的。当然,如果应用程序挂了或者操作系统挂了,就要丢失1s的数据。

        再有就是折中一下,设置innodb_flush_log_at_trx_commit=2,提交后,每次写入到redo log buffer,write到OS cache,每隔一秒,OS cache 中的数据批量刷新到redo log文件。效率或许会比innodb_flush_log_at_trx_commit=0 差一点,但也不会相差太多,因为就是多一步内存写操作,速度不会有太大影响的。当然,如果操作系统宕机或者停电什么的,也是会丢失1s的数据的。

        一般来说,innodb_flush_log_at_trx_commit=0与innodb_flush_log_at_trx_commit=2,效率相差几倍,与innodb_flush_log_at_trx_commit=1相比,效率要相差几十倍甚至上百倍。

不过,哪怕应用程序崩溃,操作系统一般也不会轻易宕机,停电什么的,对一般的大型机房很少会发生这种问题, innodb_flush_log_at_trx_commit=2已经足够,而且可以更容易的适应高并发业务。

        当然,如果对数据一致性要求特别高,也可以设置innodb_flush_log_at_trx_commit=1,应对高并发的时候,可以加机器么~,分库分表什么的,就是业务麻烦了一点。

        除了redo log之外,MySQL在执行事务提交的时候,还会将提交的SQL语句,写入到一个叫做binlog的文件中。

        binlog是MySQL数据库上层产生的,记录了MySQL数据库执行更改的所有操作,不包括select和show这类操作,因为这类操作本身并没有修改数据。

redo log是在innerDB存储引擎产生,是innerDB独有,而且记录的是物理日志,而且大小是有限制的,超过固定大小之后,会返回起点,循环使用。

        binlog 不循环使用的,在写满或者重启之后,会生成新的binlog文件,一般来说binlog文件比较大,用于恢复数据、主从复制搭建,而redo log 则是作为异常宕机或者介质故障后数据恢复使用。下面copy一张SQL执行 的逻辑图,大家可以瞅一瞅哈!

好了,今天的分享就到这里吧,感觉还行的话,点个赞呗~

                 

人贵有志,学贵有恒!

若有志何须三更睡五更起?最无益只怕一日曝十日寒!

                

 no sacrifice ,no victory~

  • 10
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

笔下天地宽

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值