分布式事务

本文探讨了分布式事务的背景和挑战,分析了CAP定理与BASE理论,介绍了强一致性、弱一致性和最终一致性。文章详细阐述了2PC、Seata等分布式事务解决方案,并讨论了各自的优缺点。此外,还提到了柔性事务,如TCC、最大努力通知型和可靠消息+最终一致性方案,特别关注了在实现这些方案中可能遇到的消息丢失、重复和积压问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

我们先复习一下本地事务

1.本地事务

数据库事务的几个特性:
    原子性(Atomicity),一致性(Consistency),持久性(Durabilily),隔离性(Isolation)

    原子性:一系列的操作整体不可拆分,同时成功同时失败
    一致性: 数据在事务前后,业务整体一致
    隔离性:事务之间互相隔离
    持久性:一旦事务成功,数据一定会落盘在数据库

1.1.事务的隔离级别

读未提交 READ UNCOMMITTED:

  该隔离级别的事务会读到其他未提交事务的数据,此现象为脏读


读已提交 READ COMMITTED:
   
  一个事务可以读取另一个已提交的事务,多次读取会造成不一样的结果,此现象称为不可重复读(Oracle默认)


可重复读 REPEATAVBLE READ:
 
 该隔离级别是mysql默认级别,在同一个事务里,select的结果是事务开始时间间点的状态,因此,同一的 
 select操作读到的结果会是一致的,但是,会有幻读现象。
 mysql的innodb引擎可以通过next-key locks机制来避免幻读


序列化 SERIALIZABLE:
 
 在该隔离级别下事务都是串行执行的,mysqk数据库的innodb引擎会给读操作隐式加一把共享锁,从而避免了脏读,幻读和不可重复读问题。
 

1.2.事务传播行为

1.PROPAGATION_REQUIRED:如果当前没有事务,就创建一个新事务,如果当前存在事务,就加入该事务,常用。

2.PROPAGATION_SUPPORTS:支持当前的事务,如果当前存在事务,就加入该事务,如果当前不存在事务,
  就以非事务方式执行。

3.PROPAGATION_MANDATORY:支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就抛异常

4.PROPAGATION_REQUIRES_NEW:创建新事务,无论当前存不存在事务,都创建新事务。

5.PROPAGATION_NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起

6.PROPAGATION_NEVER:以非事务方式执行,如果当前存在事务,就抛出异常。

7.PROPAGATION_NESTED:如果当前存在事务,则在嵌套事务内执行,如果当前没有事务,
  则执行与(PROPAGATION_REQUIRED 事务一样的操作)。

举例

@Transactional(propagation = Propagation.REQUIRED)
public void a(){

 b();
 c();

}


@Transactional(propagation = Propagation.REQUIRED)
public void b(){

}


@Transactional(propagation = Propagation.REQUIRES_NEW)
public void c(){

}


这三个方法中都使用了事务,并且a调用了b,c两个方法,a,b两个方法使用的是同一种传播行为


情况1:
  如果在a方法发生了异常,进行了回滚,那么b也会回滚,但是c不会回滚。
   
情况2:
  如果b方法发送了异常,进行了回滚,那么a方法也会进行回滚,但是c不会回滚

情况3:
  如果c方法发生了异常,进行了回滚,那么a,b方法都不会回滚。

1.3.超时时间


@Transactional(propagation = Propagation.REQUIRED,timeout = 30)
public void a(){

 b();
 c();

}

@Transactional(propagation = Propagation.REQUIRED,timeout = 5)
public void b(){

}

@Transactional(propagation = Propagation.REQUIRED_NEW,timeout = 10)
public void c(){

}


这里设置了超时时间,标识事务30秒没有执行完返回,则就回滚
那么都设置了超时时间以哪个准呢?


说明:
  a方法:首先a方法肯定是30秒事务没有返回就回滚。
  b方法:而b方法的事务的传播行为原因,导致和a方法共用一个事务,所以也是30秒。
  c方法:由于事务的传播行为不同,所以c方法和a,b都是相同的事务,所以是10秒
  
 
 

1.4.事务的坑

在同一个类里面,编写两个方法,内部调用的时候,会导致事务设置失效,原因是没有用到代理对象的原因。

如下面的代码方法都是在一个方法中的话:

@Transactional(propagation = Propagation.REQUIRED)
public void a(){
 b();
 c();

}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void b(){

}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void c(){

}

这样的话,
相当于是b方法的内容和c方法的内容直接放到了a方法中,继续使用的是a的事务,
因为不做配置相当于绕过了代理。没有使用他们直接的事务。


解决:
 
1.导入spring-boot-start-aop   它引入了aspectj动态代理
2.@EnableTransactionManagement(proxyTargetClass=true)

3.@EnableAspectJAutoProxy(exposeProxy=true)   开启使用aspectj动态代理,
     exposeProxy=true:表示对外暴露代理对象



那么使用的时候也会有所不同:

使用AopContext.currentProxy()调用方法
   
   类  变量名称 = (类)AopContext.currentProxy() 大胆使用,因为已经代理过了。

   然后通过  变量名称.方法    

注意,只有同一个类里面的事务方法才这样使用,其他的无需这样,直接调用。


2.分布式事务

分布式我们知道是多个服务模块提供服务,形成一个程序或者一个系统,而当多个服务需要在处理与数据库相关内容的时候,由于链路调用或者是异步调用,那么他们必须是一个事务,必须要保证事务的原子性,也就是要么都成功,要么都失败。

那么传统的事务可以解决分布式服务之间的调用出现的问题吗?

比如:

订单模块

在生成订单数据往数据库存之前,要有以下操作:

   首先是否有足够的库存,我们都假设可以。


   那么在订单模块生成订单存入数据库-->调用远程的减库存操作-->其他的远程服务

   
   注意:
      现在的处理方式是,生成订单,减库存,其他服务,其中发生了异常可以回滚,看起来没有什么问题,
      也就是说订单会判断减库存或者其他的服务的返回状态,来决定是否进行事务回滚。

   传统的单体应用这种事务是可行的,但是分布式下是不可行的。


答案是不可行的。

问题:

订单服务生成订单--->减库存服务--->其他服务



由于是分布式微服务之间的调用,那么我们就不能保证他的调用不会被网络所波动影响

比如:

1.

  当我们生成订单并且放入到数据库后,远程调用减库存的服务,减库存成功了,但由于网络的原因,
  或者其他的原因,导致连接超时,那那么我们的订单服务就接收到了异常,就进行了回滚的操作,
  减库存的服务虽然也有事务,但是因为减库存成功,不会进行回滚。那么问题来了,此时订单是回滚成功的,
  但是我们的库存是没有进行回滚的。那么问题大了,


2.或者说减库存成功了,但是库存后面的其它服务异常,回滚了,但是远程的减库存没有回滚。

由此可知,我们普通的事务是无法解决这样的业务场景的。


解决:

利用消息队列实现最终一致


库存服务锁定成功后发给消息队列消息(当前库存工作单),过段时间自动解锁,解锁时先查询订单的支付状态,
解锁成功修改库存工作单详情项状态为已解锁

2.1.cap定理与base理论

2.1.1.cap

cap原则又称为cap理论,指的是在一个分布式系统中

》一致性(Consistency)
  在分布式系统中的所有数据备份,在同一时刻是否相同的值。(等同于所有节点访问同一份最新数据)

》可用性(Availability)
  在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求。(对数据更新具备高可用性)

》分区容错性(Partition tolerance)
  大多数分布式系统都分布在多个子网络,每隔子网络就叫做一个区(partition)。
  分区容错的意思是,区间通信可能失败。


cap原则指的是,这三个要素最多只能同时实现两点。


一般来说,分区容错无法避免,剩下的c和a无法同时做到,必须二选一。


那么就会有两个组合ap和cp.

ap:是高可用和分区容错,那么将有可能读到不同的数据。
cp:是一致性和分区容错,那么我们要怎么保证一致性呢?
    分布式系统中实现一致性的raft算法 http://thesecretlivesofdata.com/raft/
    
  

2.1.2.base理论

是对cap理论的延申,思想是即使无法做到强一致性(cap的一致性就是强一致性),但可以采用适当的采取弱一致性,即最终一致性。


base是指
 1.基本可用(Basically Available)
    1.基本可用是指分布式系统出现故障的时候,运行损失部分可用性(例如响应时间,功能上的可用性),
      允许损失部分可用性,需要注意的是:基本可用绝不等价于系统不可用。

        1.响应时间上的损失,正常情况下搜索引擎需要在0.5秒之内返回给用户相应放入查询结果,
          但由于出现故障,断网或者断电,查询结果的相应时间增加1~2秒
        
        2.功能上的损失:购物网站在购物高峰,为了保证系统的稳定性,
          部分消费者可能会被引导到降级页面。

 2.软状态(Soft state)
    软状态是指允许系统存在中间状态,而该中间状态不会影响系统整体可用性,
    分布式存储中一般一份数据会有多个副本,允许不同副本同步的延时就是软连接的体现。
    mysql replication的异步复制也是一种体现。


3.最终一致性(Eventual Consistency)
    最终一致性是指系统的所有数据副本经过一定时间后,最终能够达到一致的状态

2.2.强一致性,弱一致性,最终一致性

从客户端角度,多进程并发访问时,更新过的数据在不同进程如何获取的不同策略,决定了不同的一致性,对于关系型数据库,要求更新过的数据能被后续的访问都能看到,这是强一致性,如果能容忍后续的部分或者全部访问不到,则是弱一致性,如果经过一段时间后要要求能访问到更新后的数据,则是最终一致性。
弱一致性是最终一致性的特殊情况。

2.3.分布式事务解决方案

2.3.1.2pc模式

数据库支持的2pc(2 phase commit 二阶提交),又叫做XA Transaction。

mysql5.5开始支持,sql server 2005开始支持,oracle 7 开始支持



第一阶段: 事务协调器要求每隔设计事务的数据库预提交(precommit)此操作,并反映是否可以提交。
          1.协调者向所有参与者发送事务内容,询问是否可以提交事务,并等待答复
          2.各事务参与者执行事务操作,将undo和redo信息记录到事务日志中,(不提交事务)
          3.如参与者执行成功,给协调者反馈同意,否则反馈终止。

第二阶段:事务协调器要求每个数据库提交数据。
 
其中,如果有任何一个数据库否决此次提交,那么数据库都会被要求回滚他们在此事务中的那部分信息。
         
         1.协调者节点向所有参与者节点发出正式提交的请求
         2.参与者节点正式完成操作,并释放整个事务期间占用的资源
         3.参与者节点向协调者节点发送ack完成信息。
         4.协调者节点收到所有参与者节点反馈的ack完成信息后,完成事务。


  如果任一参与者节点在第一阶段返回的响应消息为终止,或者协调者节点在第一阶段的询问超时之前无法获取所有参与者节点的响应消息时:
    1.协调者节点向所有参与节点发出 回滚操作的请求。
    2.参与者节点利用阶段1写入的undo信息执行回滚,并释放整个事务期间内占用的资源
    3.参与者节点向协调节点发送ack回滚完成的信息。

》XA协议比较简单,而且一旦商业数据库实现了xA协议,使用分布式事务的成本也就比较低。
》XA性能不理想,特别是在交易下单链路,往往并发量很高,XA无法满足高并发场景。
》XA目前商业数据库支持的比较理想,在mysql数据库中支持的不太理想。mysql的XA实现,
  没有记录prepare阶段日志,主备切换会导致主库与备库数据不一致。

》许多nosql也没有支持XA,这让XA的应用场景变得非常狭隘。
》也有3pc,引入超时机制(无论协调者还是参与者,在向对方发送请求后,若长时间未收到回应则做相应处理)

 不足:

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

2.可靠性问题:参与者发生故障时,协调者需要给每个参与者额外指定超时机制,超时后整个事务失效,协调者发生故障会一直阻塞下去,需要额外的备机进行容错。

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



    

使用seata来完成分布式事务,

seata虽然也是二阶段提交,但是还是有区别的,seata的第一阶段就已经提交了事务

 seata解决方案

补充:三阶段提交

三阶段提交协议是二阶段提交协议的改进版本,三阶段提交有两个改动。

1.在协调者和参与者中都引入超时机制。
2.在第一阶段和第二阶段中插入一个准备阶段,保证了在最后提交阶段之前各参与节点的状态是一致的。

也就是说,除了引入超时机制之外,3pc把2pc的准备阶段一分为二,这样就变成了 CanCommit,PreCommit,
  DoCommit三个阶段:




1.CanCommit阶段
  
  协调者向参与者发送commit请求,参与者如果可以提交就返回Yes响应,否则返回No响应。

2.PreCommit阶段
  
  协调者根据参与者的反应情况决定是否可以继续事务的PreCommit操作。根据响应情况,
  有以下两种可能:
    A.假如协调者从所有的参与者获得的反馈都是Yes响应,
      那么就会进行事务的预执行:
         发送预提交请求。协调者向参与者发送PreCommit请求,并进入准备阶段。事务预提交。
         参与者接收到PreCommit请求后,会执行事务操作,并将undo和redo信息记录到事务日志中。
         响应反馈。如果参与者成功的执行了事务操作,则返回ACK响应,同时开始等待最终指令。
    
    B.有任何一个参与者向协调者发送no提交响应,或者等待超时之后,协调者都没有接到参与者的响应, 
      那么就中断事务:
         发送中断请求。协调者向所有参与者发送终止请求请求。
         中断事务。参与者收到来自协调者的终止请求之后(或超时之后,仍未收到参与者的请求),
         执行事务的中断。


3.DoCommit阶段
   
   该阶段进行真正的事务提交,也可以分为以下两种情况:
   提交事务:
      A.发送提交请求。协调者接收到参与者发送的ACK响应,那么他将从预提交状态进入到提交状态。
        并向所有参与者发送doCommit请求。
      B.事务提交。参与者接收到doCommit请求之后,执行正式的事务提交。
        并在完成事务提交之后释放所有事务资源。
      C.响应反馈。事务提交完之后,向协调者发送ACK响应。
      D.完成事务。协调者接收到所有参与者的ACK响应之后,完成事务。


   中断事务:
      协调者没有接收到参与者发送的ACK响应(可能是接受者发送的不是ACK响应,也可能响应超时),
      那么就会执行中断事务:
         1.发送中断请求,如果协调者处于工作状态,向所有参与者发出中断请求。
         2.事务回滚 ,参与者接收到中断请求后,利用回滚日志来进行回滚,释放资源。
         3.反馈结果,参与者完成事务回滚之后,向协调者反馈ack消息。
         4.中断事务,协调者接收到参与者反馈的ack消息后,执行事务的中断。

 注意

在doCommit阶段,如果参与者无法及时接收到来自协调者的DoCommit或者中断请求,会在等待超时之后,会继续进行事务的提交,因为建立在第二阶段都是有正确响应返回给协调者的,

优缺点

优点:
 
相比二阶段提交,三阶段提交降低了阻塞范围,在等待超时后协调者或参与者会中断事务,避免了协调者单点问题,阶段3出现问题,参与者会继续提交事务。


缺点:
  
 数据不一致问题,当参与者收到preCommit请求后等待doCommit指令,此时如果协调者请求中断事务,而协调者无法与参与者正常通信,会导致参与者继续提交事务,造成数据不一致。

2.3.2.柔性事务-TCC事务补偿型方案

刚性事务:遵循ACID原则,强一致性。
柔性事务:遵循BASE理论,最终一致性。
与刚性事务不同,柔性事务允许一定时间内,不同节点的数据不一致,但要求最终一致。



一阶段prepare行为:调用自定义的prepare逻辑。
二阶段commit行为:调用自定义的commit逻辑。
三阶段rollback行为:调用自定义的rollback逻辑。

所谓TCC模式,是指把自定义的分支事务纳入到全局事务的管理中。


就是将代码分成三块,一块准备数据,第二部分是提交,第三部分是事务补偿(异常了,出错了等就进行回滚)

2.3.3.柔性事务-最大努力通知型方案

按规律进行通知,不保证数据一定能通知成功,但会提供可查询操作接口进行核对。
这种方案主要用在与第三方系统通讯时,比如:调用微信或支付宝支付后的支付结果通知。
这种方案也结合mq进行实现,例如:通过mq发送http请求,设置最大通知次数,达到通知次数后即不再通知。


案例:
  
银行通知,商户通知等(各大交易业务平台间的商户通知:多次通知,查询校对,对账文件),支付宝的支付成功异步回调



如订单模块--》调用远程减库存--->调用远程会员积分等服务

然后再订单模块发生了异常,我们就使用在mq里面发送通知,通知远程库存和会员服务,说这次业务的失败,
要求事务补偿回退到之前的样子,那么这两个服务就得先订阅这个消息,如果通知不成功,我们就个一段时间再通知,知道确认接收到了通知为止。

2.3.4.柔性事务-可靠消息+最终一致性方案(异步确保型)

实现:
  业务处理服务在业务事务提交之前,向实时消息服务请求发送消息,实时消息服务只记录消息数据,而不是真正的发送,业务处理服务在业务事务提交之后,向实时消息服务确认发送,只有在得到确认发送指令后,实时消息服务才会真正发送。

延迟队列解决方案https://blog.youkuaiyun.com/weixin_43679491/article/details/119974531

那么这种方案最主要保证的是可靠消息:

2.3.4.1.消息丢失

消息丢失
  
.消息发送出去,由于网络问题没有抵达服务器
  >做好容错方法(try-catch),发送消息可能会网络错误,失败后要有重试机制,可记录到数据库,采用定期 
    扫描重发的方式。
  >做好日志记录,每个消息状态是否都被服务器收到都应该记录。
  >做好定期重发,如果消息没有发送成功,定期去数据库扫描未成功的消息进行重发。

.消息抵达Broker,Broker要将消息写入磁盘才算成功。此时Broker尚未持久化完成,宕机。
  >publisher也必须加入确认回调机制,确认成功的消息,修改数据库消息状态。

.自动ACK的状态下,消费者收到消息,单没来的机处理消息就宕机了。
  >一定要开启收到ACK,消费才移除,失败等其他原因重新入队列。

2.3.4.2.消息重复

消息重复

.消息消费成功,事务已经提交,ack时,机器宕机,导致没有ack成功,Broker的消息重新有unack变为ready,
  并发送给其他消费者。

.消息消费失败,由于重试机制,自动又将消息发送出去

.成功消费,ack时宕机,消息由unack变为ready,Broker又重新发送。
  >消费者的业务消费接口应该涉及为幂等性,
  >使用防重表,发送消息每一个都有业务的唯一标识,处理过就不用处理。
  >RabbitMQ的每一个消息都有redelivered字段,可以获取是否是被重新投递过来的,

2.3.4.3.消息积压

.消费者宕机积压
.消费者消费能力不足积压
.发送者发送流量太大
  >上线更多的消费者
  >上线专门的队列消息服务,将消息先批量取出,记录数据库,离线慢慢处理。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值