900字范文,内容丰富有趣,生活中的好帮手!
900字范文 > MYSQL专题-MySQL事务实现原理

MYSQL专题-MySQL事务实现原理

时间:2018-09-11 17:47:45

相关推荐

MYSQL专题-MySQL事务实现原理

用过事务的伙伴大概都知道它的相关特性主要有四个:原子性(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,然后基于这三个我们再讲实现事务的原理。希望大家可以认真耐心地看完。文章主要分为四个部分:

事务的介绍redo log与undo logmysql锁技术以及MVCC事务的实现原理

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=400update finance set amount = amount + 600 where name="kmli";// 生成 重做日志 amount=600commit;

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

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

2.2 undo log

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

start transaction;// 生成 回滚日志 balance=1000update bank set balance = balance - 600 where name="kmli"; // 生成 回滚日志 amount=0update 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

更多文章请点击:更多…

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。