在今天这篇答疑文章更新前,MySQL实战这个专栏已经更新了14篇。在这些文章中,你们在评论区留下了不少高质量的留言。如今,每篇文章的评论区都有热心的同窗帮忙总结文章知识点,也有很多同窗提出了不少高质量的问题,更有一些同窗帮忙解答其余同窗提出的问题。mysql
在浏览这些留言并回复的过程当中,我倍受鼓舞,也尽我所知地帮助你解决问题、和你讨论。能够说,大家的留言活跃了整个专栏的氛围、提高了整个专栏的质量,谢谢大家。sql
评论区的大多数留言我都直接回复了,对于须要展开说明的问题,我都拿出小本子记了下来。这些被记下来的问题,就是咱们今天这篇答疑文章的素材了。数据库
到目前为止,我已经收集了47个问题,很难经过今天这一篇文章所有展开。因此,我就先从中找了几个联系很是紧密的问题,串了起来,但愿能够帮你解决关于日志和索引的一些疑惑。而其余问题,咱们就留着后面慢慢展开吧。session
我在第2篇文章《日志系统:一条SQL更新语句是如何执行的?》中,和你讲到binlog(归档日志)和redo log(重作日志)配合崩溃恢复的时候,用的是反证法,说明了若是没有两阶段提交,会致使MySQL出现主备数据不一致等问题。并发
在这篇文章下面,不少同窗在问,在两阶段提交的不一样瞬间,MySQL若是发生异常重启,是怎么保证数据完整性的?分布式
如今,咱们就从这个问题开始吧。性能
我再放一次两阶段提交的图,方便你学习下面的内容。学习
这里,我要先和你解释一个误会式的问题。有同窗在评论区问到,这个图不是一个update语句的执行流程吗,怎么还会调用commit语句?优化
他产生这个疑问的缘由,是把两个“commit”的概念混淆了:spa
而咱们这个例子里面,没有显式地开启事务,所以这个update语句本身就是一个事务,在执行完成后提交事务时,就会用到这个“commit步骤“。
接下来,咱们就一块儿分析一下在两阶段提交的不一样时刻,MySQL异常重启会出现什么现象。
若是在图中时刻A的地方,也就是写入redo log 处于prepare阶段以后、写binlog以前,发生了崩溃(crash),因为此时binlog还没写,redo log也还没提交,因此崩溃恢复的时候,这个事务会回滚。这时候,binlog还没写,因此也不会传到备库。到这里,你们均可以理解。
你们出现问题的地方,主要集中在时刻B,也就是binlog写完,redo log还没commit前发生crash,那崩溃恢复的时候MySQL会怎么处理?
咱们先来看一下崩溃恢复时的判断规则。
若是redo log里面的事务是完整的,也就是已经有了commit标识,则直接提交;
若是redo log里面的事务只有完整的prepare,则判断对应的事务binlog是否存在并完整:
a. 若是是,则提交事务;
b. 不然,回滚事务。
这里,时刻B发生crash对应的就是2(a)的状况,崩溃恢复过程当中事务会被提交。
如今,咱们继续延展一下这个问题。
回答:一个事务的binlog是有完整格式的:
另外,在MySQL 5.6.2版本之后,还引入了binlog-checksum参数,用来验证binlog内容的正确性。对于binlog日志因为磁盘缘由,可能会在日志中间出错的状况,MySQL能够经过校验checksum的结果来发现。因此,MySQL仍是有办法验证事务binlog的完整性的。
回答:它们有一个共同的数据字段,叫XID。崩溃恢复的时候,会按顺序扫描redo log:
回答:其实,这个问题仍是跟咱们在反证法中说到的数据与备份的一致性有关。在时刻B,也就是binlog写完之后MySQL发生崩溃,这时候binlog已经写入了,以后就会被从库(或者用这个binlog恢复出来的库)使用。
因此,在主库上也要提交这个事务。采用这个策略,主库和备库的数据就保证了一致性。
回答:其实,两阶段提交是经典的分布式系统问题,并非MySQL独有的。
若是必需要举一个场景,来讲明这么作的必要性的话,那就是事务的持久性问题。
对于InnoDB引擎来讲,若是redo log提交完成了,事务就不能回滚(若是这还容许回滚,就可能覆盖掉别的事务的更新)。而若是redo log直接提交,而后binlog写入的时候失败,InnoDB又回滚不了,数据和binlog日志又不一致了。
两阶段提交就是为了给全部人一个机会,当每一个人都说“我ok”的时候,再一块儿提交。
回答:这位同窗的意思是,只保留binlog,而后能够把提交流程改为这样:… -> “数据更新到内存” -> “写 binlog” -> “提交事务”,是否是也能够提供崩溃恢复的能力?
答案是不能够。
若是说历史缘由的话,那就是InnoDB并非MySQL的原生存储引擎。MySQL的原生引擎是MyISAM,设计之初就有没有支持崩溃恢复。
InnoDB在做为MySQL的插件加入MySQL引擎家族以前,就已是一个提供了崩溃恢复和事务支持的引擎了。
InnoDB接入了MySQL后,发现既然binlog没有崩溃恢复的能力,那就用InnoDB原有的redo log好了。
而若是说实现上的缘由的话,就有不少了。就按照问题中说的,只用binlog来实现崩溃恢复的流程,我画了一张示意图,这里就没有redo log了。
这样的流程下,binlog仍是不能支持崩溃恢复的。我说一个不支持的点吧:binlog没有能力恢复“数据页”。
若是在图中标的位置,也就是binlog2写完了,可是整个事务尚未commit的时候,MySQL发生了crash。
重启后,引擎内部事务2会回滚,而后应用binlog2能够补回来;可是对于事务1来讲,系统已经认为提交完成了,不会再应用一次binlog1。
可是,InnoDB引擎使用的是WAL技术,执行事务的时候,写完内存和日志,事务就算完成了。若是以后崩溃,要依赖于日志来恢复数据页。
也就是说在图中这个位置发生崩溃的话,事务1也是可能丢失了的,并且是数据页级的丢失。此时,binlog里面并无记录数据页的更新细节,是补不回来的。
你若是要说,那我优化一下binlog的内容,让它来记录数据页的更改能够吗?但,这其实就是又作了一个redo log出来。
因此,至少如今的binlog能力,还不能支持崩溃恢复。
回答:若是只从崩溃恢复的角度来说是能够的。你能够把binlog关掉,这样就没有两阶段提交了,但系统依然是crash-safe的。
可是,若是你了解一下业界各个公司的使用场景的话,就会发如今正式的生产库上,binlog都是开着的。由于binlog有着redo log没法替代的功能。
一个是归档。redo log是循环写,写到末尾是要回到开头继续写的。这样历史日志无法保留,redo log也就起不到归档的做用。
一个就是MySQL系统依赖于binlog。binlog做为MySQL一开始就有的功能,被用在了不少地方。其中,MySQL系统高可用的基础,就是binlog复制。
还有不少公司有异构系统(好比一些数据分析系统),这些系统就靠消费MySQL的binlog来更新本身的数据。关掉binlog的话,这些下游系统就无法输入了。
总之,因为如今包括MySQL高可用在内的不少系统机制都依赖于binlog,因此“鸠占鹊巢”redo log还作不到。你看,发展生态是多么重要。
回答:redo log过小的话,会致使很快就被写满,而后不得不强行刷redo log,这样WAL机制的能力就发挥不出来了。
因此,若是是如今常见的几个TB的磁盘的话,就不要过小气了,直接将redo log设置为4个文件、每一个文件1GB吧。
回答:这个问题其实问得很是好。这里涉及到了,“redo log里面究竟是什么”的问题。
实际上,redo log并无记录数据页的完整数据,因此它并无能力本身去更新磁盘数据页,也就不存在“数据最终落盘,是由redo log更新过去”的状况。
若是是正常运行的实例的话,数据页被修改之后,跟磁盘的数据页不一致,称为脏页。最终数据落盘,就是把内存中的数据页写盘。这个过程,甚至与redo log毫无关系。
在崩溃恢复场景中,InnoDB若是判断到一个数据页可能在崩溃恢复的时候丢失了更新,就会将它读到内存,而后让redo log更新内存内容。更新完成后,内存页变成脏页,就回到了第一种状况的状态。
回答:这两个问题能够一块儿回答。
在一个事务的更新过程当中,日志是要写屡次的。好比下面这个事务:
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消耗。可是可能会出现“被动写入磁盘”,好比内存不够、其余事务提交等状况。这个问题咱们会在后面第22篇文章《MySQL有哪些“饮鸩止渴”的提升性能的方法?》中再详细展开)。
单独执行一个更新语句的时候,InnoDB会本身启动一个事务,在语句执行完成的时候提交。过程跟上面是同样的,只不过是“压缩”到了一个语句里面完成。
以上这些问题,就是把你们提过的关于redo log和binlog的问题串起来,作的一次集中回答。若是你还有问题,能够在评论区继续留言补充。
接下来,我再和你分享@ithunter 同窗在第8篇文章《事务究竟是隔离的仍是不隔离的?》的评论区提到的跟索引相关的一个问题。我以为这个问题挺有趣、也挺实用的,其余同窗也可能会碰上这样的场景,在这里解答和分享一下。
问题是这样的(我文字上稍微作了点修改,方便你们理解):
业务上有这样的需求,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锁层面有没有办法处理?
首先,我要先赞一下这样的提问方式。虽然极客时间如今的评论区还不能追加评论,但若是你们可以一次留言就把问题讲清楚的话,其实影响也不大。因此,我但愿你在留言提问的时候,也能借鉴这种方式。
接下来,我把@ithunter 同窗说的表模拟出来,方便咱们讨论。
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;
虽然这个题干中,并无说到friend表的索引结构。但我猜想friend_1_id和friend_2_id也有索引,为便于描述,我给加上惟一索引。
顺便说明一下,“like”是关键字,我通常不建议使用关键字做为库名、表名、字段名或索引名。
我把他的疑问翻译一下,在并发场景下,同时有两我的,设置为关注对方,就可能致使没法成功加为朋友关系。
如今,我用你已经熟悉的时刻顺序表的形式,把这两个事务的执行语句列出来:
因为一开始A和B之间没有关注关系,因此两个事务里面的select语句查出来的结果都是空。
所以,session 1的逻辑就是“既然B没有关注A,那就只插入一个单向关注关系”。session 2也一样是这个逻辑。
这个结果对业务来讲就是bug了。由于在业务设定里面,这两个逻辑都执行完成之后,是应该在friend表里面插入一行记录的。
如提问里面说的,“第1步即便使用了排他锁也不行,由于记录不存在,行锁没法生效”。不过,我想到了另一个方法,来解决这个问题。
首先,要给“like”表增长一个字段,好比叫做 relation_ship,并设为整型,取值一、二、3。
值是1的时候,表示user_id 关注 liker_id;
值是2的时候,表示liker_id 关注 user_id;
值是3的时候,表示互相关注。
而后,当 A关注B的时候,逻辑改为以下所示的样子:
应用代码里面,比较A和B的大小,若是A<B,就执行下面的逻辑
mysql> 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,则执行下面的逻辑
mysql> 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的这条记录。
不知道你会不会吐槽:以前明明还说尽可能不要使用惟一索引,结果这个例子一上来我就建立了两个。这里我要再和你说明一下,以前文章咱们讨论的,是在“业务开发保证不会插入重复记录”的状况下,着重要解决性能问题的时候,才建议尽可能使用普通索引。
而像这个例子里,按照这个设计,业务根本就是保证“我必定会插入重复数据,数据库必定要要有惟一性约束”,这时就没啥好说的了,惟一索引建起来吧。
这是专栏的第一篇答疑文章。
我针对前14篇文章,你们在评论区中的留言,从中摘取了关于日志和索引的相关问题,串成了今天这篇文章。这里我也要再和你说一声,有些我答应在答疑文章中进行扩展的话题,今天这篇文章没来得及扩展,后续我会再找机会为你解答。因此,篇幅所限,评论区见吧。
最后,虽然这篇是答疑文章,但课后问题仍是要有的。
咱们建立了一个简单的表t,并插入一行,而后对这一行作修改。
mysql> 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)。假设,我如今要执行:
mysql> update t set a=2 where id=1;
你会看到这样的结果:
结果显示,匹配(rows matched)了一行,修改(Changed)了0行。
仅从现象上看,MySQL内部在处理这个命令的时候,能够有如下三种选择:
更新都是先读后写的,MySQL读出数据,发现a的值原本就是2,不更新,直接返回,执行结束;
MySQL调用了InnoDB引擎提供的“修改成(1,2)”这个接口,可是引擎发现值与原来相同,不更新,直接返回;
InnoDB认真执行了“把这个值修改为(1,2)"这个操做,该加锁的加锁,该更新的更新。
你以为实际状况会是以上哪一种呢?你能否用构造实验的方式,来证实你的结论?进一步地,能够思考一下,MySQL为何要选择这种策略呢?
你能够把你的验证方法和思考写在留言区里,我会在下一篇文章的末尾和你讨论这个问题。感谢你的收听,也欢迎你把这篇文章分享给更多的朋友一块儿阅读。
上期的问题是,用一个计数表记录一个业务表的总行数,在往业务表插入数据的时候,须要给计数值加1。
逻辑实现上是启动一个事务,执行两个语句:
insert into 数据表;
update 计数表,计数值加1。
从系统并发能力的角度考虑,怎么安排这两个语句的顺序。
这里,我直接复制 @阿建 的回答过来供你参考:
并发系统性能的角度考虑,应该先插入操做记录,再更新计数表。
知识点在《行锁功过:怎么减小行锁对性能的影响?》
由于更新计数表涉及到行锁的竞争,先插入再更新能最大程度地减小事务之间的锁等待,提高并发度。
评论区有同窗说,应该把update计数表放后面,由于这个计数表可能保存了多个业务表的计数值。若是把update计数表放到事务的第一个语句,多个业务表同时插入数据的话,等待时间会更长。
这个答案的结论是对的,可是理解不太正确。即便咱们用一个计数表记录多个业务表的行数,也确定会给表名字段加惟一索引。相似于下面这样的表结构:
CREATE TABLE `rows_stat` ( `table_name` varchar(64) NOT NULL, `row_count` int(10) unsigned NOT NULL, PRIMARY KEY (`table_name`) ) ENGINE=InnoDB;
在更新计数表的时候,必定会传入where table_name=$table_name,使用主键索引,更新加行锁只会锁在一行上。
而在不一样业务表插入数据,是更新不一样的行,不会有行锁。