MySQL实战45讲 15「终于解决」

MySQL实战45讲 15「终于解决」MySQL实战45讲 15 日志相关 业务设计问题

MySQL实战45讲 15

15 | 答疑文章(一):日志和索引相关问题

日志相关

binlog(归档日志)和redo log(重做日志)配合崩溃恢复,在两阶段提交的不同瞬间,MySQL如果发生异常重启,是怎么保证数据完整性的?

img

Q:这个图不是一个update 语句的执行流程吗,怎么还会调用 commit 语句?

A:

两个“commit”的概念

  • “commit 语句”是指 MySQL 语法中,用于提交一个事务的命令。一般跟begin/start transaction 配对使用。

  • 而图中用到的这个“commit 步骤”,指的是事务提交过程中的一个小步骤,也是最后一步。当这个步骤执行完成后,这个事务就提交完成了。

  • “commit 语句”执行的时候,会包含“commit 步骤”。

而这个例子里面,没有显式地开启事务,因此这个 update 语句自己就是一个事务,在执行完成后提交事务时,就会用到这个“commit 步骤“

分析在两阶段提交的不同时刻,MySQL 异常重启会出现什么现象

如果在图中时刻 A 的地方,也就是写入 redo log 处于 prepare 阶段之后、写 binlog 之前,发生了崩溃(crash),由于此时 binlog 还没写,redo log 也还没提交,所以崩溃恢复的时候,这个事务会回滚。这时候,binlog 还没写,所以也不会传到备库。

在时刻 B,也就是 binlog 写完,redo log 还没 commit前发生 crash,那崩溃恢复的时候 MySQL 会怎么处理?

崩溃恢复时的判断规则:

  1. 如果 redo log 里面的事务是完整的,也就是已经有了 commit 标识,则直接提交;
  2. 如果 redo log 里面的事务只有完整的 prepare,则判断对应的事务 binlog 是否存在并完整
    a. 如果是,则提交事务;
    b. 否则,回滚事务。

这里,时刻 B 发生 crash 对应的就是 2(a) 的情况,崩溃恢复过程中事务会被提交。

Q:MySQL 怎么知道 binlog 是完整的?

A:一个事务的 binlog 是有完整格式的:

  • statement格式的binlog,最后会有COMMIT;
  • row格式的binlog,最后会有一个XID event。

另外,在MySQL 5.6.2版本以后,还引入了binlog-checksum参数,用来验证binlog内容的正确性。

对于binlog日志由于磁盘原因,可能会在日志中间出错的情况,MySQL可以通过校验checksum的结果来发现。所以,MySQL还是有办法验证事务binlog的完整性的。

Q:redo log 和 binlog 是怎么关联起来的?

A:

它们有一个共同的数据字段,叫 XID。崩溃恢复的时候,会按顺序扫描 redo log

  • 如果碰到既有 prepare、又有 commit 的 redo log,就直接提交;
  • 如果碰到只有 parepare、而没有 commit 的 redo log,就拿着 XID 去 binlog 找对应的事务

Q:处于 prepare 阶段的 redo log 加上完整 binlog,重启就能恢复,MySQL 为什么要这么设计?

A:这个问题和数据与备份的一致性有关。在时刻B,也就是 binlog 写完以后 MySQL 发生崩溃,这时候 binlog 已经写入了,之后就会被从库(或者用这个 binlog 恢复出来的库)使用。所以,在主库上也要提交这个事务。采用这个策略,主库和备库的数据就保证了一致性。

Q:如果这样的话,为什么还要两阶段提交呢?干脆先 redo log 写完,再写 binlog。崩溃恢复的时候,必须得两个日志都完整才可以。是不是一样的逻辑?

A:

回答:其实,两阶段提交是经典的分布式系统问题,并不是MySQL独有的。

如果必须要举一个场景,来说明这么做的必要性的话,那就是事务的持久性问题

对于InnoDB引擎来说,如果redo log提交完成了,事务就不能回滚(如果这还允许回滚,就可能覆盖掉别的事务的更新)。而如果redo log直接提交,然后binlog写入的时候失败,InnoDB又回滚不了,数据和binlog日志又不一致了

两阶段提交就是为了给所有人一个机会,当每个人都说“我ok”的时候,再一起提交

Q :不引入两个日志,也就没有两阶段提交的必要了。只用 binlog 来支持崩溃恢复,又能支持归档,不就可以了?

A:意思是只保留 binlog,然后可以把提交流程改成这样:… -> “数据更新到内存” -> “写 binlog” -> “提交事务”,是不是也可以提供崩溃恢复的能力?

不可以。

历史原因: InnoDB 并不是 MySQL 的原生存储引擎。MySQL 的原生引擎是 MyISAM,设计之初就有没有支持崩溃恢复。InnoDB 在作为 MySQL 的插件加入 MySQL 引擎家族之前,就已经是一个提供了崩溃恢复和事务支持的引擎了。InnoDB 接入了 MySQL 后,发现既然 binlog 没有崩溃恢复的能力,那就用 InnoDB 原有的 redo log 好了。

实现上的原因:

假设只用binlog

img

这样的流程下,binlog 还是不能支持崩溃恢复的。binlog 没有能力恢复“数据页”

如果在图中标的位置,也就是 binlog2 写完了,但是整个事务还没有 commit 的时候,MySQL 发生了 crash

重启后,引擎内部事务 2 会回滚,然后应用 binlog2 可以补回来;但是对于事务 1 来说,系统已经认为提交完成了,不会再应用一次 binlog1

但是,InnoDB 引擎使用的是 WAL 技术,执行事务的时候,写完内存和日志,事务就算完成了如果之后崩溃,要依赖于日志来恢复数据页。

也就是说在图中这个位置发生崩溃的话,事务 1 也是可能丢失了的,而且是数据页级的丢失。此时,binlog 里面并没有记录数据页的更新细节,是补不回来的。

如果要说,优化一下 binlog 的内容,让它来记录数据页的更改可以吗?但,这其实就是又做了一个 redo log 出来。所以,至少现在的 binlog 能力,还不能支持崩溃恢复。

Q:那能不能反过来,只用 redo log,不要 binlog?

A:如果只从崩溃恢复的角度来讲是可以的。你可以把 binlog 关掉,这样就没有两阶段提交了,但系统依然是 crash-safe 的。

但 binlog 有着 redo log 无法替代的功能。

一个是归档。redo log 是循环写,写到末尾是要回到开头继续写的。这样历史日志没法保留,redo log 也就起不到归档的作用。

一个就是 MySQL 系统依赖于 binlog。binlog 作为 MySQL 一开始就有的功能,被用在了很多地方。其中,MySQL 系统高可用的基础,就是 binlog 复制。

还有很多公司有异构系统(比如一些数据分析系统),这些系统就靠消费 MySQL 的binlog 来更新自己的数据。关掉 binlog 的话,这些下游系统就没法输入了

Q:redo log 一般设置多大?

A:redo log 太小的话,会导致很快就被写满,然后不得不强行刷 redo log,这样WAL 机制的能力就发挥不出来了。直接将 redo log 设置为4 个文件、每个文件 1GB 。

Q:正常运行中的实例,数据写入后的最终落盘,是从 redo log 更新过来的还是从 buffer pool 更新过来的呢?

A:

实际上,redo log 并没有记录数据页的完整数据,所以它并没有能力自己去更新磁盘数据页,也就不存在“数据最终落盘,是由 redo log 更新过去”的情况。

  1. 如果是正常运行的实例的话,数据页被修改以后,跟磁盘的数据页不一致,称为脏页。最终数据落盘,就是把内存中的数据页写盘。这个过程,甚至与 redo log 毫无关系。(指落盘的过程)

  2. 在崩溃恢复场景中,InnoDB 如果判断到一个数据页可能在崩溃恢复的时候丢失了更新,就会将它读到内存,然后让 redo log 更新内存内容。更新完成后,内存页变成脏页,就回到了第一种情况的状态。

Q:redo log buffer 是什么?是先修改内存,还是先写 redo log文件?

A:

在一个事务的更新过程中,日志是要写多次的。比如下面这个事务:

begin;
insert into t1 ...
insert into t2 ...
commit;

这个事务要往两个表中插入记录,插入数据的过程中,生成的日志都得先保存起来但又不能在还没 commit 的时候就直接写到 redo log 文件里

所以,redo log buffer 就是一块内存,用来先存 redo 日志的。也就是说,在执行第一个insert 的时候,数据的内存被修改了,redo log buffer 也写入了日志。但是,真正把日志写到 redo log 文件(文件名是 ib_logfile+ 数字),是在执行 commit语句的时候做的。

这里说的是事务执行过程中不会“主动去刷盘”,以减少不必要的 IO 消耗。但是可能会出现“被动写入磁盘”,比如内存不够、其他事务提交等情况。

业务设计问题

业务上有这样的需求,A、B 两个用户,如果互相关注,则成为好友

设计上是有两张表,一个是 like 表,一个是 friend 表,like 表有 user_id、liker_id 两个字段,我设置为复合唯一索引即 uk_user_id_liker_id。语句执行逻辑是这样的:

以 A 关注 B 为例:
第一步,先查询对方有没有关注自己(B 有没有关注 A)
select * from like where user_id = B and liker_id = A;

如果有,则成为好友
insert into friend;

没有,则只是单向关注关系
insert into like;

但是如果 A、B 同时关注对方,会出现不会成为好友的情况因为上面第 1步,双方都没关注对方。第 1 步即使使用了排他锁也不行,因为记录不存在,行锁无法生效。请问这种情况,在 MySQL 锁层面有没有办法处理?

即:在并发场景下,同时有两个人,设置为关注对方,就可能导致无法成功加为朋友关系

模拟出表:

PS:业务根本就是保证“我一定会插入重复数据,数据库一定要要有唯一性约束”,这时直接建唯一索引。不用考虑在“业务开发保证不会插入重复记录”的情况下,着重要解决性能问题的时候,才建议尽量使用普通索引

CREATE TABLE `like` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` int(11) NOT NULL,
  `liker_id` int(11) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_user_id_liker_id` (`user_id`,`liker_id`)
) ENGINE=InnoDB;

CREATE TABLE `friend` (
  id` int(11) NOT NULL AUTO_INCREMENT,
  `friend_1_id` int(11) NOT NULL,
  `firned_2_id` int(11) NOT NULL,
  UNIQUE KEY `uk_friend` (`friend_1_id`,`firned_2_id`)
  PRIMARY KEY (`id`)
) ENGINE=InnoDB;

img

由于一开始 A 和 B 之间没有关注关系,所以两个事务里面的 select 语句查出来的结果都是空。

因此,session 1 的逻辑就是“既然 B 没有关注 A,那就只插入一个单向关注关系”。session 2 也同样是这个逻辑。

这个结果对业务来说就是 bug 了。因为在业务设定里面,这两个逻辑都执行完成以后,是应该在 friend 表里面插入一行记录的。

如提问里面说的,“第 1 步即使使用了排他锁也不行,因为记录不存在,行锁无法生效”。不过,使用另外一个方法,来解决这个问题。

首先,要给“like”表增加一个字段,比如叫作 relation_ship,并设为整型,取值 1、2、3。

值是 1 的时候,表示 user_id 关注 liker_id;
值是 2 的时候,表示 liker_id 关注 user_id;
值是 3 的时候,表示互相关注

然后,当 A 关注 B 的时候,逻辑改成如下所示的样子:

应用代码里面,比较 A 和 B 的大小,如果 A<B,就执行下面的逻辑:

begin; /* 启动事务 */
insert into `like`(user_id, liker_id, relation_ship) values(A, B, 1) on duplicate key update relation_ship=relation_ship | 1;
select relation_ship from `like` where user_id=A and liker_id=B;
/* 代码中判断返回的 relation_ship,
  如果是 1,事务结束,执行 commit
  如果是 3,则执行下面这两个语句:
  */
insert ignore into friend(friend_1_id, friend_2_id) values(A,B);
commit;

如果 A>B,则执行下面的逻辑

begin; /* 启动事务 */
insert into `like`(user_id, liker_id, relation_ship) values(B, A, 2) on duplicate key update relation_ship=relation_ship | 2;
select relation_ship from `like` where user_id=B and liker_id=A;
/* 代码中判断返回的 relation_ship,
  如果是 2,事务结束,执行 commit
  如果是 3,则执行下面这两个语句:
*/
insert ignore into friend(friend_1_id, friend_2_id) values(B,A);
commit;

这个设计里,让“like”表里的数据保证 user_id < liker_id,这样不论是 A 关注 B,还是B 关注 A,在操作“like”表的时候,如果反向的关系已经存在,就会出现行锁冲突

然后,insert … on duplicate 语句,确保了在事务内部,执行了这个 SQL 语句后,就强行占住了这个行锁,之后的 select 判断 relation_ship 这个逻辑时就确保了是在行锁保护下的读操作。

操作符 “|” 是按位或,连同最后一句 insert 语句里的 ignore,是为了保证重复调用时的幂等性。???

这样,即使在双方“同时”执行关注操作,最终数据库里的结果,也是 like 表里面有一条关于 A 和 B 的记录,而且 relation_ship 的值是 3, 并且 friend 表里面也有了 A 和 B 的这条记录。

Q:???

我们创建了一个简单的表 t,并插入一行,然后对这一行做修改。

CREATE TABLE `t` (
`id` int(11) NOT NULL primary key auto_increment,
`a` int(11) DEFAULT NULL
) ENGINE=InnoDB;
insert into t values(1,2);

这时候,表 t 里有唯一的一行数据 (1,2)。假设,现在要执行:

update t set a=2 where id=1;

结果:

img

结果显示,匹配(rows matched)了一行修改(Changed)了0行

仅从现象上看,MySQL内部在处理这个命令的时候,可以有以下三种选择:

  1. 更新都是先读后写的,MySQL 读出数据,发现 a 的值本来就是 2,不更新,直接返回,执行结束;
  2. MySQL 调用了 InnoDB 引擎提供的“修改为 (1,2)”这个接口,但是引擎发现值与原来相同,不更新,直接返回;
  3. InnoDB 认真执行了“把这个值修改成 (1,2)”这个操作,该加锁的加锁,该更新的更新。

你觉得实际情况会是以上哪种呢?你可否用构造实验的方式,来证明你的结论?进一步地,可以思考一下,MySQL 为什么要选择这种策略呢?

A:

答案应该是选项 3,即:InnoDB 认真执行了“把这个值修改成 (1,2)”这个操作,该加锁的加锁,该更新的更新。

MySQL实战45讲 15「终于解决」

第一个选项是,MySQL 读出数据,发现值与原来相同,不更新,直接返回,执行结束。这里可以用一个锁实验来确认。

假设,当前表 t 里的值是 (1,2)。

MySQL实战45讲 15「终于解决」

session B 的 update 语句被 blocked 了,加锁这个动作是 InnoDB 才能做的,所以排除选项 1。

第二个选项是,MySQL 调用了 InnoDB 引擎提供的接口,但是引擎发现值与原来相同,不更新,直接返回。用一个可见性实验来确认。

假设当前表里的值是 (1,2)。

MySQL实战45讲 15「终于解决」

session A 的第二个 select 语句是一致性读(快照读),它是不能看见 session B 的更新的。

现在它返回的是 (1,3),表示它看见了某个新的版本,这个版本只能是 session A 自己的 update 语句做更新的时候生成。

原文地址:https://www.cnblogs.com/ydssx7/archive/2022/07/25/16517601.html

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
转载请注明出处: https://daima100.com/4966.html

(0)
上一篇 2023-05-28 15:30
下一篇 2023-05-28

相关推荐

  • Python环境变量设置之添加py文件路径

    Python环境变量设置之添加py文件路径环境变量是操作系统提供的一种在不同进程之间传递参数的机制,它记录了操作系统的参数和当前运行环境的一些相关信息,如路径、用户、库等,Python就利用了环境变量来管理它的模块搜索路径和执行文件搜索路径。在Python中,我们需要将自己的Py文件路径添加到环境变量中,才能让Python识别和调用我们写的模块文件。

    2024-01-17
    110
  • Python如何清空列表

    Python如何清空列表
    列表是Python中非常常用的一种数据类型,它可以容纳多个元素,并且可以通过索引访问到其中的每个元素。
    在Python中,我们经常会遇到需要清空列表数据的情况,比如说在重新使用列表之前,需要将之前的数据清空掉。
    本文将介绍Python中常用的几种清空列表的方法,并且对它们进行详细的讲解。

    2024-05-12
    91
  • 探究redis两种持久化方式下的数据恢复方法_redis开启持久化

    探究redis两种持久化方式下的数据恢复方法_redis开启持久化对长期奋战在一线的后端开发人员来说,都知道redis有两种持久化方式RDB和AOF,虽说大家都知道这两种方式大概运作方式,但想必有实操了解得不会太多。 这里是自己实操两种持久化方式的一点点记录。 先看

    2023-01-23
    145
  • 可查看当前数据库中的所有视图和表_sqlserver实例根目录

    可查看当前数据库中的所有视图和表_sqlserver实例根目录SQL脚本 /*************1:删除临时表*************/ if exists(select * from tempdb..sysobjects where id=object

    2023-01-31
    149
  • 一致性读 当前读_update和upgrade

    一致性读 当前读_update和upgrade作者:赵黎明 爱可生 MySQL DBA 团队成员,Oracle 10g OCM,MySQL 5.7 OCP,擅长数据库性能问题诊断、事务与锁问题的分析等,负责处理客户 MySQL 及我司自研 DM…

    2023-03-15
    149
  • 🔨Deepin 安装 MySQL「终于解决」

    🔨Deepin 安装 MySQL「终于解决」 🔨Deepin 安装 MySQL 参考文章:https://blog.csdn.net/sinat_37064286/article/details/82224562 1. 检查是否安装过…

    2023-03-25
    154
  • 巨杉Tech | SequoiaDB数据域及存储规划

    巨杉Tech | SequoiaDB数据域及存储规划1 背景 近年来,企业的各项业务发展迅猛,客户数目不断增加,后台服务系统压力也越来越大,系统的各项硬件资源也变得非常紧张。因此,在技术风险可控的基础上,希望引入大数据技术,利用大数据技术优化现有IT…

    2022-12-15
    148
  • Python简单易学,广泛应用于数据分析和自然语言处理

    Python简单易学,广泛应用于数据分析和自然语言处理Python是一种高级编程语言,具有简单易学、可读性强等特点,因此在编程初学者中颇受欢迎。但Python不仅仅是“入门级”语言,它也是一种功能丰富的语言,被广泛应用于数据分析和自然语言处理领域。

    2023-12-16
    118

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注