在工作当中相信大家一定遇到了各种各样的事务问题,本文拟从ACID开始谈起,最终到目前比较常用的分布式事务,让大家对事务有一个整体贯连性的理解
事务的基本特性
逻辑日志与物理日志
隔离性及Mysq的隔离性
分布式事务
一、事务的基本特性
提起事务就不能不提ACID,但是需要知道这个不是Mysql的规范而是所有数据的事务规范,这里不是要针对这四个特性的描述进行大篇幅的说明
因为那个东西在网上随便一找就很多,这里主要是对每个特性进行有助于理解的说明(偏向于Mysql)
原子性(Atomicity):原子性是要求一个事物的所有操作保持一个原子的状态,要不全部执行成功,要不全不执行
那Mysql中到底是怎么保证的呢?
Mysql在执行insert ,update,delete的时候会记录一个undo log,是一种逻辑日志。
怎么理解这个逻辑日志的,就是比如我们insert一条语句的时候,Mysql在undo log中记录了一个对应的delete语句
如果我们执行的是一个update语句,那就对应记录一个反向的update语句,所以也就是通过这个undo log,一旦一个事务需要回滚的时候
那么就可以通过undo log来将数据恢复到之前的状态,以此来保证原子性。
一致性(Consistency):一致性就是要求数据库在执行一个事务之前和之后必须要保证一个一致性的状态,必须是从一个状态到另一个状态,不应该出现任何其他因素影响状态的变化
在原子性中我们提到了undo log,用来保证数据回滚的时候保证数据库能够恢复到事务开始的状态,那么如果事务正常进行
但是在数据持久化到磁盘的时候出现故障了,导致一部分数据没有持久化到磁盘上,那这个时候数据库的状态就不处于一致性了,这种情况是怎么保证的呢?
其实这里Mysql在记录undo log 的时候还记录了一个redo log , 具体的undo log 与 redo log 会在下一小节进行详细说明
这里大家只需要知道redo log是一个物理日志,用来记录数据的变更。那么一个事务的提交,是要保证日志先提交,然后才提交事务的
所以如果在事务提交过程中出现电脑断电等异常情况,在数据库重启的时候,会先检查redo log,将还没有完全持久化到磁盘上的数据持久化
这样就能够保证数据执行一个事务,是从一个一致性的状态到另一个一致性状态的转变了。
隔离性(Isolation):
隔离性这里可能需要描述的内容就比较多了,按照惯例我们还是简单说明一下隔离性,一个事务在执行的过程中不能被其他事务干扰
每个事务都应该有自己完整的数据空间,既一个事务内部的操作及使用的数据,对其他并发事务都是隔离开来的
那如果不隔离开会产生什么问题呢?
1.脏读:所谓的脏读,其实就是读到了别的事务回滚前的脏数据。比如事务B执行过程中修改了数据X,在未提交前,事务A读取了X,而事务B却回滚了,这样事务A就形成了脏读。
2.不可重复读:事务A首先读取了一条数据,然后执行逻辑的时候,事务B将这条数据改变了,然后事务A再次读取的时候,发现数据不匹配了
就是所谓的不可重复读了。其实就是在一个事务过程中,多次读取到的数据不一致。
3.幻读:事务A首先根据条件索引得到N条数据,然后事务B改变了这N条数据之外的M条或者增添了M条符合事务A搜索条件的数据
导致事务A再次搜索发现有N+M条数据了,就产生了幻读
了解了上面是那种问题之后我们就能够理解为什么要有隔离性了,因为上面问题中的任何一个,都有可能导致事务处理的结果和预想的不一致
也就是我们上述说的一致性,就可能会被打乱等等带来的很多问题
所以针对上述问题,在标准的SQL规范中提出了四个事务级别(注意这个也不是只针对Mysql的,只是Mysql实现了这四个事务级别):
1.read uncommited:未授权读,是隔离级别最低的一个。其实现就是如果一个事务正在处理一个数据,并对其进行更改,但是还为完成整个事务的提交。
但此时另外一个事务能够读取到当前数据并进行处理。但是之后之前的事务执行了回滚操作,也就是刚才更改的数据失效了。而此时后面读取到数据的事务读取到的就只脏读数据。
2.read commited:授权读,与未授权读相似,唯一的不同就是,这里后面的事务不再能读取到之前事务为提交的数据了,这样就可以避免脏读的问题,但是可以读取到其他事务已提交的数据,
虽然避免了脏读,但是还是不能避免不可重复读的问题。
3.repeateable read:可重复读,这个级别是在上面两个级别上进行了更深度的升级。这个级别能够保证在一个事务处理过程中,读取到的数据都是一致的。因此呢能够避免脏读和不可重复读的
问题,但是理论上不可避免幻读的问题。
这里要提一点,就是这个级别是Mysql的默认隔离级别,理论上这个级别不能避免幻读的问题,但是Mysql在这个级别上就成功的避免了脏读,不可重复读和幻读三种问题,具体原因会在下面的小节中进行说明
串行化(Serializable):是最严格的一种隔离级别,它要求所有的事务都被串行,不能并发的处理。那么带来的影响就是性能极差,但是可以完全保证避免三种数据问题
二、逻辑日志与物理日志
上小节在说明特性的实现的时候提到了Mysql的两种日志
1.undo log
2.redo log
本节中对这两种日志进行深度的解读,希望读者看完本节内容之后,能够对事务的ACID的特性以及其实现原理有更加深刻的理解。
首先说啊,Mysql有一个force log at commit机制实现事务的持久性,即在事务提交的时候
必须先将该事务的所有事务日志写入到磁盘上的redo log file和undo log file中进行持久化,这也是为什么通过这两个日志能够保证ACID的特性。
那么首先说说redo log,在上文中也说了,这个redo log是一个物理日志,那么到底什么是物理日志呢?
这里我们不过多赘述buffer 以及fsync,就说说这个日志是怎么个物理法。
比如我们要在Mysql中执行一个update的操作,那么是要对指定记录进行更新操作的,这个时候数据库并不是直接去对应的数据文件中去找到对应的一条记录,然后把它给更改的。
而是先将存储对应数据记录的数据磁盘块,这里也就是硬盘上一个扇页的大小(这里为了方便,redo log每一块的大小也是一个扇页的大小)
先将对应的存储扇页中的内容读取到buffer中,然后将对应的修改更改到这个buffer 中,然后redo log就是记录了在这个数据页的什么地方,做了什么修改的操作,这就是物理日志。
那这里我们思考一下,为什么Mysql要这么记录日志呢?这里我自己思考了一下应该有以下几个方面的原因:
1.恢复方便:因为一旦在数据要持久化到磁盘的过程中出现了不可预知的问题,那么就可能出现更改的数据并没有同步更改的数据文件中去,比如停电等等
那么在数据库再次重启的时候,数据库会通过checkpoint去根据redo log来对每来得及持久化的内容进行恢复,这样就可以保证不管发生了什么,只要事务提交了
那么它就一定是满足一致性原理的,且恢复的速度也是比较快的。
2.用更小的空间记录更多的变化:这里为什么这么说,因为比如我们将一个数字从1,每次加1的变更,一直变更到100000。那么如果我们要是记录变更的过程,就需要很多条sql语句
但是如果使用redo log,那么就是直接在对应物理块上将1改为100000就行了,不用记录中间的过程。
所以这个redo log的设计还是很巧妙的。
接下来我们看看undo log。与redo log不同的是,undo log 是一个逻辑日志。那什么是逻辑日志呢,这里有一点需要强调以下,不管是redo还是undo ,都是要持久到磁盘上的
只是记录的内容不容,所以这里千万不要按照字面意思去片面的理解。
言归正传说说我们的undo log,所谓逻辑日志,就是记住数据变更的逻辑。
在Mysql 中如果是一条update语句,那对应的逻辑日志就是一条delete语句,如果是一个update语句那对应的就是一个反向的update语句。
这么做唯一的目的就是一旦在出现问题,需要将数据恢复到之前的状态的时候,就可以直接执行这个undo log 中的语句了。
说了这么多,其实并没有多么深入的去介绍两种日志的实现,毕竟本文的主题是事务。这里说明两种日志只是希望读者能够理解ACID的实现原理,因为毕竟只有理解了才能记得更牢。
三、隔离性及Mysq的隔离性
在第一小节中已经介绍了四种事务的隔离性,作为典型的关系型数据库Mysql也必然实现了这四种。
这里要给大家提示以下,就是四种事务级别我们是可以根据自己的实际业务场景进行更改的,就像是Mysql提供各种存储引擎以下。
如果真的是业务场景十分特殊,可以牺牲以下特性,选择一个更加高效的引擎和隔离级别都是不错的选择
然后隔离级别越低,那么性能就越高,因为它不需要在事务进行的过程中去调用很多资源去保护隔离性。
在回答这个问题之前,我们思考以下Mysql是怎么保证多个事务读取同一条记录的隔离性的呢?
这里网上很多说明是说通过锁,其实是不准确的。因为Mysql 中的锁分很多种,功能也有不同。
这里我们也是简单说明,比如我们常用的for update,其实就是一种独占锁,也就是在执行过程中,不允许其他操作对数据进行读取和更改。这样就极大的限制了并发性。
所以Mysql在除了我们熟知的行锁和表锁之外,在隔离级别中用了另外一种方式MVCC(多版本并发控制)。MVCC的实现,是通过保存数据在某个时间点的快照来实现的.
InnoDB的MVCC是通过在每行记录后面保存2个隐藏的列来实现的,一列保存了行的创建时间,一列保存了行的过期时间(或删除时间)
但它们都存储的是系统版本号
MVCC最大的作用是: 实现了非阻塞的读操作,写操作也只锁定了必要的行
MYSQL的MVCC 只在 read committed 和 repeatable read 2个隔离级别下工作
在MVCC的机制下,mysql InnoDB(默认隔离级别)的增删改查变成了如下模式:
这个小节主要是回答上面提出的问题:为什么Mysql的repeateable read隔离级别能够避免幻读呢?
SELECT:
1.InnoDB只查找版本早于当前事务版本的数据行(行的系统版本号小于等于事务的系统版本号)
2.行的删除号要么未定义,要么大于当前事务版本号,这样可以确保事务读取到的行,在事务开始之前未被删除.
INSERT:
InnoDB 为新插入的每一行保存当前系统版本号做为行版本号。
DELETE:
INNODB 为删除的每一行保存当前系统版本号作为行删除标识
UPDATE:
InnoDB 为插入的每一行新记录,保存当前系统版本号作为行版本号,同时保存当前系统版本号到原来的行作为行删除标识。
总结起来说就是,Mysql在事务中使用了MVCC,而不是直接锁住对应的行,这样在进行读取操作的时候
就可以直接读取对应的行快找版本,而不是等待其他操作释放锁,这样大大提高了并发性。
读到这里可能读者会比较好奇,那还是没有说明为什么Mysql在默认的隔离级别下是怎么解决的幻读的问题啊,别着急。了解了数据的MVCC之后我们这里就要开始解答了。
其实介绍MVCC就是为了说明数据库不仅仅有锁,还有另外一种机制用来保证数据的可见性。两种之间没有谁可以替代谁,都有不同的使用场景。
下面我们就介绍以下Mysql在REPEATABLE READ隔离级别下是怎么解决问题的。
有三种锁需要先说明一下:
1.Record Locks(记录锁):在索引记录上加锁。
2.Gap Locks(间隙锁):在索引记录之间加锁,或者在第一个索引记录之前加锁,或者在最后一个索引记录之后加锁。
3.Next-Key Locks:在索引记录上加锁,并且在索引记录之前的间隙加锁。它相当于是Record Locks与Gap Locks的一个结合
这个在官方文档中也是有说明的,但是对于使用哪种锁是根据实际情况来确定的。在默认的隔离级别中,普通的SELECT用的是一致性读不加锁
而对于锁定读、UPDATE和DELETE,则需要加锁,至于加什么锁视情况而定。如果你对一个唯一索引使用了唯一的检索条件,那么只需锁定索引记录即可;
如果你没有使用唯一索引作为检索条件,或者用到了索引范围扫描,那么将会使用间隙锁或者next-key锁以此来阻塞其它会话向这个范围内的间隙插入数据。
所以这就是Mysql默认隔离级别的秘密,通过Gap Lock或者是Next-Key Lock来保证不会出现幻读的情况。
四、分布式事务XA以及Mysql XA
上面聊了那么多ACID,相信大家对事务已经有了很多的了解,下面我们来聊聊分布式的事务。
首先我们来看看分布式事务与传统事务有什么不同的地方:
1.通信异常:我们的系统架构从集中式走向分布式,必然要引入网络这个概念。那么由于网络本身的不可靠性因此也引入了额外的问题
这里不管是路由器,DNS或者还是网络故障等等,都有可能导致分布式节点之间的网络通信出现问题,进而导致系统状态的异常。
2.网络分区:由于网络发生异常,导致分布式各节点之间的延迟不断增大,最终导致组成分布式的节点中只有部分节点能够正常通讯,而另外一些节点则不能
我们称这个现象为网络分区。当出现网络分区时,分布式系统中就会出现局部的小集群,在极端情况下,这些局部小集群会独立完成原本需要整个分布式系统才能完成的功能
这对分布式系统的一致性提出了很大的挑战。
3.三态:在分布式系统的每一次请求中,都可能存在三态的概念:成功,失败和超时。这里可能是因为网络不不可靠因素或者是其他不可预知的因素
最终的结果都是调用者不能够清晰的知道这次调用的结果,也就无法对接下来的操作作出准确的判断。
所以基于在分布式系统环境下会出现的上述三个问题,同样有别与集中式系统的ACID特性,在分布式系统中有接下来的两个理论用于支持分布式事务:
1.CAP
2.BASE
1.CAP: 首先说说CAP,在一个分布式系统不可能同时满足一致性(Consistency),可用性(Avaliablity)和分区容错性(Partition tolerance)这三个基本需求
最多只能通过满足其中的两个。
一致性:这里要特殊强调一下,这里的一致性和ACID 中的一致性是有所不同的,这里的一致性指的在分布式系统中多个副本之间能够保持的一致性,即使数据变更
也能够保证不同数据副本的一致性。
可用性:可用性是指系统提供的服务必须一直处于可用的状态,对于用户的每一次的请求总是能够在有限的时间内给出相应结果
这里的有限时间指的是必须在系统执行的相应时间之内给出相应的结果。不应该出现系统执行成功,但是返回结果超时的情况。
分区容错性:分区容错性约束了一个分布式系统中需要具备以下几个特性:分布式系统在遇到任何网络分区故障的时候,仍然能够保证对外提供满足一致性和可用性的服务
除非是整个网络环境都发生了故障。
以上就是对CAP定理的一些解释,因此我们子对分布式系统进行构建的时候,需要根据实际的业务场景进行取舍
但是对于一个分布式系统而言,分区容错性应该可以说是一个基本的要求,为什么这么说 ,非常简单因为一个分布式系统,那么各个组件必然是分不到网络中的不同节点上了
因此也就必然的出现了自网络的情况,所以也就一定会出现网络的问题。所以解决分区容错性也就是一个基本的需求,所以我们需要将经历花费在A和C的上面进行权衡。
2.BASE:
BASE其实是对CAP中一致性和可用性权衡的结果,其来源是大规模互联网分布式实践的总结,是由CAP定理逐渐演化而来
其核心思想是即使无法做到强一致性,但是每个应用都可以根据自身的业务特点,采用适当的方式使系统能够达到最终的一致性。
BA(Basically Avaliblity)基本可用
S (Soft state)软状态
E (Eventually consistent)最终一致性
基本可用:这里怎么理解基本可用呢,其实就是在分布式系统出现不可预知的故障的时候,允许损失部分可用性,但请注意这并不等同于系统不可用。
这里可以体现在相应时间上的损失和功能上的损失。
相应时间上的损失也就是超过正常系统的相应时间,可以适当的增加一些。然后功能上的损失实现的方式有很多
比如在系统压力过大的时候可以进行页面的整体或者部分降级,或者通过消息Mq的方式进行延迟处理等等。
软状态:指系统可以存在数据的中间状态,并认为该状态并不会影响系统的整体可用性,只是存在一些系统延迟而已。
最终一致性:最终一致性是强调系统中所有数据的副本,在经过一段时间的同步之后,最终都能够达到一个一致性的状态,这也算是在分布式环境下的一种妥协