MVCC又是一个难嚼的大块头。MVCC就是用来实现上面的第三个隔离级别,可重复读RR。
MVCC:Multi-Version Concurrency Control,即多版本的并发控制协议。
MVCC的特点就是在同一时刻,不同事务可以读取到不同版本的数据,从而可以解决脏读和不可重复读的问题。MVCC实际上就是通过数据的隐藏列和回滚日志(undo log),实现多个版本数据的共存。这样的好处是,使用MVCC进行读数据的时候,不用加锁,从而避免了同时读写的冲突。
在实现MVCC时,每一行的数据中会额外保存几个隐藏的列,比如当前行创建时的版本号和删除时间和指向undo log的回滚指针。这里的版本号并不是实际的时间值,而是系统版本号。每开始新的事务,系统版本号都会自动递增。事务开始时的系统版本号会作为事务的版本号,用来和查询每行记录的版本号进行比较。
每个事务又有自己的版本号,这样事务内执行数据操作时,就通过版本号的比较来达到数据版本控制的目的。
另外,InnoDB实现的隔离级别RR时可以避免幻读现象的,这是通过next-key lock
机制实现的。
next-key lock
实际上就是行锁的一种,只不过它不只是会锁住当前行记录的本身,还会锁定一个范围。比如上面幻读的例子,开始查询0<阅读量<100的文章时,只查到了一个结果。next-key lock
会将查询出的这一行进行锁定,同时还会对0<阅读量<100这个范围进行加锁,这实际上是一种间隙锁。间隙锁能够防止其他事务在这个间隙修改或者插入记录。这样一来,就保证了在0<阅读量<100这个间隙中,只存在原来的一行数据,从而避免了幻读。
间隙锁:封锁索引记录中的间隔
虽然InnoDB使用next-key lock
能够避免幻读问题,但却并不是真正的可串行化隔离。再来看一个例子吧。
首先提一个问题:
在T6时间,事务A提交事务之后,猜一猜文章A和文章B的阅读量为多少?
答案是,文章AB的阅读量都被修改成了10000。这代表着事务B的提交实际上对事务A的执行产生了影响,表明两个事务之间并不是完全隔离的。虽然能够避免幻读现象,但是却没有达到可串行化的级别。这还说明,避免脏读、不可重复读和幻读,是达到可串行化的隔离级别的必要不充分条件。可串行化是都能够避免脏读、不可重复读和幻读,但是避免脏读、不可重复读和幻读却不一定达到了可串行化。
一致性
一致性是指事务执行结束后,数据库的完整性约束没有被破坏,事务执行的前后都是合法的数据状态。一致性是事务追求的最终目标,原子性、持久性和隔离性,实际上都是为了保证数据库状态的一致性而存在的。这就不多说了吧。你细品。
4 MySQL日志系统
了解完MySQL的基本架构,大体上能够对MySQL的执行流程有了比较清晰的认知。接下来我将为大家介绍一下日志系统。MySQL日志系统是数据库的重要组件,用于记录数据库的更新和修改。若数据库发生故障,可通过不同日志记录恢复数据库的原来数据。因此实际上日志系统直接决定着MySQL运行的鲁棒性和稳健性。MySQL的日志有很多种,如二进制日志(binlog)、错误日志、查询日志、慢查询日志等,此外InnoDB存储引擎还提供了两种日志:redo log(重做日志)和undo log(回滚日志)。这里将重点针对InnoDB引擎,对重做日志、回滚日志和二进制日志这三种进行分析。
重做日志(redo log)
重做日志(redo log)是InnoDB引擎层的日志,用来记录事务操作引起数据的变化,记录的是数据页的物理修改。重做日记的作用其实很好理解,我打个比方。数据库中数据的修改就好比你写的论文,万一哪天论文丢了怎么呢?以防这种不幸的发生,我们可以在写论文的时候,每一次修改都拿个小本本记录一下,记录什么时间对某一页进行了怎么样的修改。这就是重做日志。
InnoDB引擎对数据的更新,是先将更新记录写入redo log日志,然后会在系统空闲的时候或者是按照设定的更新策略再将日志中的内容更新到磁盘之中。这就是所谓的预写式技术(Write Ahead logging)。这种技术可以大大减少IO操作的频率,提升数据刷新的效率。
脏数据刷盘
值得注意的是,redo log日志的大小是固定的,为了能够持续不断的对更新记录进行写入,在redo log日志中设置了两个标志位置,checkpoint和write_pos,分别表示记录擦除的位置和记录写入的位置。redo log日志的数据写入示意图可见下图。
当write_pos标志到了日志结尾时,会从结尾跳至日志头部进行重新循环写入。所以redo log的逻辑结构并不是线性的,而是可看作一个圆周运动。write_pos与checkpoint中间的空间可用于写入新数据,写入和擦除都是往后推移,循环往复的。
当write_pos
追上checkpoint
时,表示redo log日志已经写满。这时不能继续执行新的数据库更新语句,需要停下来先删除一些记录,执行checkpoint
规则腾出可写空间。
checkpoint规则:checkpoint触发后,将buffer中脏数据页和脏日志页都刷到磁盘。 脏数据:指内存中未刷到磁盘的数据。
redo log中最重要的概念就是缓冲池buffer pool
,这是在内存中分配的一个区域,包含了磁盘中部分数据页的映射,作为访问数据库的缓冲。
当请求读取数据时,会先判断是否在缓冲池命中,如果未命中才会在磁盘上进行检索后放入缓冲池;当请求写入数据时,会先写入缓冲池,缓冲池中修改的数据会定期刷新到磁盘中。这一过程也被称之为 刷脏 。
因此,当数据修改时,除了修改buffer pool
中的数据,还会在redo log中记录这次操作;当事务提交时,会根据redo log的记录对数据进行刷盘。如果MySQL宕机,重启时可以读取redo log中的数据,对数据库进行恢复,从而保证了事务的持久性,使得数据库获得crash-safe
能力。脏日志刷盘除了上面提到的对于脏数据的刷盘,实际上redo log日志在记录时,为了保证日志文件的持久化,也需要经历将日志记录从内存写入到磁盘的过程。redo log日志可分为两个部分,一是存在易失性内存中的缓存日志redo log buff
,二是保存在磁盘上的redo log日志文件redo log file
。为了确保每次记录都能够写入到磁盘中的日志中,每次将redo log buffer
中的日志写入redo log file
的过程中都会调用一次操作系统的fsync
操作。
fsync函数:包含在UNIX系统头文件#include <unistd.h>中,用于同步内存中所有已修改的文件数据到储存设备。
在写入的过程中,还需要经过操作系统内核空间的os buffer
。redo log日志的写入过程可见下图。
二进制日志(binlog)
二进制日志binlog是服务层的日志,还被称为归档日志。binlog主要记录数据库的变化情况,内容包括数据库所有的更新操作。所有涉及数据变动的操作,都要记录进二进制日志中。因此有了binlog可以很方便的对数据进行复制和备份,因而也常用作主从库的同步。
这里binlog所存储的内容看起来似乎与redo log很相似,但是其实不然。redo log是一种物理日志,记录的是实际上对某个数据进行了怎么样的修改;而binlog是逻辑日志,记录的是SQL语句的原始逻辑,比如”给ID=2这一行的a字段加1 "。binlog日志中的内容是二进制的,根据日记格式参数的不同,可能基于SQL语句、基于数据本身或者二者的混合。一般常用记录的都是SQL语句。这里的物理和逻辑的概念,我的个人理解是:
物理的日志可看作是实际数据库中数据页上的变化信息,只看重结果,而不在乎是通过“何种途径”导致了这种结果;逻辑的日志可看作是通过了某一种方法或者操作手段导致数据发生了变化,存储的是逻辑性的操作。
同时,redo log是基于crash recovery
,保证MySQL宕机后的数据恢复;而binlog是基于point-in-time recovery
,保证服务器可以基于时间点对数据进行恢复,或者对数据进行备份。
事实上最开始MySQL是没有redo log日志的。因为起先MySQL是没有InnoDB引擎的,自带的引擎是MyISAM。binlog是服务层的日志,因此所有引擎都能够使用。但是光靠binlog日志只能提供归档的作用,无法提供crash-safe
能力,所以InnoDB引擎就采用了学自于Oracle的技术,也就是redo log,这才拥有了crash-safe
能力。这里对redo log日志和binlog日志的特点分别进行了对比:
在MySQL执行更新语句时,都会涉及到redo log日志和binlog日志的读写。一条更新语句的执行过程如下:
从上图可以看出,MySQL在执行更新语句的时候,在服务层进行语句的解析和执行,在引擎层进行数据的提取和存储;同时在服务层对binlog进行写入,在InnoDB内进行redo log的写入。
不仅如此,在对redo log写入时有两个阶段的提交,一是binlog写入之前prepare
状态的写入,二是binlog写入之后commit
状态的写入。
之所以要安排这么一个两阶段提交,自然是有它的道理的。现在我们可以假设不采用两阶段提交的方式,而是采用“单阶段”进行提交,即要么先写入redo log,后写入binlog;要么先写入binlog,后写入redo log。这两种方式的提交都会导致原先数据库的状态和被恢复后的数据库的状态不一致。
先写入redo log,后写入binlog:
在写完redo log之后,数据此时具有crash-safe
能力,因此系统崩溃,数据会恢复成事务开始之前的状态。但是,若在redo log写完时候,binlog写入之前,系统发生了宕机。此时binlog没有对上面的更新语句进行保存,导致当使用binlog进行数据库的备份或者恢复时,就少了上述的更新语句。从而使得id=2
这一行的数据没有被更新。
先写入binlog,后写入redo log:
写完binlog之后,所有的语句都被保存,所以通过binlog复制或恢复出来的数据库中id=2这一行的数据会被更新为a=1。但是如果在redo log写入之前,系统崩溃,那么redo log中记录的这个事务会无效,导致实际数据库中id=2
这一行的数据并没有更新。
由此可见,两阶段的提交就是为了避免上述的问题,使得binlog和redo log中保存的信息是一致的。
回滚日志(undo log)
回滚日志同样也是InnoDB引擎提供的日志,顾名思义,回滚日志的作用就是对数据进行回滚。当事务对数据库进行修改,InnoDB引擎不仅会记录redo log,还会生成对应的undo log日志;如果事务执行失败或调用了rollback,导致事务需要回滚,就可以利用undo log中的信息将数据回滚到修改之前的样子。但是undo log不redo log不一样,它属于逻辑日志。
它对SQL语句执行相关的信息进行记录。当发生回滚时,InnoDB引擎会根据undo log日志中的记录做与之前相反的工作。比如对于每个数据插入操作(insert),回滚时会执行数据删除操作(delete);对于每个数据删除操作(delete),回滚时会执行数据插入操作(insert);对于每个数据更新操作(update),回滚时会执行一个相反的数据更新操作(update),把数据改回去。undo log由两个作用,一是提供回滚,二是实现MVCC。
5 主从复制
主从复制的概念很简单,就是从原来的数据库复制一个完全一样的数据库,原来的数据库称作主数据库,复制的数据库称为从数据库。从数据库会与主数据库进行数据同步,保持二者的数据一致性。
主从复制的原理实际上就是通过bin log日志实现的。bin log日志中保存了数据库中所有SQL语句,通过对bin log日志中SQL的复制,然后再进行语句的执行即可实现从数据库与主数据库的同步。
主从复制的过程可见下图。主从复制的过程主要是靠三个线程进行的,一个运行在主服务器中的发送线程,用于发送binlog日志到从服务器。两外两个运行在从服务器上的I/O线程和SQL线程。I/O线程用于读取主服务器发送过来的binlog日志内容,并拷贝到本地的中继日志中。SQL线程用于读取中继日志中关于数据更新的SQL语句并执行,从而实现主从库的数据一致。
之所以需要实现主从复制,实际上是由实际应用场景所决定的。主从复制能够带来的好处有:
1. 通过复制实现数据的异地备份,当主数据库故障时,可切换从数据库,避免数据丢失。2. 可实现架构的扩展,当业务量越来越大,I/O访问频率过高时,采用多库的存储,可以降低磁盘I/O访问的频率,提高单个机器的I/O性能。3. 可实现读写分离,使数据库能支持更大的并发。4. 实现服务器的负载均衡,通过在主服务器和从服务器之间切分处理客户查询的负荷。
6 总结
MySQL数据库应该算是程序员必须掌握的技术之一了。无论是项目过程中还是面试中,MySQL都是非常重要的基础知识。不过,对于MySQL来说,真的东西太多了。我在写这篇文章的时候,查阅了大量的资料,发现越看不懂的越多。还真是应了那句话:
你知道的越多,不知道的也就越多。
这篇文章着重是从理论的角度去解析MySQL基本的事务和日志系统的基本原理,我在表述的时候尽可能的避免采用实际的代码去描述。即便是这篇将近一万字+近二十副纯手工绘制的图解,也难以将MySQL的博大精深分析透彻。
但是我相信,对于初学者而言,这些理论能够让你对MySQL有一个整体的感知,让你对“何谓关系型数据库”这么一个问题有了比较清晰的认知;而对于熟练掌握MySQL的大佬来说,或许本文也能够唤醒你尘封已久的底层理论基础,对你之后的面试也会有一定帮助。
技术这种东西没有绝对的对错,倘若文中有误还请谅解,并欢迎与我讨论。自主思考永远比被动接受更有效。