用过事务的伙伴大概都知道它的相关特性主要有四个:原子性(Atomicity),一致性(Consistency),隔离型(Isolation)以及持久性(Durability)。今天想跟大家一起研究下事务内部到底是怎么实现的。首先大家想一想,为什么需要事务?其实使用事务一方面为了保证数据的可靠性,另一方面是对于并发处理提供了很好的解决方案:

  • 可靠性:数据库要保证当insert或update操作时抛异常或者数据库crash的时候需要保障数据的操作前后的一致,想要做到这个,我需要知道我修改之前和修改之后的状态,所以就有了undo log和redo log。
  • 并发处理:当多个并发请求过来,并且其中有一个请求是对数据修改操作的时候会有影响,为了避免读到脏数据,所以需要对事务之间的读写进行隔离,至于隔离到啥程度得看业务系统的场景了,实现这个就得用MySQL 的隔离级别。

上面提到了两个文件:undo log和redo log,大家对这个不太了解的可以看我的另一篇文章MYSQL专题-MySQL三大日志binlog、redo log和undo log。其实除了这两个,MYSQL还提供了一个MVVC(多版本并发控制),这里会给大家再介绍一下undo log,redo log和MVVC,然后基于这三个我们再讲实现事务的原理。希望大家可以认真耐心地看完。文章主要分为四个部分:

  1. 事务的介绍
  2. redo log与undo log
  3. mysql锁技术以及MVCC
  4. 事务的实现原理

1 事务的介绍

1.1 为什么需要事务

以转账案例为例进行讲解。假设现在要从A账户向B账户中转入500 块,当进行转账时,需要先从银行账户A中取出钱,然后再存入银行账户B中,SQL如下:

// 第一步:A账户余额减少减少1000
update balance set money = money -500 where name= ‘A’;
// 第二步:B账户余额增加1000
update balance set money = money +500 where name= ‘B’;

如果在完成了第1步的时候突然宕机了,A的钱减少了而B的钱没有增加,那A岂不是白白丢了500 块。这时候就需要用到我们的事务了,开启事务后SQL如下:

// 第一步:开始事务
start transaction;
// 第二步:A账户余额减少减少1000
update balance set money = money -500 where name= ‘A’;
// 第三步:B账户余额增加1000
update balance set money = money +500 where name= ‘B’;
// 第四步:提交事务
commit;

1.2 什么是事务

有了以上的案例,接下里回答什么是事务。事务(Transaction)是访问和更新数据库的程序执行单元;事务中可能包含一个或多个sql语句,这些语句要么都执行成功,要么全部执行失败。

1.3 事务的四大特性(ACID)

开篇我们就提到事务具有四大特性,接下里给大家简单描述以下:

  • 原子性(Atomicity,或称不可分割性)

    • 一个事务必须被视为一个不可分割的最小工作单元,整个事务中所有的操作要么全部提交成功,要么全部失败回滚,对于一个事务来说,不可能只执行其中的一部分操作。
  • 一致性(Consistency)
    • 数据库总是从一个一致性的状态转换到另外一个一致性的状态,在事务开始之前和之后,数据库的完整性约束没有被破坏。
  • 隔离性(Isolation)
    • 事务内部的操作与其他事务是隔离的,并发执行的各个事务之间不能互相干扰。严格的隔离性,对应了事务隔离级别中的Serializable (可串行化),但实际应用中出于性能方面的考虑很少会使用可串行化。
  • 持久性(Durability)
    • 事务一旦提交,它对数据库的改变就应该是永久性的,接下来的其他操作或故障不应该对其有任何影响。

1.4 事务的隔离级别

在上面我们提到了隔离性,但实际上隔离性比想象的要复杂的多。在SQL标准中定义了四种隔离级别,每一种隔离级别都规定了一个事务所做的修改,哪些在事务内和事务间是可见的,哪些是不可见的,较低级别的隔离通常可以执行更高的并发,系统的开销也更低。MySQL的默认隔离级别是可重复读(REPEATABLE READ)。

  • 读未提交(READ UNCOMMITTED)

    • 在这个隔离级别下,事务的修改即使没有提交,对其他事务也是可见的。事务可以读取未提交的数据,这也被称之为脏读。这个级别会带来很多问题,从性能上来说,READ UNCOMMITTED不会比其他的级别好太多,但是却会带来很多问题,除非真的有非常必要的理由,在实际应用中一般很少使用。
  • 读已提交(REDA COMMITED)

    • 大多数数据系统的默认隔离级别都是REDA COMMITED(MySql不是),REDA COMMITED满足前面提到的隔离性的简单定义:一个事务开始时,只能看到已经提交的事务所做的修改。换句话说,一个事物从开始直到提交前,所做的修改对其他事务不可见。这个级别有时候也叫做不可重复读,因为执行两次相同的查询可能会得到不同的结果。
  • 可重复读(REPEATABLE READ)

    • REPEATABLE READ解决了脏读以及不可重复度的问题。该级别保证了同一个事务多次读取同样记录的结果是一致的。但是理论上,可重复度还是无法解决另外一个幻读的问题。所谓幻读,指的是当某个事务在读取某个范围内的记录时,另外一个事务又在该范围内插入了新的记录,当之前的事务再次读取该范围的记录时,就会产生幻读。不可重复读跟幻读的区别在于,前者是数据发生了变化,后者是数据的行数发生了变化。
  • 串行化(SERIALIZABLE)

    • SERIALIZABLE是最高的隔离级别,它通过强制事务串行执行,避免前面说的幻读。简单来说SERIALIZABLE会在读取的每一行数据上都加锁,所以可能会导致大量的超时和锁争用的问题。实际应用中也很少使用这个隔离级别,只有在非常需要确保数据一致性而且可以接受没有并发的情况下,才考虑此级别。

1.5 保存点

我们可以在事务执行的过程中定义保存点,在回滚时直接指定回滚到指定的保存点而不是事务开始之初,有点像我们玩游戏的时候可以存档而不是每次都要重新再来。语法如下:

SAVEPOINT 保存点名称;

当我们想回滚到某个保存点时,可以使用下边这个语句(下边语句中的单词WORK和SAVEPOINT是可有可无的):

ROLLBACK [WORK] TO [SAVEPOINT] 保存点名称;

2 redo log与undo log

2.1 redo log

redo log叫做重做日志,用来实现事务的持久性,当宕机或断电时用来恢复数据。该日志文件由两部分组成:重做日志缓冲(redo log buffer)以及重做日志文件(redo log),前者是在内存中,后者在磁盘中。当事务提交之后会把所有修改信息都会存到该日志中。我们假设有这样一个场景:用户需要将银行账户里的钱存500百块钱存到自己的理财账户中去。这里由两张表,一张银行卡账户信息表,包含账户id,账户名和账户余额,另一张表是理财账户表,包含理财账户id,理财账户名称和理财账户余额。为了方便我们假设用户名称不允许重复。则假设银行卡账户信息表里的余额为1000,理财账户表的余额为0,则需要将银行账户信息中的余额减去600 ,然后将理财表里的余额加上600,对应的sql语句如下:

start transaction;
update bank set balance = balance - 600 where name="kmli";
// 生成 重做日志 balance=400
update finance set amount = amount + 600 where name="kmli";
// 生成 重做日志 amount=600
commit;

在以上依据中标注了redo log生成的时机。为了更好的帮助大家理解,这里画一个流程图:

我们知道,mysql 为了提升性能并不会把每次的修改都实时同步到磁盘,而是会先存到Boffer Pool(缓冲池)里头,把这个当作缓存来用,然后使用后台线程去做缓冲池和磁盘之间的同步。如果还没来的同步的时候宕机或断电了怎么办?对应流程图中红色的操作。这样会导致丢部分已提交事务的修改信息!所以引入了redo log来记录已成功提交事务的修改信息,并且会把redo log持久化到磁盘,系统重启之后在读取redo log恢复最新数据。

2.2 undo log

undo log 叫做回滚日志,用于记录数据被修改前的信息,保障未提交事务的原子性。每次写入数据或者修改数据之前都会把修改前的信息记录到 undo log。正好跟前面所说的重做日志所记录的相反,重做日志记录数据被修改后的信息。undo log主要记录的是数据的逻辑变化,为了在发生错误时回滚之前的操作,需要将之前的操作都记录下来,然后在发生错误时才可以回滚。还是以上面的例子为例进行说明:

start transaction;
// 生成 回滚日志 balance=1000
update bank set balance = balance - 600 where name="kmli";
// 生成 回滚日志 amount=0
update finance set amount = amount + 600 where name="kmli";
commit;

在以上依据中标注了undo log生成的时机。为了更好的帮助大家理解,这里画一个流程图:

undo log 记录事务修改之前版本的数据信息,因此假如由于系统错误或者rollback操作而回滚的话可以根据undo log的信息来进行回滚到没被修改前的状态。

3 mysql锁机制(InnoDB)以及MVCC

3.1 mysql锁锁机制

当有多个请求来读取表中的数据时可以不采取任何操作,但是多个请求里有读请求,又有修改请求时必须有一种措施来进行并发控制,不然很有可能会造成不一致。Mysql中通过各种锁的组合来对读写请求进行控制,这里只简单介绍几类,后面我们也会出文章对于其中的锁进行具体的分析。

  • 读锁跟写锁

    • 读锁:可以共享,可以叫共享锁(shared lock),多个读请求可以共享一把锁读数据,不会造成阻塞,只能读不能修改。。
    • 写锁:可以叫排他锁(exclusive lock),写锁会排斥其他所有获取锁的请求,一直阻塞,直到写入完成释放锁。

通过读写锁,可以做到读读可以并行,但是不能做到写读,写写并行 事务的隔离性就是根据读写锁来实现的。

  • 行锁跟表锁

    • 行锁:只锁定需要操作的数据,并发性能好。
    • 表锁:在操作数据时会锁定整张表,并发性能较差。

由于加锁本身需要消耗资源(获得锁、检查锁、释放锁等都需要消耗资源),因此在锁定数据较多情况下使用表锁可以节省大量资源。MySQL中不同的存储引擎支持的锁是不一样的,例如MyIsam只支持表锁,而InnoDB同时支持表锁和行锁,且出于性能考虑,绝大多数情况下使用的都是行锁。

3.2 MVCC

MVCC (MultiVersion Concurrency Control) 叫做多版本并发控制。InnoDB的 MVCC ,是通过在每行记录的后面保存两个隐藏的列来实现的。这两个列, 一个保存了行的创建时间,一个保存了行的过期时间,当然存储的并不是实际的时间值,而是系统版本号。通过数据多版本来做到读写分离,从而实现不加锁读进而做到读写并行。(我们在后期会专门写一篇关于MVCC的文章)

4 事务的实现原理

前面讲的重做日志,回滚日志以及锁技术就是实现事务的基础:

  • 事务的原子性是通过 undo log 来实现的
  • 事务的持久性性是通过 redo log 来实现的
  • 事务的隔离性是通过 (读写锁+MVCC)来实现的
  • 事务的一致性是通过原子性,持久性,隔离性来实现的

MySQL中不是所有的存储引擎都支持事务,MyISAM就不支持事务,实际上支持事务的只有InnoDB跟NDB Cluster,本文关于事务的分析都是基于InnoDB。在分析事务的实现原理之前我们介绍一下InnoDB的相关知识。

  • InnoDB是一个将表中的数据存储到磁盘上的存储引擎,所以即使关机后重启我们的数据还是存在的。而真正处理数据的过程是发生在内存中的,所以需要把磁盘中的数据加载到内存中,如果是处理写入或修改请求的话,还需要把内存中的内容刷新到磁盘上。而我们知道读写磁盘的速度非常慢,和内存读写差了几个数量级,所以当我们想从表中获取某些记录时,InnoDB存储引擎需要一条一条的把记录从磁盘上读出来么?不,那样会慢死,InnoDB采取的方式是:将数据划分为若干个页,以页作为磁盘和内存之间交互的基本单位,InnoDB中页的大小一般为 16 KB。也就是在一般情况下,一次最少从磁盘中读取16KB的内容到内存中,一次最少把内存中的16KB内容刷新到磁盘中。
  • InnoDB作为MySQL的存储引擎,数据是存放在磁盘中的,但如果每次读写数据都需要磁盘IO,效率会很低。为此,InnoDB提供了缓存(Buffer Pool),Buffer Pool中包含了磁盘中部分数据页的映射,作为访问数据库的缓冲:当从数据库读取数据时,会首先从Buffer Pool中读取,如果Buffer Pool中没有,则从磁盘读取后放入Buffer Pool;当向数据库写入数据时,会首先写入Buffer Pool,Buffer Pool中修改的数据会定期刷新到磁盘中(这一过程称为刷脏)。
  • InnoDB存储引擎文件主要可以分为两类,表空间文件及重做日志文件(redo log file),表空间文件又可以细分为两类,共享表空间跟独立表空间。「undo log位于共享表空间中的undo段中」,每个表空间都被划分成了若干个页面,「凡是页面的读写都在buffer pool中进行,这意味着undo log也需要先写入到buffer pool,所以undo log的生成也需要持久化,也就是说undo log的生成需要记录对应的redo log」。(注意:不是所有的undo log的生成都会产生对应的redo log,对于操作临时表生成的undo log并不会生成对应的undo log,因为修改临时表而产生的undo日志只需要在系统运行过程中有效,如果系统奔溃了,那么在重启时也不需要恢复这些undo日志所在的页面,所以在写针对临时表的Undo页面时,并不需要记录相应的redo日志。)

我们要探究MySQL中事务的实现原理,实际上就是要弄明白它的ACID特性是如何实现的。正如上面的描述,ACID中的一致性是事务的最终目标,前面提到的原子性、持久性和隔离性,都是为了保证数据库状态的一致性。所以我们要分析的就是MySQL的原子性、持久性和隔离性的实现原理。接下来我们对事务的每一特性的原理进行说明。

4.1 原子性实现原理

前面提到了,所谓原子性就是指整个事务是一个不可分隔的整体,组成事务的一组SQL要么全部成功,要么全部失败,要达到这个目的就意味着当某一个SQL执行失败时,我们要能够撤销掉其它SQL的执行结果,在MySQL中这是依赖undo log(回滚日志)来实现。
undo log属于逻辑日志(redo log属于物理日志,记录的是数据页的情况),我们可以这么认为,当delete一条记录时,undo log中会记录一条对应的insert记录,反之亦然,当update一条记录时,它记录一条对应相反的update记录,执行发生异常时,会根据undo log中的记录进行回滚。undo log主要分为两种:

  • insert undo log:在insert 操作中产生的undo log。insert操作的记录,只对事务本身可见,对其他事务不可见。故该undo log可以在事务提交后直接删除,不需要进行purge操作。
  • update undo log:delete 和update操作产生的undo log。该undo log可能需要提供MVCC机制,因此不能在事务提交时就进行删除。提交时放入undo log链表,等待purge线程进行最后的删除。

对undo log有一定了解后,我们再分析文章开头的例子,我们以以下流程图说明undo log保证原子性的原理:

4.2 持久性实现原理

InnoDB引入了Buffer Pool来优化读写的性能,但是虽然Buffer Pool优化了性能,但同时也带来了新的问题:如果MySQL宕机,而此时Buffer Pool中修改的数据还没有刷新到磁盘,就会导致数据的丢失,事务的持久性无法保证。基于此,redo log就诞生了。
redo log是物理日志,记录的是数据库中数据库中物理页的情况,事务开始之后就产生redo log,redo log的落盘并不是随着事务的提交才写入的,而是在事务的执行过程中,便开始写入redo log文件中。
redo log包括两部分:

  • 内存中的日志缓冲(redo log buffer),该部分日志是易失性的;
  • 磁盘上的重做日志文件(redo log file),该部分日志是持久的。

在概念上,innodb通过force log at commit机制实现事务的持久性,即在事务提交的时候,必须先将该事务的所有事务日志写入到磁盘上的redo log file和undo log file中进行持久化。既然redo log也需要在事务提交时将日志写入磁盘,为什么它比直接将Buffer Pool中修改的数据写入磁盘(即刷脏)要快呢?主要有以下两方面的原因:

  • 刷脏是随机IO,因为每次修改的数据位置随机,但写redo log是追加操作,属于顺序IO。
  • 刷脏是以数据页(Page)为单位的,MySQL默认页大小是16KB,一个Page上一个小修改都要整页写入;而redo log中只包含真正需要写入的部分,无效IO大大减少。
    我们还是以文章开头的例子画图说明:

4.3 隔离性实现原理

隔离性是事务ACID特性里最复杂的一个。在SQL标准里定义了四种隔离级别,级别越低的隔离级别可以执行越高的并发,但同时实现复杂度以及开销也越大。只要彻底理解了隔离级别以及他的实现原理就相当于理解了ACID里的隔离型。前面说过原子性,隔离性,持久性的目的都是为了要做到一致性,但隔离型跟其他两个有所区别,原子性和持久性是为了要实现数据的可性保障靠,比如要做到宕机后的恢复,以及错误后的回滚。那么隔离性是要做到什么呢?隔离性是要管理多个并发读写请求的访问顺序,这种顺序包括串行或者是并行,需要说明的是写请求不仅仅是指insert操作,又包括update操作。相关特性在上面已经介绍过,大家如果不记得可以往上回看。

4.4 一致性实现原理

数据库总是从一个一致性的状态转移到另一个一致性的状态。主要保证了前面几个特性,那么一致性就可以得到保证。执行更新操作时发生异常,我们可以依据原子性进行回滚,又或者事务提交之后,缓冲池还没同步到磁盘的时候宕机了,这个时候可以依据持久性恢复数据,有并发事务请求的时候依据隔离性做好事务之间的可见性问题,避免造成脏读,不可重复读,幻读等。

猜你感兴趣
MYSQL专题-绝对实用的MYSQL优化总结
MYSQL专题-使用Binlog日志恢复MySQL数据
MYSQL专题-MVCC多版本并发控制
MYSQL专题-MySQL三大日志binlog、redo log和undo log

更多文章请点击:更多…

MYSQL专题-MySQL事务实现原理相关推荐

  1. MYSQL专题-MySQL三大日志binlog、redo log和undo log

    日志是mysql数据库的重要组成部分,记录着数据库运行期间各种状态信息.mysql日志主要包括重做日志(redo log).回滚日志(undo log).二进制日志(bin log).错误日志(err ...

  2. mysql+after+commit_Spring事务aftercommit原理及实践

    来道题 CREATE TABLE `goods` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `good_id` varchar(20) DEFAULT NU ...

  3. 事务的基本概念及Mysql事务实现原理

    Spring事务专题(三)事务的基本概念,Mysql事务处理原理 前言 本专题大纲: 我重新整理了大纲,思考了很久,决定单独将MySQL的事务实现原理跟Spring中的事务示例分为两篇文章,因为二者毕 ...

  4. MYSQL专题-由简到繁理解索引结构

    大家可能都听过数据库索引,当然作为开发者来说其实大部分时间也用过索引.但是可能有的人知道索引是干什么的,但是对于索引的结构却不是很了解.所以这篇博客我会谈谈对索引结构的一些知识以及分享如何从零开始一层 ...

  5. MYSQL专题-使用Binlog日志恢复MySQL数据

    大家有没有碰到过由于误操作把测试数据库的一张表给删除了,导致测试的数据都被删除了,然后手足无措,测试把你一定数落,顿时感觉自己要死了?今天就教你即使误删了也可以将删除的数据恢复,以后误删再也不用惊吓了 ...

  6. MYSQL专题-MVCC多版本并发控制

    MVCC,全称Multi-Version Concurrency Control,即多版本并发控制.MVCC是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问,在编程语言中实现事务内 ...

  7. 【Mysql】Mysql事务隔离界别及其实现原理

    文章目录 1.持久性 2.原子性 3.一致性 4.隔离性 (1)写写操作--锁 (2)写读(脏读,不可重复读,幻读)操作--MVCC 1.持久性 1.每次操作除了更新到buffer中,还会记录在red ...

  8. 玩转MySQL:你懂什么是事务机制原理吗

    引言 众所周知,MySQL数据库的核心功能就是存储数据,通常是整个业务系统中最重要的一层,可谓是整个系统的"大本营",因此只要MySQL存在些许隐患问题,对于整个系统而言都是致命的 ...

  9. MySQL事务篇:ACID原则、事务隔离级别及事务机制原理剖析

    引言 众所周知,MySQL数据库的核心功能就是存储数据,通常是整个业务系统中最重要的一层,可谓是整个系统的"大本营",因此只要MySQL存在些许隐患问题,对于整个系统而言都是致命的 ...

最新文章

  1. EJB与JAVA BEAN_J2EE的异步消息机制
  2. 编程:利用杨辉三角形原理来计算组合数
  3. python调用接口实例化_python 类静态方法实例化另一个类对象的问题?
  4. python语言 行业_如何入门编程开发行业 选择Python语言怎么样
  5. HDU2515 Yanghee 的算术
  6. python open读取_python,一读取文件open()
  7. Linux bashrc和profile的用途和区别
  8. 大数据自学好还是培训好?
  9. system进程总是100%
  10. 终于等到你:国内***团队360Vulcan公布iOS 12.1越狱漏洞细节
  11. Windows server 2003 伪静态配置方法
  12. Python打印五子棋棋盘
  13. 读书笔记 | 自动驾驶中的雷达信号处理(第1章 雷达系统基础)
  14. 搜索及代码在GitHub上查重小技巧
  15. js中的JSON对象转换,过滤特殊字符数据
  16. obj转stl_STL转STP的方法视频教程,OBJ格式转STP或者IGS开模具格式的过程,STL转STP软件介绍...
  17. [NOI2017]蔬菜
  18. 联邦学习攻击与防御综述
  19. 【python】HTTP压力测试过程中遇到的问题与解决方案
  20. 动画交互设计软件:Principle for Mac

热门文章

  1. TEEC_AllocateSharedMemory()和 TEEC_RegisterSharedMemory()的总结
  2. 宝塔 mysql迁移_(2020年最新方法)如何快速迁移网站?使用宝塔一键迁移转移网站数据详细教程...
  3. 【Web安全】从xxe到phar反序列化
  4. P5367 【模板】康托展开
  5. 当年的聊天室,今天的我(java实现聊天室群聊功能)
  6. 【uni-app】深度作用选择器解决修改checkbox样式无效问题
  7. Spring boot默认日志配置
  8. 手机数控模拟器安卓版_车床模拟器2手机版下载-车床模拟器2游戏 v2.5.0安卓版_5577安卓网...
  9. sdk版本过低怎么办_滴滴ElasticSearch平台跨版本升级以及平台重构之路
  10. 【IDEA】怎么把idea的目录结构,以文本形式输出?