数据库事务浅述

事务

事务分为:单机事务,分布式事务

单机事务:

事务(Transaction),一般是指要做的或所做的事情。在计算机术语中是指访问并可能更新数据库中各种数据项的一个程序执行单元(unit)。事务是一个原子操作。

事务是恢复和并发控制基本单位。

为何要涉及原子性操作,因为需要回滚和控制并发,假设都是单线程和不需要回滚,那就没必要定义一个原子性的执行单元。

事务应具有ACID特性:

原子性(atomicity)。一个事务是一个不可分割的工作单位,事务中包括的操作要么都做,要么都不做。

一致性(consistency)。事务必须是使数据库从一个一致性状态变到另一个一致性状态。一致性与原子性是密切相关的。

隔离性(isolation)。一个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。

持久性(durability)。持久性也称永久性(permanence),指一个事务一旦提交,它对数据库中数据的改变就应该是永久性的。接下来的其他操作或故障不应该对其有任何影响。

分析一下事务逻辑,原子性执行单元是事务定义特性,不可分割的执行单元,,所以一致性是这个单元是提交,还是回滚,回滚不对数据进行任何影响,提交,将会对原子性操作执行提交,一致性是必然的,因为涉及执行操作和处理数据的单元仅仅只有两个状态,隔离性也是必然,原子态的执行单元是之间必然不可影响,提交后已经对数据进行改变了,事务需要将数据持久化,持久性也是必然特性。

综上所述,如果要实现一个事务,就需要实现ACID特性即可,所以这重点在于实现原子性的执行单元,意味着要么提交,要么回滚。

标准事务:

对于一个标准事务来说,应该实现完全的ACID,也就是原子性执行单元,但是如果完全实现可能会对性能影响,所以事务的实际情况应该是在安全性和性能的衡量下进行选择,对应可能会出现所谓的脏读,不可重复读,幻读,串行读,隔离级别也就有,读未提交(Read uncommitted),读已提交(Read committed),可重复读(Repeatable read),串行化(Serializable )

上述你可能会存在疑惑,没关系,接着向下看。

单机事务剖析:

事务实现是实现原子性执行单元,为了是解决恢复和并发控制问题,然而实现原子性并非是这一系列操作真的变成一个操作了,而是通过一定技术方式实现ACID,其实是完全实现ACID特性的过程,在实现过程中也有中间态,原子的区别是最高隔离,其他原子看不到,对外体现的就是只有提交和回滚,安全性是最好的,但是在性能的衡量下(影响性能的主要还是和并发相关),完全隔离相当于失去并发,有违初衷,但是在特定情况下,也可能适应不同安全性和性能需求,所以隔离方面对应以隔离级别来实现,以解决并发问题,回滚问题就是进行逻辑时的自身问题,是否执行出错,是否需要回滚,实现可回滚的机制一般是通过日志回滚(参考MySQL的innoDB引擎),将一个单元的操作信息记录,如果出错,则通过回滚日志回滚,用以保障数据的一致性。

分析数据库事务,应该重点分析回滚机制和隔离级别。接下来将会在串行事务下分析回滚机制,并行事务下分析隔离机制,数据分析原型来源于MySQL的innoDB引擎的事务机制。

说事务是完全原子性的我认为从各个状态方面中都不太贴切,应该是在最高隔离级别下事务呈现原子性,或者说在一定应用角度下事务呈现原子性(意思是,在某个应用角度,该事务仅仅需要读未提交就可以达到相关的隔离效果,虽然其他方向有可能不是,但是这个应用角度没有涉及,也可以认为是)。原子态无论从什么角度来理解都应该是执行单元的表面现象,从外表看有可能是没有中间态的,但是具体实现必然是有中间态的,无非是是否可见而已。将原子剖开,其实可见其他实现过程的中间态,应该有如下:

  • Active:事务的初始状态,表示事务正在执行;
  • Partially Commited:在最后一条语句执行之后;
  • Failed:发现事务无法正常执行之后;
  • Aborted:事务被回滚并且数据库恢复到了事务进行之前的状态之后;
  • Commited:成功执行整个事务;

如果在深入剖析,进入过程内部,还会有其他的,本文不深入探究。

InnoDB实现ACID可以参考(MySQL中InnoDB引擎如何实现事务的ACID特性_tuziud233的博客-CSDN博客)

回滚机制:

在MySQL事务中回滚机制是通过回滚日志(undo log)实现的,保存了事务发生之前的数据的一个版本,可以用于回滚。比如一条 INSERT 语句,对应一条 DELETE 的 undo log ,对于每个 UPDATE 语句,对应一条相反的 UPDATE 的 undo log ,这样在发生错误时,就能回滚到事务之前的数据状态。同时,undo log 也是 MVCC (多版本并发控制) 实现的关键。

涉及到事务相关日志问题,就必然应该提到一个应用于事务持久性的日志重做日志。

前提:

数据都是先读到内存中,然后修改内存中的数据,最后将数据写回磁盘。

这里把数据库内容在磁盘上的部分叫data file,把数据库内容在内存中的缓存叫data buffer。data buffer与data file内容不同,此时把data buffer的内容叫脏数据,但是不能每次事务提交时都同步到磁盘,这样磁盘IO开销太大,应该等到data buffer内数据比较多时再全部更新到磁盘。

这里把日志磁盘上的文件叫log file,把日志在内存上的缓存叫log buffer。log buffer与log file内容也不同,但是日志可以经常性地持久化到磁盘,因为日志文件是顺序写的,所以同步日志的开销远比同步数据的开销要小。

必须保证脏页数据在同步到磁盘前,该数据页相对应的日志记录已经刷新到磁盘中。

实现机制:

在事务过程中,当我们在一个事务中尝试对数据进行修改时,它会先将数据从磁盘读入内存,并更新内存中缓存的数据,然后生成一条重做日志并写入重做日志缓存,当事务真正提交时,MySQL 会将重做日志缓存中的内容刷新到重做日志文件,再将内存中的数据更新到磁盘上。

在 InnoDB 中,重做日志都是以 512 字节的块的形式进行存储的,同时因为块的大小与磁盘扇区大小相同,所以重做日志的写入可以保证原子性,不会由于机器断电导致重做日志仅写入一半并留下脏数据。

防止在发生故障的时间点,尚有脏页未写入磁盘,在重启mysql服务的时候,根据redo log进行重做,从而达到事务的持久性这一特性。

隔离机制:

在事务进行逻辑操作是一般分为读和写,读是共享的,写是互斥的,意味着写只能同时写一个,读的话可以多个同时读,读写之间的关系是事务的隔离级别决定的,在并发控制中,分为LBCC,MVCC。

隔离级别介绍的文章非常多,本文就不多做介绍。可以参考(事务的隔离级别_zhouym_的博客-CSDN博客_事务隔离级别)

LBCC是读写串行化,事务隔离级别是Serializable,相当于没有并发,都是串行化的进行读写,读写冲突。

MVCC是读写不冲突,事务仅适用于事务隔离级别的Repeatable read,Read committed,通过多版本和快照隔离,涉及当前读和快照读,不同的隔离级别就对应不同的读取方式,很多数据库对MVCC都有不同实现,例如mysql就是使用回滚日志实现。

隔离级别Read uncommitted的话,不用啥机制,相当于没有隔离。

Spring事务:

spring事务:参考(Spring 事务管理机制概述_Rico's Blogs-CSDN博客_spring事务)

分布式事务:

分布式事务用于在分布式系统中保证不同节点之间的数据一致性。分布式事务是基于单机事务的,但是实现方法不同。

名词介绍:

事务参与者:例如每个数据库就是一个事务参与者

事务协调者:访问多个数据源的服务程序

资源管理器(Resource Manager,RM):通常与事务参与者同义

事务管理器(Transaction Manager,TM):通常与事务协调者同义

在分布式事务模型中,一个 TM 管理多个 RM,即一个服务程序访问多个数据源;TM 是一个全局事务管理器,协调多方本地事务的进度,使其共同提交或回滚,最终达成一种全局的 ACID 特性

常见解决方案:

分布式事务有很多种方案,这里介绍常见的 5 种方案:

XA 方案,TCC 方案,本地消息表,可靠消息最终一致性方案,最大努力通知方案

方案简述:

XA 方案:即“全票通过方案”,要求所有的事务系统必须全部准备好,才可以进行事务处理,这种方案好处是实现成本低简单,缺点是耗费性能,所以这种方案一般用的不多。

TCC 方案:即“局部通过方案”,要求部分事务系统准备好处理事务即可,相对比XA方案灵活了许多,同时它的处理方式是将事务问题交给系统本身处理,需要用大量的代码控制,优点是数据一致性也很高,缺点是控制事务的逻辑代码复杂冗余,性能也很差。所以这种方案也不常用。

本地消息表:这种方案是基于数据库表的一种方案,由各个系统分建自己的消息表,记录数据的发起及接收,并给数据做状态标记,借助MQ,观察消息的状态来决定事务是否需要回滚。优点是代码量少,数据可以保持最终一致性,缺点是表需要维护,且对高并发的支持不怎么好。

可靠消息最终一致性方案:该方案借助MQ,由系统发起一条预发送消息,当系统本身的事务执行完毕后再将MQ中的消息变为确认消息,同样其他系统接收到MQ的消息后开始处理本地事务,根据处理情况决定事务是否需要回滚。相对来说优点是事务控制较为灵活,缺点是不稳定因素较多。

最大努力通知方案:该方案其原理与上述方案类似,只是预发送消息也没有了,有系统处理完本地事务后直接发起MQ,而接受方是本地的一套专门处理事务的服务,此服务调用待接收系统的接口,以此处理事务。优点是事务节点较少,缺点是事务处理服务的维护成本较高,同时需要多个系统的接口才行。

方案详解:

XA方案:全票通过方案,基于XA协议,分为两种(分段提交),2PC(两段提交)和3PC(三段提交)

XA协议由Tuxedo首先提出的,并交给X/Open组织,作为资源管理器(数据库)与事务管理器的接口标准。目前,Oracle、Informix、DB2和Sybase等各大数据库厂家都提供对XA的支持。XA协议采用两阶段提交方式来管理分布式事务。XA接口提供资源管理器与事务管理器之间进行通信的标准接口。

        2PC: 即2段提交,第一阶段:perpare准备阶段,第二阶段:commit提交阶段。

准备阶段:TM给所有RM发送指令,让RM做好提交前的所有准备,然后给TM发送yes或no指令或者超时。

提交阶段:如果TM收到了所有RM返回的yes指令,则TM给所有RM发送提交指令,每个RM收到指令后提交事务,给RM返回ACK。如果收到超时指令或者其中一个RM给TM发送了no,则TM给所有RM发送回滚指令,所有TM开始回滚刚才提交的事务,然后发送ACK。

2PC虽然可以一定程度上解决一致性问题,但是缺点明显:

        1、同步阻塞问题。执行过程中,所有参与节点都是事务阻塞型的。当参与者占有公共资源时,其他第三方节点访问公共资源不得不处于阻塞状态。

        2、单点故障。由于协调者的重要性,一旦协调者发生故障。参与者会一直阻塞下去。尤其在第二阶段,协调者发生故障,那么所有的参与者还都处于锁定事务资源的状态中,而无法继续完成事务操作。(如果是协调者挂掉,可以重新选举一个协调者,但是无法解决因为协调者宕机导致的参与者处于阻塞状态的问题)

        3、数据不一致。在二阶段提交的阶段二中,当协调者向参与者发送commit请求之后,发生了局部网络异常或者在发送commit请求过程中协调者发生了故障,这回导致只有一部分参与者接受到了commit请求。而在这部分参与者接到commit请求之后就会执行commit操作。但是其他部分未接到commit请求的机器则无法执行事务提交。于是整个分布式系统便出现了数据部一致性的现象。

        4、二阶段无法解决的问题:协调者再发出commit消息之后宕机,而唯一接收到这条消息的参与者同时也宕机了。那么即使协调者通过选举协议产生了新的协调者,这条事务的状态也是不确定的,没人知道事务是否被已经提交。

        3PC:3PC的阶段在2PC的基础上新增的CanCommit阶段,并且新增了超时机制。

        1、引入超时机制。同时在协调者和参与者中都引入超时机制。
        2、在第一阶段和第二阶段中插入一个准备阶段。保证了在最后提交阶段之前各参与节点的状态是一致的。
在3PC在准备阶段前,资源调度者会事先询问各个事务参与者的状况,得到yes的回答后,才会执行percommit。具体就不叙述了。3PC解决了超时问题,但是性能差和数据不统一的问题依旧没有解决。

总结:XA方案基于XA协议完成分布式事务,XA协议比较简单,且大多商业数据库都支持,使用分布式事务的成本也比较,但是有其致命缺陷,性能和数据不统一问题,在mysql数据库中支持的不太理想,mysql的XA实现,没有记录prepare阶段日志,主备切换回导致主库与备库数据不一致。许多nosql也没有支持XA,这让XA的应用场景变得非常狭隘。

TCC 方案:补偿事务,(Try-Confirm-Cancel)

这种类型和可补偿操作类似,就是提供一种提交和回滚的机制。是一种典型的两阶段类型的操作。

TCC过程分为预留和执行,执行如果存在错误则进行取消执行。

Try: 尝试执行业务

完成所有业务检查(一致性) 预留必须业务资源(准隔离性)

Confirm:确认执行业务

真正执行业务 不作任何业务检查 只使用Try阶段预留的业务资源 Confirm操作要满足幂等性

Cancel: 取消执行业务

释放Try阶段预留的业务资源  
Cancel操作要满足幂等性

预留资源举例:正常在下单中,要对库存表中库存字段进行减操作。但是在TCC方案里面,库存表应该加一个冻结库存字段,用于计算冻结库存,在下单后,我们实际上先不对库存字段减,我们对冻结库存进行加操作,表示已经冻结,然后后端将两个字段都给前端,前端就用库存减去冻结库存,显示给用户,用户看到的是库存减后的数据,实际上没有减,只是冻结预留了

TCC分析:

所谓的TCC编程模式,也是两阶段提交的一个变种。TCC就是通过代码,人为实现了两阶段提交,不同的业务场景所写的代码都不一样,复杂度也不一样,因此,这种模式并不能很好地被复用。

TCC事务,是为了解决应用拆分带来的跨应用业务操作原子性的问题,灵活性高

Try:预留业务资源:把多个应用中的业务资源预留和锁定住,为后续的确认打下基础
Confirm:确认执行业务操作:在Try操作中涉及的所有应用均成功之后进行确认,使用预留的业务资源
Cancel:取消执行业务操作:当Try操作中涉及的所有应用没有全部成功,需要将已成功的应用进行取消(即Rollback回滚)

TCC与2PC协议比较:

TCC位于业务服务层而非资源层

TCC没有单独的准备(Prepare)阶段,Try操作兼备资源操作与准备能力

Try操作可以灵活选择业务资源的锁定粒度(以业务定粒度)

TCC有较高开发成本

本地消息表:

此方案的核心是将需要分布式处理的任务通过消息日志的方式来异步执行。消息日志可以存储到本地文本、数据库或消息队列,再通过业务规则自动或人工发起重试。人工重试更多的是应用于支付场景,通过对账系统对事后问题的处理。 

 

本地消息表实现分布式事务的最终一致性的时, 需要明白首先需要在本地数据库新建一张本地消息表,然后必须还要一个MQ(不一定是mq,但必须是类似的中间件)。

消息表怎么创建呢?这个表应该包括这些字段: id, biz_id, biz_type, msg, msg_result, msg_desc,atime,try_count。分别表示uuid,业务id,业务类型,消息内容,消息结果(成功或失败),消息描述,创建时间,重试次数, 其中biz_id,msg_desc字段是可选的。

具体怎么做呢?消息生产方(也就是发起方),需要额外建一个消息表,并记录消息发送状态。消息表和业务数据要在一个事务里提交,也就是说他们要在一个数据库里面。然后消息会经过MQ发送到消息的消费方。如果消息发送失败,会进行重试发送。

消息消费方(也就是发起方的依赖方),需要处理这个消息,并完成自己的业务逻辑。此时如果本地事务处理成功,表明已经处理成功了,如果处理失败,那么就会重试执行。如果是业务上面的失败,可以给生产方发送一个业务补偿消息,通知生产方进行回滚等操作。

生产方和消费方定时扫描本地消息表,把还没处理完成的消息或者失败的消息再发送一遍。

本地消息表总结:
此方案进行对于系统耦合性不高,通过MQ异步进行执行逻辑或者回滚,使得系统最终达成一致性,方案实现简单,代码量少,同时性能不高,缺点是表需要维护,且对高并发的支持不怎么好,在系统实时性要求不高,非关键系统模块的需求上,可以实现一致性。

可靠消息最终一致性方案

 该方案是指当事务发起方执行完成本地事务后并发出一条消息,事务参与方(消息消费者)一定能够接收消息并处理事务成功,此方案强调的是只要消息发给事务参与方最终事务要达到一致。 事务发起方(消息生产方)将消息发给消息中间件,事务参与方从消息中间件接收消息,事务发起方和消息中间件之间,事务参与方(消息消费方)和消息中间件之间都是通过网络通信,由于网络通信的不确定性会导致分布式事务问题。

在这里插入图片描述 

这种方案这是经常使用的,相对来说优点是事务控制较为灵活,缺点是不稳定因素较多。因为其网络稳定性,需要解决的问题较多,部署灵活,本地事务与消息发送的原子性问题,事务参与方接收消息的可靠性,消息重复消费的问题等等。

该方案在生产环境中使用较多,再此只是简述,原理简单,重点在于进行生产环境中高可用,如有需要可以参考(【坑爹呀!】最终一致性分布式事务如何保障实际生产中99.99%高可用? - 掘金)

 

最大努力通知方案:

该方案和可靠消息一致性方案类似,但是不进行预通知,事务发起方进行本地事务完成后进行发起通知,且通过一定的机制最大努力将业务处理结果通知到接收方。并且预留接口可以进行查询,当消息无法被接收方接收时,由接收方主动查询消息(业务处理结果)。

1、内部系统解决方案

内部系统解决方案

 

2、对外系统解决方案
通过增加通知程序,使用网络请求的方式去通知接受通知方,其中通知程序可以是单独的服务,也可以和发起通知方一个服务。

 

对外系统解决方案

两种方案的不同点

内部系统解决方案:接收通知方与MQ接口,即接收通知方案监听 MQ,此方案主要应用与内部应用之间的通知。
对外系统解决方案:由通知程序与MQ接口,通知程序监听MQ,收到MQ的消息后由通知程序通过互联网接口协议调用接收通知方。此方案主要应用于外部应用之间的通知,例如支付宝、微信的支付结果通知。

分布式事务解决方案分析:

在这五种常见分布式事务解决方案中,目的都是进行数据一致性,但是过程有所不同,XA,TCC和本地消息表,可靠消息最终一致性,最大努力通知,是不同的思想,XA和TCC方案是通过多段提交思想实现,将整个业务事务看做一个大的事务,注重实时性,应用场景应该是系统区域业务中进行,耦合性较高,强于实时性,问题于系统内相对可靠,本地消息表,可靠消息最终一致性,最大努力通知是通过消息通知思想,将整体业务事务划分,更加适合分布式微服务下应用场景,耦合性低,实时性相对低,但是性能并不一定,更加适合系统,模块间等应用。在应用方面前者有其局限性,而后者以通知方式进行的话,某些外部业务的通知也可以进行方便实现,技术无非好坏,仅看适用,个人认为有些技术解决方案在合适的需求下同样适用

参考较多文中并未一一列出,请见谅,本文有一些微末之见,如有错误,敬请指教。

2022希望在不断进步中提升自己,加油代码人。

2022.1.11

小陈

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值