在现在互联网业务中使用范围最广的数据库无疑仍是关系型数据库MySQL,之因此用"仍是"这个词,是由于最近几年国内数据库领域也取得了一些长足进步,例如以TIDB、OceanBase等为表明的分布式数据库,但它们暂时尚未造成绝对的覆盖面,因此现阶段还得继续学习MySQL数据库以应对工做中遇到的一些问题,以及面试过程当中关于数据库部分的考察。面试
今天的内容就和你们聊一聊MySQL数据库中关于并发控制、事务以及存储引擎这几个最核心的问题。本内容涉及的知识图谱以下图所示:数据库
并发控制是一个内容庞大的话题,在计算机软件系统中只要在同一时刻存在多个请求同时修改数据的状况,就都会产生并发控制的问题,例如Java中的多线程安全问题等。在MySQL中的并发控制,主要是讨论数据库如何控制表数据的并发读写。缓存
例若有一张表useraccount,其结构以下:安全
此时若是有以下两条SQL语句同一时刻向数据库发起请求:服务器
SQL-A:多线程
update useraccount t set t.account=t.account+100 where username='wudimanong';
SQL-B: 架构
update useraccount t set t.account=t.account-100 where username='wudimanong'
当上述语句都执行完成,正确结果应该是account=100,但在并发状况下,却有可能发生这样的状况:并发
那么在MySQL中是如何进行并发控制的呢?实际上与大多数并发控制方式同样,在MySQL中也是利用锁机制来实现并发控制的。分布式
1.MySQL锁类型ide
在MySQL中主要是经过"读写锁"来实现并发控制。
读锁(read lock):也叫共享锁(share lock),多个读请求能够同时共享一把锁来读取数据,而不会形成阻塞。
写锁(write lock):也叫排他锁(exclusive lock),写锁会排斥其余全部获取锁的请求,一直阻塞,直到完成写入并释放锁。
读写锁能够作到读读并行,可是没法作到写读、写写并行。后面会讲到的事务隔离性就是根据读写锁来实现的!
2.MySQL锁粒度
上面说起的读写锁是根据MySQL的锁类型来划分的,而读写锁可以施加的粒度在数据库中主要体现为表和行,也称为表锁(table lock)、行锁(row lock)。
表锁(table lock):是MySQL中最基本的锁策略,它会锁定整张表,这样维护锁的开销最小,可是会下降表的读写效率。若是一个用户经过表锁来实现对表的写操做(插入、删除、更新),那么先须要得到锁定该表的写锁,那么在这种状况下,其余用户对该表的读写都会被阻塞。通常状况下"alter table"之类的语句才会使用表锁。
行锁(row lock):行锁能够最大程度地支持并发读写,但数据库维护锁的开销会比较大。行锁是咱们平常使用最多的锁策略,通常状况下MySQL中的行级锁由具体的存储引擎实现,而不是MySQL服务器层面去实现(表锁MySQL服务器层面会实现)。
3.多版本并发控制(MVCC)
MVCC(MultiVersion Concurrency Control),多版本并发控制。在MySQL的大多数事务引擎(如InnoDB)中,都不仅是简单地实现了行级锁,不然会出现这样的状况:"数据A被某个用户更新期间(获取行级写锁),其余用户读取该条数据(获取读锁)都会被阻塞“。但现实状况显然不是这样,这是由于MySQL的存储引擎基于提高并发性能的考虑,经过MVCC数据多版本控制,作到了读写分离,从而实现不加锁读取数据进而作到了读写并行。
以InnoDB存储引擎的MVCC实现为例:
InnoDB的MVCC,是经过在每行记录后面保存两个隐藏的列来实现的。这两个列,一个保存了行的建立时间,一个保存了行的过时时间。固然它们存储的并非实际的时间值,而是系统版本号。每开启一个新的事务,系统版本号都会自动递增;事务开始时刻的系统版本号会做为事务的版本号,用来和查询到的每行记录的版本号进行比较。
MVCC在MySQL中实现所依赖的手段主要是:"undo log和read view"。
undo log :undo log 用于记录某行数据的多个版本的数据。
undo log在后面讲述事务还会介绍到。关于MVCC的读写原理示意图以下:
上图演示了MySQL InnoDB存储引擎,在REPEATABLE READ(可重复读)事务隔离级别下,经过额外保存两个系统版本号(行建立版本号、行删除版本号)实现MVCC,从而使得大多数读操做均可以不用再加读锁。这样的设计使得数据读取操做更加简单、性能更好。
那么在MVCC模式下数据读取操做是如何保证数据读取正确的呢?以InnoDB为例,Select时会根据如下两个条件检查每行记录:
只查找版本号小于或等于当前事务版本的数据行,这样能够确保事务读取的行要么是在事务开始前已经存在,要么是事务自身插入或者修过的。
只有符合上述两个条件的记录,才能返回做为查询的结果!以图中示范的逻辑为例,写请求将account变动为200的过程当中,InnoDB会再插入一行新记录(account=200),并将当前系统版本号做为行建立版本号(createVersion=2),同时将当前系统版本号做为原来行的行删除版本号(deleteVersion=2),那么此时关于这条数据有两个版本的数据副本,具体以下:
假如如今写操做还未结束,事务对其余用户暂不可见,按照Select检查条件只有accout=100的记录才符合条件,所以查询结果会返回account=100的记录!
上述过程就是InnoDB存储引擎关于MVCC实现的基本原理,可是后面须要注意MVCC多版本并发控制的逻辑只能工做在“REPEATABLE READ(可重复读)和READ COMMITED(提交读)”两种事务隔离级别下。其余两个隔离级别都与MVCC不兼容,由于READ UNCOMMITED(未提交读)老是读取最新的数据行,而不是符合当前事务版本的数据行;而SERIALIZABLE则会对全部读取的行都加锁,也不符合MVCC的思想。
前面在讲解了关于MySQL并发控制的过程当中,也提到了事务相关的内容,接下来咱们来更全面的梳理下关于事务的核心知识。
相信你们在平常的开发过程当中,都使用过数据库事务,对事务的特色也都能张口就来——ACID。那么事务内部究竟是怎么实现的呢?在接下来的内容中,就来和你们具体聊一聊这个问题!
1.事务概述
数据库事务自己所要达成的效果主要体如今:"可靠性"以及"并发处理"这两个方面。
可靠性:数据库要保证当insert或update操做抛出异常,或者数据库crash的时候要保障数据操做的先后一致。
实现MySQL数据库事务功能主要有三个技术,分别是日志文件(redo log和undo log)、锁技术及MVCC。
2.redo log与undo log
redo log与undo log是实现MySQL事务功能的核心技术。
1)、redo log
redo log叫作重作日志,是实现事务持久性的关键。redo log日志文件主要由2部分组成:重作日志缓冲(redo log buffer)、重作日志文件(redo log file)。
在MySql中为了提高数据库性能并不会把每次的修改都实时同步到磁盘,而是会先存到一个叫作“Boffer Pool”的缓冲池中,以后会再使用后台线程去实现缓冲池和磁盘之间的同步。
若是采起这样的模式,可能会出现这样的问题:若是在数据还没来得及同步的状况下出现宕机或断电,那么就可能会丢失部分已提交事务的修改信息!而这种状况对于数据库软件来讲是不能够接受的。
因此redo log的主要做用就是用来记录已成功提交事务的修改信息,而且会在事务提交后实时将redo log持久化到磁盘,这样在系统重启以后就能够读取redo log来恢复最新的数据。
接下来咱们之前面SQL-A所开启的事务为例来演示redo log的具体是如何运行的,以下图所示:
如上图所示,当修改一行记录的事务开启,MySQL存储引擎是把数据从磁盘读取到内存的缓冲池上进行修改,这个时候数据在内存中被修改后就与磁盘中的数据产生了差别,这种有差别的数据也被称之为“脏页”。
而通常存储引擎对于脏页的处理并非每次生成脏页就即刻将脏页刷新回磁盘,而是经过后台线程“master thread”以大体每秒运行一次或每10秒运行一次的频率去刷新磁盘。在这种状况下,出现数据库宕机或断电等状况,那么还没有刷新回磁盘的数据就有可能丢失。
而redo log日志的做用就是为了调和内存与磁盘的速度差别。当事务被提交时,存储引擎会首先将要修改的数据写入redo log,而后再去修改缓冲池中真正的数据页,并实时刷新一次数据同步。若是在这个过程当中,数据库挂了,因为redo log物理日志文件已经记录了事务修改,因此在数据库重启后就能够根据redo log日志进行事务数据恢复。
2)、undo log
上面咱们聊了redo log日志,它的做用主要是用来恢复数据,保障已提交事务的持久化特性。在MySQL中还有另一种很是重要的日志类型undo log,又叫回滚日志,它主要是用于记录数据被修改前的信息,这与记录数据被修改后信息的redo log日志正好相反。
undo log 主要记录事务修改以前版本的数据信息,假如因为系统错误或者rollback操做而回滚的话就能够根据undo log日志来将数据回滚到没被修改以前的状态。
每次写入数据或者修改数据以前存储引擎都会将修改前的信息记录到undo log。
3.事务的实现
前面咱们讲到了锁、多版本并发控制(MVCC)、重作日志(redo log)以及回滚日志(undo log),这些内容就是MySQL实现数据库事务的基础。从事务的四大特性来讲,其对应关系主要体现以下:
实际上事务原子性、持久性、隔离性的最终目的都是为了确保事务数据的一致性。而ACID只是个概念,事务的最终目的是要保障数据的可靠性和一致性。
接下来咱们再具体分析下事务ACID特性的实现原理。
1)、原子性的实现
原子性,是指一个事务必须被视为不可分割的最小单位,一个事务中的全部操做要么所有执行成功、要么所有失败回滚,对一个事务来讲不可能只执行其中的部分操做,这就是事务原子性的概念。
而MySQL数据库实现原子性的主要是经过回滚操做来实现的。所谓回滚操做就是当发生错误异常或者显示地执行rollback语句时须要把数据还原到原先的模样,而这个过程就须要借助undo log来进行。具体规则以下:
每条数据变动(insert/update/delete)操做都伴随着一条undo log的生成,而且回滚日志必须先于数据持久化到磁盘上;
2)、持久性的实现
持久性,指的是事务一旦提交其所做的修改会永久地保存到数据库中,此时即便系统崩溃修改的数据也不会丢失。
事务的持久性主要是经过redo log日志来实现的。redo log日志之因此可以弥补缓存同步所形成的数据差别,主要其具有如下特色:
redo log的存储是顺序的,而缓存同步则是随机操做;
关于redo log实现事务持久性的逻辑可参考本文前面关于redo log部分的内容!
3)、隔离性的实现
隔离性是事务ACID特性中最复杂的一个。在SQL标准里定义了四种隔离级别,每一种隔离级别都规定一个事务中的修改,那些是事务之间可见的,那些是不可见的。
MySQL隔离级别有如下四种(级别由低到高):
READ UNCOMMITED (未提交读);
READ COMMITED (提交读)
REPEATABLE READ (可重复读)
隔离级别越低,则数据库能够执行的并发度越高,可是实现的复杂度和开销也越大。只要完全理解了隔离级别以及它的实现原理,就至关于理解了ACID中的事务隔离性。
前面提到过,原子性、持久性、隔离性的目的最终都是为了实现数据的一致性,但隔离性与其它两个有所区别,原子性和持久性主要是为了保障数据的可靠性,好比作到宕机后的数据恢复,以及错误后的数据回滚。而隔离性的核心目标则是要管理多个并发读写请求的访问顺序,实现数据库数据的安全和高效访问,实质上就是一场数据的安全性与性能之间的权衡游戏。
可靠性高的隔离级别,并发性能低(例如SERIALIZABLE隔离级别,由于全部的读写都会加锁);而可靠性低的,并发性能高(例如READ UNCOMMITED,由于读写彻底不加锁)。
接下来咱们再分别分析下这四种隔离级别的特色:
READ UNCOMMITTED
在READ UNCOMMITTED隔离级别下,一个事务中的修改即便尚未提交,对其它事务也是可见,也就是说事务能够读取到未提交的数据。
由于读不会添加锁,因此写操做在读的过程当中修改数据的话会形成"脏读"。未提交读隔离级别读写示意图以下:
如上图所示,写请求将account修改成200,此时事务未提交;可是读请求能够读取到未提交的事务数据account=200;随后写请求事务失败回滚account=100;那么此时读请求读取的account=200的数据就是脏数据。
这种隔离级别的优势是读写并行、性能高;可是缺点是容易形成脏读。因此在MySQL数据库中通常状况下并不会采起此种隔离级别!
READ COMMITED
这种事务隔离级别也叫"不可重复读或提交读"。它的特色是一个事务在它提交以前的全部修改,其它事务都是不可见的;其它事务只能读到已提交的修改变化。
这种隔离级别看起来很完美,也符合大部分逻辑场景,但该事务隔离级别会产生"不可重读"和"幻读"的问题。
不可重读:是指一个事务内屡次读取的相同行的数据,结果却不同。例如事务A读取a行数据,而事务B此时修改了a行的数据并提交了事务,那么事务A在下一次读取a行数据时,发现和第一次不同了!
幻读:是指一个事务按照相同的查询条件检索数据,可是屡次检索出的数据结果却不同。例如事务A第一次以条件x=0检索数据获取了5条记录;此时事务B向表中插入了一条x=0的数据并提交了事务;那么事务A第二次再以条件x=0检索数据时,发现获取了6条记录!
那么在READ COMMITED隔离级别下为何会产生不可重复读和幻读的问题呢?
实际上不可重复读事务隔离级别也采用了咱们前面讲过的MVCC(多版本并发控制)机制。但在READ COMMITED隔离级别下的MVCC机制,会在每次select的时候都生成一个新的系统版本号,因此事务中每次select操做读到的不是一个副本而是不一样的副本数据,因此在每次select之间,若是有其它事务更新并提交了咱们读取的数据,那么就会产生不可重复读和幻读的现象。
不可重复读产生的缘由示意图以下:
REPEATABLE READ
事务隔离级别REPEATABLE READ,也叫可重复读,它是MySQL数据库的默认事务隔离级别。在这种事务隔离级别下,一个事务内的屡次读取结果是一致的,这种隔离级别能够避免脏读、不可重复读等查询问题。
这种事务隔离级别的实现手段主要是采用读写锁+MVCC机制。具体示意图以下:
如上图所示,在该事务隔离级别下的MVCC机制,并不会在事务内每次查询都产生一个新的系统版本号,因此一个事务内的屡次查询,数据副本都是一个,所以不会产生不可重复读问题。关于此隔离级别下MVCC更多的细节可参考前面内容!
可是须要注意,此隔离级别解决了不可重复读的问题,可是并无解决幻读的问题,因此若是事务A中存在条件查询,另一个事务B在此期间新增或删除了该条件的数据并提交了事务,那么依然会形成事务A产生幻读。因此在使用MySQL时须要注意这个问题!
SERIALIZABLE
该隔离级别理解起来最简单,由于它读写请求都会加排他锁,因此不会形成任何数据不一致的问题,就是性能不高,因此采用此隔离级别的数据库不多!
4)、一致性的实现
一致性主要是指经过回滚、恢复以及在并发条件下的隔离性来实现数据库数据的一致!前面所讲述的原子性、持久性及隔离性最终就是为了实现一致性!
前面的内容咱们分别讲述了MySQL并发控制和事务的内容,而实际上在并发控制和事务的具体细节都是依赖于MySql存储引擎来实现的。MySQL最重要、最不同凡响的特性就是它的存储引擎架构,这种将数据处理和存储分离的架构设计使得用户在使用时能够根据性能、特性以及其它具体需求来选择相应的存储引擎。
虽然如此,但绝大部分状况下使用MySQL数据库时选择的仍是InnoDB存储引擎,不过这并不妨碍咱们适当地了解下其它存储引擎的特色。接下来给你们简单总结下,具体以下:
以上咱们简单总结了MySQL各类存储引擎的大概特色及其大体适用的场景,但实际上除了InnoDB存储引擎外,在互联网业务中不多会看到其它存储引擎的身影。虽然MySQL内置了多种针对特定场景的存储引擎,可是它们大多都有相应的替代技术,例如日志类应用如今有Elasticsearch、而数仓类应用如今则有Hive、HBase等产品,至于内存数据库有MangoDB、Redis等NoSQL数据产品,因此可以给MySQL发挥的也只有InnoDB了!