注册 登录
  • 欢迎访问"运维那点事",推荐使用Google浏览器访问,可以扫码关注本站的"微信公众号"。
  • 如果您觉得本站对你有帮助,那么可以扫码捐助以帮助本站更好地发展。

MySQL InnoDB单机事务原理(一)

MySQL 彭东稳 6602次浏览 已收录 0个评论

一、事务简介

事务(transaction)就是数据库区别于文件系统的一个重要特性,事务就是一组原子性的SQL查询,或者说是一个独立的工作单元。简单说就是由一堆SQL语句组成的一个执行单元,在这组执行单元中如果有任何一条语句执行不了,那么所有的语句都不会执行。也就是说,事务内的语句,要呢全部执行成功,要么全部执行失败。在MySQLInnodb存储引擎支持事务操作,这里也主要以Innodb为主,其他支持事务的存储引擎有兴趣自行看。

二、单事务单元

银行应用是解释事务必要性的一个经典例子,现在假设有这么一个单事务单元,要从用户Bob的账户转移100元到Smith的账号,那么需要至少以下几个步骤(按照事务时间序)。

1步:锁定Bob的账户

2步:锁定Smith的账户

3步:检查Bob的账户是否100

4步:如果Bob的账户有100元,那么就减去100

5步:然后Smith的账户加上100

6步:释放Bob账户的锁

7步:释放Smith账户的锁

PS:加锁和解锁都是在执行具体语句时会自动做,称为“隐式锁”,上面的操作中把锁抽象出来说了;当然也有“显式锁”,后面具体介绍锁再说。

上述七个步骤的操作必须打包在一个事务中,任何一个步骤失败,则必须回滚所有的步骤。这也就是事务的原子性。单纯的事务概念并不是故事的全部,下面先简单说一下这一个事务在执行过程中会遇到什么常见问题及解决方案?

1)试想一下,如果执行到第5步时服务器崩溃了,会发生了什么?Bob用户可能会损失100元。那么现实中这种问题肯定是不允许发生的,所以数据库的解决方案就是利用事务日志,当服务器重新启动后数据库启动时会从事务日志中检查到这个事务没有完成,然后数据库进程会把这个事务进行回滚操作,也就是把此事务中执行过的操作反向执行一遍。当所有数据回滚操作都完成之后就会正常提供服务。是不允许用户看到一个中间状态就是100凭空消失了。

2)如果在执行第4步和第5步之间时,另外一个进程要删除Bob的所有余额(比喻刷卡刷了),那么结果可能就是银行在不知道这个逻辑的情况下白白给Smith用户100元。但这里想说的是,由于使用了锁机制(MySQL有表锁和行锁)所以不会出现这种情况。如下图:

MySQL InnoDB单机事务原理(一)

由于第一个事务操作时使用了锁,所以第二个事务以及第三个事务在第一个事务操作时都无法操作Bob账户和Smith账户。直至事务1整个事务完成。另外还有一个特殊的情况就是当事务1执行第4步后此时这两个账户都没有这100块钱,而这个中间状态同样是不允许被其他进程看见的,这也就是保证数据的一致性。

3)如果当执行到第3步时发现Bob账户上并没有100元,这属于业务属性不匹配,此时这个事务是不是就不应该再执行了,应该马上利用事务日志进行事务回滚操作。

不一样的事务?

除了上面给出的这个事务单元的例子外,还有一些操作也是事务单元。InnoDB存储引擎是一个事务型存储引擎,所以在InnoDB存储引擎上所有的针对数据的操作都是一个事务单元。比如,对这个表建立索引或删除索引是一个事务单元、从这个表读取数据也是一个事务单元、向表中写入一行记录并同时更新这行记录的所有索引也是一个事务单元、删除整张表也是一个事务单元,不过InnoDB默认是自动提交事务(这个后面会说)。而这些操作都会记录到事务日志中已保证数据的安全性,比如你删除一张大表,正在执行删除时机器宕机了,那么当你重新启动数据库时会根据事务日志检测到事务没有完成,所以会回滚已经删除了的数据,回滚完成后数据库服务才能正常提供服务。从MySQL 8.0开始,对于DDL操作才已经支持原子性了,在之前版本是不支持的。

结合上述问题,一个真正支持事务的关系型数据库系统必须要经过严格的ACID测试,ACID标准能够保证事务的完整性要求,否则空谈事务的概念是不够的。 

三、ACID

一个支持事务的关系型数据库必须要必备ACID,也就是数据的原子性(Automicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)。这四点对于一个支持事务的数据库非常重要,但是实际上要真正做到ACID也是非常难的。在InnoDB存储引擎中的事务是完全符合ACID的特性,下面介绍一下ACID

原子性(Automicity

一个事务必须被视为一个不可分割的最小工作单元,整个事务所引起的数据修改要么全部提交,要么全部失败回滚。事务中任何一个SQL执行失败,已经执行成功的SQL语句也必须回滚,数据库状态应该退回到执行事务前的状态。换句话来说就是一个事务的执行,要么为最终状态,要么返回到原始状态,不能只执行其中的一部分操作,这就是事务的原子性。在InnoDB存储引擎中原子性是由redo log来保证的。

一致性(Consistency

一致性指事务将数据库从一个一致性的状态转到另外一个一致性的状态,在事务开始和结束以后,数据库的完整性约束没有被破坏,数据都必须保持一致状态。例如,在表中有一个字段为姓名,为唯一约束,即在表中姓名不能重复。如果一个事务对姓名字段进行了修改,但是在事务提交或事务操作发生回滚后,表中的姓名变得非唯一了,这就破坏了事务一致性要求,即事务将数据库从一种状态变为了一种不一致的状态。因此,事务是一致性的单位,如果事务中某个动作失败了,系统可以自动撤销事务—返回初始化的状态。在InnoDB存储引擎中事务一致性是由undo log来保证的。

隔离性(Isolation

隔离性就是以性能为理由,对一致性的一种破坏。为什么这么说呢?上面我们说了必须要保证事务的一致性,那么保证一致性最好的实现方式就是让事务一个一个按照顺序排队执行。而事务间的隔离性就违背事务的一致性,隔离级别中的RURC级别都破坏了数据的一致性。但由于事务排队执行的效率太低才有了隔离性,好处就是通过不同的机制来提高事务的执行效率。后面介绍隔离级别时在详解。隔离性还有一些其他称呼,如并发控制、可串行化和锁。通常来说,一个事务所做的修改在最终提交之前对其他事务是不可见的,即一个事务的执行不会影响另外一个事务的执行,这通常使用锁来实现。后面讨论隔离级别时,会发现为什么要说“通常来说”是不会可见的。在InnoDB存储引擎中隔离性就是靠锁来保证的。

持久性(Durability

事务一旦提交,其结果就是永久性的保持在数据库中,即使在事务信息还没有刷新到磁盘的时候发生服务器宕机行为,数据库也能将数据恢复。需要注意的是,持久性只能从事务本身的角度来保证结果的永久性,如事务提交后,所有的变化都是永久的。但如果不是数据库本身的故障而是一些外部的原因,比如磁盘坏掉了,那么所有数据库信息都会丢失。因此持久性保证的是事务系统的高可靠性,而不是高可用性,对于高可用性的实现事务本身并不能保证。在InnoDB存储引擎中持久性是由redo log & undo log来保证的。

四、多事务单元

根据上面介绍过的单事务单元后,下面说一说多事务单元时间的关系,如下图:

MySQL InnoDB单机事务原理(一)

事务单元①会锁定BobSmith这两个账号,同一时刻事务单元②会请求锁定Smith(但由于被事务单元①锁定,所以只能等待)和job这两个账号,同一时刻事务单元③会请求锁定joeBob这两个账号(但由于两个账号都被锁定,所以只能等待)。

从以上说明可以看出,当多个事务针对同一个数据时会排队执行,也就是说只有当前面一个事务完成之后,后面的一个事务才能够去执行操作,它只能看见前一个事务执行完成后的记过而看不到中间状态。有人分析过事务单元跟事务单元之间可能发生的关系(事务处理这本书中讲的),如是有了以下答案,也称之为事务单元之间的happen-before关系,主要由以下四种关系(这四种关系指的是多个事务之间对同一个数据的操作,一定要清楚这个不然会搞混)。

MySQL InnoDB单机事务原理(一)

那么问题来了,既然事务跟事务之间只有这四种关系,接下来就是考虑如何能够以最快的速度完成?但前提是必须要保证事务单元之间的一致性关系之后(保证上面四种操作的逻辑顺序),仍然可以让系统能够以最快的速度完成。把这两个因素进行细致化总结之后,我们会发现它有这么几种不一样的做法。

第一选择:排队法

MySQL InnoDB单机事务原理(一)

第二选择:排他锁(理解为写锁)

MySQL InnoDB单机事务原理(一)

使用排对法来处理事务虽然是一种办法,且保证数据的一致性,但是效率实在太低,如果有大的事务执行时后面的所有事务都必须得等待,直到超时。后来数据库大师们又想到了使用锁(行锁)来处理事务,也就是当两个事务操作的数据不是同一个的时候就可以同时来执行;但是当两个事务操作的数据有相同的时候就无法进行并行执行了。

但是人们对于速度的追求是无禁止的,有没有更好的方法可以并行的速度更快呢?于是又有一些聪明人想到一些聪明的办法。

第三选择:读写锁

MySQL InnoDB单机事务原理(一)

如上图,人们发现事务单元之间的happen-before关系,其中如果把读写锁分开的话,那样写读、写写、读写、读读中的读读就可以完全并行到一起,而对已其他三种仍然让它串行。这样的话你可以发现对于那种读多写少环境一定可以进一步提升事务的并行度。这一做法也是以前主流数据库的一些做法,用读写锁加大读的并行度。也就因为这样所以才会出现了隔离性的其中两个不同的隔离级别,一个是“可重复读”,一个是“读已提交”。

“可重复读”就是在同一个事务中发出同一个SELECT语句两次或更多次,那么产生的结果数据集总是相同的。因此,使用可重复读隔离级别的事务可以多次检索同一行集,并对它们执行任意操作,直到提交或回滚操作终止该事务,但可重复读只能解决读读的并行执行,并不能使读写、写写、写读的并行执行,下图解释的很详细。

MySQL InnoDB单机事务原理(一)

这个时候如何能让读写做到并行呢?又提出一种隔离级别就是“读已提交”。在读已提交隔离级别,他们用的方法就是“读锁升级”,也就是说此时可以做到“读读”并行时且如果后来又来了一个写锁,可以把读进行升级,也就是允许读的时候进行写操作。在这个情况下的时候你会发现这个系统就可以进一步的提高并行度(如下图)。

MySQL InnoDB单机事务原理(一)

但是这种模式所带来的代价就是在一个事务中,第一次读的结果是一个数据,而第二次读的结果又是另外一个数据了。这个问题也就是所谓的“不可重复读”问题。同时你也更能感受到了ACIDI就是对C的破坏了。具体就是当“读读”两个事务并行的时候,这时又有一个事务进行写操作,于是就会出现一个问题,可能原先那个事务版本号为0的记录这个时候由于更改已经变为1了。由于都是并行执行,所以当第一个读事务再次有读请求进来就会发现数据被更改了,两次读取的数据的版本是不同的且数据也不同。

第四选择:MVCC(多版本并发控制)

看完上面三种选择之后发现从一开始的事务一条一条执行,到针对不同数据操作时事务的并行执行,到读读并行执行,到读写并行执行。这些以加锁的方式来增加并行执行已经是很老的方法了。

接下来介绍目前数据库厂商主流的一种事务并行控制方式MVCC机制,看完之后只有无限的膜拜大神。MVCC可以做到写不阻塞读,主要针对写进行的优化(上面的都是针对读进行的优化),解决了维持一个大事务的问题,在没有MVCC之前如果有一个大的写操作事务在运行,那么其余的读只能等待,极大降低了并发性能;使用MVCC的代价就是系统复杂度变大(后面会介绍MVCC)。

MySQL InnoDB单机事务原理(一)

通过上面对多事务执行的一系列分析,大概说明了事务并发整个发展过程以及事务并发执行会产生哪些问题。首先多事务并行必面临读读、读写、写读和写写这四个问题,那么如何让这四种操作能够并行执行也就是ACID中的隔离性所要考虑的问题。InnoDB对于ACID中的隔离性使用InnoDB锁和MVCC机制来控制管理,而对于原子性、一致性和持久性则是靠redoundo这两个日志实现的。事务的ACID特性可以确保银行不会弄丢你的钱。而在应用逻辑中,要实现这一点非常难,甚至可以说是不可能完成的任务。一个兼容ACID的数据库系统,需要做很多复杂的工作,才能确保ACID的实现。

五、事务日志

上面多次提到事务日志(redo),以及用于事务回滚的undo回滚日志。事务日志用于保证事务的可靠性,在进行事务操作时先将操作写进事务日志,而事务日志会记录老数据和新数据从而实现回滚。并且不用每次都将修改的数据本身持久到磁盘,事务日志采用的是追加的方式记录。因此写日志操作时磁盘上一小块区域内的顺序I/O,而不像随机I/O需要在磁盘的多个地方移动磁头。所以采用事务日志的方式相对来说要快得多。事务日志持久以后,内存中被修改的数据在后台可以慢慢地刷回磁盘。目前大多数存储引擎都是这样设计的。我们通常称之为预写式日志,修改数据需要写两次磁盘。

如果数据的修改意见记录到事务日志并持久化,但数据本身还没有写回磁盘,此时系统崩溃,存储引擎在重启时能够自动恢复这部分修改的数据。具体的恢复方式视存储引擎而定。


如果您觉得本站对你有帮助,那么可以支付宝扫码捐助以帮助本站更好地发展,在此谢过。
喜欢 (3)or分享 (0)
关于作者:

您必须 登录 才能发表评论!