关于mysql:五分钟详解MySQL并发控制及事务原理

2次阅读

共计 7230 个字符,预计需要花费 19 分钟才能阅读完成。

在现在互联网业务中应用范畴最广的数据库无疑还是关系型数据库 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 锁类型

在 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 用于记录某行数据的多个版本的数据。
  • read view : 用来判断以后版本数据的可见性

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 事务

后面在解说了对于 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 的生成,并且回滚日志必须先于数据长久化到磁盘上;
  • 所谓的回滚就是依据 undo log 日志做逆向操作,比方 delete 的逆向操作为 insert,insert 的逆向操作为 delete,update 的逆向操作为 update 等;

2)、持久性的实现

持久性,指的是事务一旦提交其所作的批改会永恒地保留到数据库中,此时即便零碎解体批改的数据也不会失落。

事务的持久性次要是通过 redo log 日志来实现的。redo log 日志之所以可能补救缓存同步所造成的数据差别,次要其具备以下特点:

  • redo log 的存储是程序的,而缓存同步则是随机操作;
  • 缓存同步是以数据页为单位,每次传输的数据大小大于 redo log;

对于 redo log 实现事务持久性的逻辑可参考本文后面对于 redo log 局部的内容!

3)、隔离性的实现

隔离性是事务 ACID 个性中最简单的一个。在 SQL 规范里定义了四种隔离级别,每一种隔离级别都规定一个事务中的批改,那些是事务之间可见的,那些是不可见的。

MySQL 隔离级别有以下四种(级别由低到高):

  • READ UNCOMMITED (未提交读);
  • READ COMMITED (提交读)
  • REPEATABLE READ (可反复读)
  • SERIALIZABLE (可串行化)

隔离级别越低,则数据库能够执行的并发度越高,然而实现的复杂度和开销也越大。只有彻底了解了隔离级别以及它的实现原理,就相当于了解了 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 最重要、最不同凡响的个性就是它的存储引擎架构,这种将数据处理和存储拆散的架构设计使得用户在应用时能够依据性能、个性以及其它具体需要来抉择相应的存储引擎。

尽管如此,但绝大部分状况下应用 MySQL 数据库时抉择的还是 InnoDB 存储引擎,不过这并不障碍咱们适当地理解下其它存储引擎的特点。接下来给大家简略总结下,具体如下:

以上咱们简略总结了 MySQL 各种存储引擎的大略特点及其大抵实用的场景,但实际上除了 InnoDB 存储引擎外,在互联网业务中很少会看到其它存储引擎的身影。尽管 MySQL 内置了多种针对特定场景的存储引擎,然而它们大多都有相应的代替技术,例如日志类利用当初有 Elasticsearch、而数仓类利用当初则有 Hive、HBase 等产品,至于内存数据库有 MangoDB、Redis 等 NoSQL 数据产品,所以可能给 MySQL 施展的也只有 InnoDB 了!

正文完
 0