分布式事务方案 总结

本文详细介绍了分布式事务的ACID特性、事务隔离级别以及Spring的事务支持。重点讨论了多种分布式事务解决方案,如TCC、可靠消息最终一致性、最大努力通知、XA规范以及2PC、3PC协议。同时,提到了Atomikos、Seata AT模式和ByteTCC的具体实现,分析了各种方案的优缺点和适用场景,并探讨了在实际应用中可能出现的问题及其应对策略。

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

事务的ACID

 

这个先说一下ACID,必须得知道:

(1)Atomic:原子性,就是一堆SQL,要么一起成功,要么都别执行,不允许某个SQL成功了,某个SQL失败了,这就是扯淡,不是原子性。

(2)Consistency:一致性,这个是针对数据一致性来说的,就是一组SQL执行之前,数据必须是准确的,执行之后,数据也必须是准确的。别搞了半天,执行完了SQL,结果SQL对应的数据修改没给你执行,那不是坑爹么。

(3)Isolation:隔离性,这个就是说多个事务在跑的时候不能互相干扰,别事务A操作个数据,弄到一半儿还没弄好呢,结果事务B来改了这个数据,导致事务A的操作出错了,那不就搞笑了。

(4)Durability:持久性,事务成功了,就必须永久对数据的修改是有效的,别过了一会儿数据自己没了,不见了,那就好玩儿了。

 

 

事务隔离级别

 

(1)读未提交,Read Uncommitted:这个很坑爹,就是说某个事务还没提交的时候,修改的数据,就让别的事务给读到了,这就恶心了,很容易导致出错的。这个也叫做脏读。

(2)读已提交,Read Committed(不可重复读):这个比上面那个稍微好一点,但是一样比较尴尬,就是说事务A在跑的时候, 先查询了一个数据是值1,然后过了段时间,事务B把那个数据给修改了一下还提交了,此时事务A再次查询这个数据就成了值2了,这是读了人家事务提交的数据啊,所以是读已提交。这个也叫做不可重复读,就是所谓的一个事务内对一个数据两次读,可能会读到不一样的值。

(3)可重复读,Read Repeatable:这个就是比上面那个再好点儿,就是说事务A在执行过程中,对某个数据的值,无论读多少次都是值1;哪怕这个过程中事务B修改了数据的值还提交了,但是事务A读到的还是自己事务开始时这个数据的值。

(4)串行化:幻读,不可重复读和可重复读都是针对两个事务同时对某条数据在修改,但是幻读针对的是插入,比如某个事务把所有行的某个字段都修改为了2,结果另外一个事务插入了一条数据,那个字段的值是1,然后就尴尬了。第一个事务会突然发现多出来一条数据,那个数据的字段是1。如果要解决幻读,就需要使用串行化级别的隔离级别,所有事务都串行起来,不允许多个事务并行操作。

MySQL的默认隔离级别是Read Repeatable,就是可重复读,就是说每个事务都会开启一个自己要操作的某个数据的快照,事务期间,读到的都是这个数据的快照罢了,对一个数据的多次读都是一样的。

MySQL是通过MVCC机制来实现的,就是多版本并发控制,multi-version concurrency control。 实现可以重复读

 

Spring的事务支持以及传播特性

(1)PROPAGATION_REQUIRED(默认,支持原事务,如果没有事务就以自己事务为准):这个是最常见的,就是说,如果ServiceA.method调用了ServiceB.method,如果ServiceA.method开启了事务,然后ServiceB.method也声明了事务,那么ServiceB.method不会开启独立事务,而是将自己的操作放在ServiceA.method的事务中来执行,ServiceA和ServiceB任何一个报错都会导致整个事务回滚。这就是默认的行为,其实一般我们都是需要这样子的。

(2)PROPAGATION_SUPPORTS(支持原事务):如果ServiceA.method开了事务,那么ServiceB就将自己加入ServiceA中来运行,如果ServiceA.method没有开事务,那么ServiceB自己也不开事务

(3)PROPAGATION_MANDATORY(调用方法必须有事务):必须被一个开启了事务的方法来调用自己,否则报错

(4)PROPAGATION_REQUIRES_NEW(开启新的事务):ServiceB.method强制性自己开启一个新的事务,然后ServiceA.method的事务会卡住,等ServiceB事务完了自己再继续。这就是影响的回滚了,如果ServiceA报错了,ServiceB是不会受到影响的,ServiceB报错了,ServiceA也可以选择性的回滚或者是提交。

(5)PROPAGATION_NOT_SUPPORTED(不支持事务,不影响事务):就是ServiceB.method不支持事务,ServiceA的事务执行到ServiceB那儿,就挂起来了,ServiceB用非事务方式运行结束,ServiceA事务再继续运行。这个好处就是ServiceB代码报错不会让ServiceA回滚。

(6)PROPAGATION_NEVER(不能被一个事务来调用):不能被一个事务来调用,ServiceA.method开事务了,但是调用了ServiceB会报错

(7)PROPAGATION_NESTED(事务嵌套,回滚自己的事务):开启嵌套事务,ServiceB开启一个子事务,如果回滚的话,那么ServiceB就回滚到开启子事务的这个save point。

 

Spring aop 事务源码

1.TransactionIntercepor其实就是如果我们给我们的service组件加了@Transactional注解之后,就会对我们的这个service组件里的方法在调用的时候,就会先走这个TransactionoIntercepor的拦截 执行invoke方法

2.执行前 TransactionIntercepor 会先 调用 我们ORM 框架去 beginTransaction 开启事务 然后 调用 我们具体方法

3.根据调用是否异常判断是否回滚和提交 调用TransactionManager 调用对应的方法

 

 

 

 

 

使用场景

TCC分布式事务:解决的就是大多数的分布式事务场景,服务链式调用

可靠消息最终一致性方案:重要必须执行成功,但是耗时,可以接受异步化

最大努力通知方案:耗时,可以接受异步化,而且可有可无,可以接受失败不成功

 

 

XA规范以及2PC分布式事务理论介绍

 

有个叫做X/Open的组织定义了分布式事务的模型,这里面有几个角色,就是AP(Application,应用程序),TM(Transaction Manager,事务管理器),RM(Resource Manager,资源管理器),CRM(Communication Resource Manager,通信资源管理器)

其实Application说白了就是我们的系统,TM的话就是一个在系统里嵌入的一个专门管理横跨多个数据库的事务的一个组件,RM的话说白了就是数据库(比如MySQL)

 

 

那么XA是啥呢?说白了,就是定义好的那个TM与RM之间的接口规范,就是管理分布式事务的那个组件跟各个数据库之间通信的一个接口:

比如管理分布式事务的组件,TM就会根据XA定义的接口规范,刷刷刷跟各个数据库通信和交互,告诉大家说,各位数据库同学一起来回滚一下,或者是一起来提交个事务把,大概这个意思这个XA仅仅是个规范,具体的实现是数据库产商来提供的,比如说MySQL就会提供XA规范的接口函数和类库实现,等等

 

 

2PC协议

 

2PC说白了就是基于XA规范搞的一套分布式事务的理论方案,也可以叫做一套规范,或者是协议,都ok。Two-Phase-Commitment-Protocol,两阶段提交协议

2PC,其实就是基于XA规范,来让分布式事务可以落地,定义了很多实现分布式事务过程中的一些细节

 

(1)准备阶段 prepare 就是不提交罢了

 

1.通知:TM 发送 prepare RM 

2.RM 接收到通知 本地执行事务,不提交返回执行结果 给 TM

(2)提交阶段

1.根据各个RM的返回结果 判断 发送commit 和 rollback(失败或者超时)

总结: 

1.RM 一阶段的 prepare 不提交 锁资源  性能低

2.TM单点故障

3pc协议

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

2.PreCommit阶段:     假如协调者从所有的参与者获得的反馈都是Yes响应,那么就会执行事务的预执行,协调者发送PreCommit消息,执行prepare。

3.doCommit阶段:     该阶段进行真正的事务提交,执行提交。

总结:

1.引入了CanCommit阶段 :rm 出问题就不用执行sql了

2.在DoCommit阶段,各个库自己也有超时机制,解决了TM挂掉的单点问题,资源阻塞问题也能减轻一下

3pc缺陷:如果人家TM在DoCommit阶段发送了abort消息给各个库,结果因为脑裂问题(TM挂了或者RM网络问题),某个库没接收到abort消息,自己还颠儿颠儿的执行了commit操作,不是也不对么

 

实现方案

0.mysql原生api就支持XA规范的分布式事务 ,手动基于api的形式 手动控制

1.客户端的TM第三方库Atomikos  支持XA规范

2.TCC方案  ByteTCC(saga事务) Seata TCC  tcc-transaction框架  服务调用执行效率比较快的,不需要异步调用的

 

TCC的全程是:Try、Confirm、Cancel。

这个其实是用到了补偿的概念,分为了三个阶段:

 

1)Try阶段:这个阶段说的是对各个服务的资源做检测以及对资源进行锁定或者预留(字段:状态,金额字段)

2)Confirm阶段:这个阶段说的是在各个服务中执行实际的操作(失败机制 :confirm 失败会一直重试,不会触发cancel 所以try 和 confirm 或者 try cancel 配对的 不存在你 confirm 后 在cancel)

3)Cancel阶段:如果任何一个服务的业务方法执行出错,那么这里就需要进行补偿,就是执行已经执行成功的业务逻辑的回滚操作

总结:代码侵入性强,性能好 需要强一致性

 

3.本地消息表

国外的ebay搞出来的这么一套思想

这个大概意思是这样的

1)A系统在自己本地一个事务里操作同时,插入一条数据到消息表

2)接着A系统将这个消息发送到MQ中去

3)B系统接收到消息之后,在一个事务里,往自己本地消息表里插入一条数据,同时执行其他的业务操作,如果这个消息已经被处理过了,那么此时这个事务会回滚,这样保证不会重复处理消息

4)B系统执行成功之后,就会更新自己本地消息表的状态以及A系统消息表的状态

5)如果B系统处理失败了,那么就不会更新消息表状态,那么此时A系统会定时扫描自己的消息表,如果有没处理的消息,会再次发送到MQ中去,让B再次处理

6)这个方案保证了最终一致性,哪怕B事务失败了,但是A会不断重发消息,直到B那边成功为止

这个方案说实话最大的问题就在于严重依赖于数据库的消息表来管理事务啥的???这个会导致如果是高并发场景咋办呢?咋扩展呢?所以一般确实很少用

4.可靠消息最终一致性方案   

这个方案,适合于那那种比较耗时的操作,通过这个消息中间件做成异步调用,发送一个消息出去,人家服务消费消息来执行业务逻辑

 

这个的意思,就是干脆不要用本地的消息表了,直接基于MQ来实现事务。比如阿里的RocketMQ就支持消息事务。

大概的意思就是:

1)A系统先发送一个prepared消息到mq,如果这个prepared消息发送失败那么就直接取消操作别执行了

2)如果这个消息发送成功过了,那么接着执行本地事务,如果成功就告诉mq发送确认消息,如果失败就告诉mq回滚消息

3)如果发送了确认消息,那么此时B系统会接收到确认消息,然后执行本地的事务

4)mq会自动定时轮询所有prepared消息回调你的接口,问你,这个消息是不是本地事务处理失败了,所有没发送确认消息?那是继续重试还是回滚?一般来说这里你就可以查下数据库看之前本地事务是否执行,如果回滚了,那么这里也回滚吧。这个就是避免可能本地事务执行成功了,别确认消息发送失败了。

5)这个方案里,要是系统B的事务失败了咋办?重试咯,自动不断重试直到成功,如果实在是不行,要么就是针对重要的资金类业务进行回滚,比如B系统本地回滚后,想办法通知系统A也回滚;或者是发送报警由人工来手工回滚和补偿 或者 通过zookeeper 来监听来通知 A 系统 重新发消息

这个还是比较合适的,目前国内互联网公司大都是这么玩儿的,要不你举用RocketMQ支持的,要不你就自己基于类似ActiveMQ?RabbitMQ?自己封装一套类似的逻辑出来,总之思路就是这样子的

 

可靠消息:

1.rocketMQ 通过prepared自动扫描 回调A系统 继续发送confirm消息 ,让A系统一定会发送确认消息

2.rocketMQ通过配置时可以达到消息持久化不丢失

 

5.最大努力通知方案 :比较适合那种不太核心一些服务调用的操作,比如说消息服务,充值好了以后发送短信,一般来说肯定是要发出去短信的,但是如果真的不小心发送失败了,发送短信失败了也无所谓的。。。

 

 

这个方案的大致意思就是:

1)系统A本地事务执行完之后,发送个消息到MQ

2)这里会有个专门消费MQ的最大努力通知服务,这个服务会消费MQ然后写入数据库中记录下来,或者是放入个内存队列也可以,接着调用系统B的接口

3)要是系统B执行成功就ok了;要是系统B执行失败了,那么最大努力通知服务就定时尝试重新调用系统B,反复N次,最后还是不行就放弃

 

6.saga 长事务类型 业务流程长

 

不锁资源 交错执行 异步提交  每个子事务都对其他事务所有补偿行为不需要Confirm操作

参与者包含其它公司或遗留系统服务,无法提供 TCC 模式要求的三个接口

 

 

 

 

ByteTCC具体实现

两种模式:

1.默认配置模式 三个接口写在一个类,仅适合一个接口只提供一个方法的服务场景,当一个接口有多个方法时,必须使用一般配置。

2.自定义模式 写三个类 分别放 对应三个接口 在注解上指定类

 

CompensableContextAware 上下文对象  可以在 三个接口 中 传递 数据
/**
 * ByteTCC倾向于认为: 使用SpringCloud时, 直接对外提供服务的Controller应该明确规划好它是普通服务还是TCC服务.<br />
 * 因此, 0.4.x版本强制对外提供TCC服务的Controller必须加@Compensable注解(若没有实质业务, 也可以不必指定confirmableKey和cancellableKey).<br />
 * 若不加@Compensable注解, 则ByteTCC将其当成普通服务对待, 不接收Consumer端传播的事务上下文. 若它后续调用TCC服务, 则将开启新的TCC全局事务.
 */
@Compensable(interfaceClass = IAccountService.class, confirmableKey = "accountServiceConfirm", cancellableKey = "accountServiceCancel")

1.ByteTCC对feign进行了封装生成了自己的动态代理对象 CompensableFeignHandler,

对feign的调用之前做了自己的一些事情(请求头(上下文 事务id) ribbon(负载均衡 try 和 cc 是不是同一个服务--正常的ribbon是会去轮询 是不会同一个服务的)

 

在try阶段,只要你调用的服务,都会记录到resourceList中去。服务报错会在本地直接回滚然后通知TransactionInceptor ,它会根据CompensableTransactionImpl(事务管理)中的resourceList(调用过的接口集合 和 状态 包含 ip host) 去基于restTemplate 去调用回滚接口

 

 

 

 

2.重启服务做的事情:

2.1.CompensableAnnotationValidator 扫描 compensable注解的bean(校验这些bean是否合法 -- tcc接口是否有加事务注解)

2.1生成自己的数据源代理对象 -- 记录日志(事务的全局id 事务的状态

2.2启动ResourceAdapterImpl 后台任务组件 -- 恢复日志里未完成的事务

2.3 扫描的tcc接口封装成bean,提供给事务调用使用

 

使用:

(1)LocalXADataSource,bytecc框架的,来封装了一下底层的数据源

(2)@Compensable注解,以及tcc三个接口

(3)@Import(SpringCloudConfiguration.class)

(4)@ImportResource({ "classpath:bytetcc-supports-springcloud.xml" })  

@Import和@ImportResource里面一定是搞了一大堆bytetcc自己的一些bean以及组件,在系统启动的时候这里的bean和组件就会开始工作,比如说,在服务里,你既然加了@Compensable注解之后,肯定得有组件去扫描和识别这个注解,然后将每个接口封装成tcc三个接口

 

 

 

 

 

 

 

可靠消息最终一致性

 

具体的执行流程如下所示

(1)上游服务发送一个待确认消息给可靠消息服务

(2)可靠消息服务将这个待确认的消息保存到自己本地数据库里,保存起来,但是不发给MQ,这个时候消息的状态是“待确认”

(3)上游服务操作本地数据库

(4)上游服务根据自己操作本地数据库的结果,来通知可靠消息服务,可以确认发送消息了,或者是删除消息

操作完本地数据库之后,会有两个结果,第一个结果是操作失败了,第二个结果是操作成功了,如果本地数据库操作失败了,本地操作会回滚,回滚之后,上游服务就要通知可靠消息服务删除消息;如果本地数据库操作成功了,那么此时本地事务就提交了,接着就可以通知可靠消息服务发送消息

(5)可靠消息服务将这个消息的状态修改为“已发送”,并且将消息发送到MQ中间件里去

这个环节是必须包裹在一个事务里的,如果发送MQ失败报错,那么可靠消息服务更新本地数据库里的消息状态为“已发送”的操作也必须回滚,反之如果本地数据库里的消息状态为“已发送”,那么必须成功投递消息到MQ里去

@Transactional

public void confirmMessage(Long messageId) {

messageDAO.updateStatus(messageId, MessageStatus.SENT);

rabbitmqProducer.send(message);

}

如果更新数据库里的消息状态报错了,那么消息根本不会投递到MQ里去;如果更新数据库里的消息状态成功了,但是事务还没提交,然后将消息投递到MQ里去报错了,此时事务管理器会感知到这个异常,然后会直接回滚掉整个事务,更新数据库里消息状态的操作也会回滚掉的

就可以保证说,更新数据库里的消息状态和投递消息到MQ,要么一起成功,要么一起失败,这里这第5个步骤,必须是一起成功或者是一起失败

MQ,rabbitmq,都有事务消息的一个实现,你可以先去投递一个prepare的消息,接着如果说数据库操作成功过了,那么就commit那个消息发送给rabbitmq,然后如果数据库操作失败了,就通知mq去rollback一条消息

但是MQ的事务消息最好别轻易用,因为那个性能实在是太低了,吞吐量太差

所以说我这里给大家介绍的是上面的那种方案

(6)下游服务从MQ里监听到一条消息

(7)下游服务根据消息,在自己本地操作数据库

(8)下游服务对本地数据库操作完成之后,对MQ进行ack操作,确认这个消息处理成功

现在的MQ中间件,无论是rabbitmq、rocketmq、kafka,都是支持手动ack。如果你是使用的默认自动ack的模式,那么就会导致消息的丢失;现在一般都会用手动ack,当本地操作执行成功之后,再对MQ执行手动的ack确认

只有当我手动ack确认之后,mq才会删除消息

如果我还没ack,本地数据库比如操作失败报错了,此时MQ一直没收到ack消息,会怎么样呢?此时MQ会保证重新投递一次消息,可以给其他的消费者实例去消费

(9)下游服务对MQ进行ack之后,再给可靠消息服务发送个请求,通知该服务说,ok,我这里处理完毕了,可靠消息服务收到通知之后,将消息的状态修改为“已完成”

可靠消息方案,方案思想,大概就是这样子,分为这么几个组件,整个流程基本上就是这样子,当然还得加一些额外机制进去

 

 

 

咱们来针对下面9个步骤里的任何一个环节如果失败了,进行一下分析

(1)上游服务发送一个待确认消息给可靠消息服务

比如说springcloud服务之间的调用,超时了,网络问题,调用失败,通知失败了,如果这个环节失败了,也就是上游服务就没法发送一个待确认消息给可靠消息服务,那么无所谓啊,从头儿开始就失败了,数据没有任何不一致吧

(2)可靠消息服务将这个待确认的消息保存到自己本地数据库里,保存起来,但是不发给MQ,这个时候消息的状态是“待确认”

可能就是可靠消息服务插入自己本地数据库的时候报错了,不知道是因为什么愿意吧,反正就是报错了如果这个环节失败了,也就是说可靠消息服务没有成功的将消息保存在本地数据库,比如自己本地数据库插入报错了,那也没事啊,因为肯定会返回给上游服务一个失败的消息的,此时上游服务就不会继续往下执行了,对吧

(3)上游服务操作本地数据库

如果这个环节失败了,上游服务执行本地数据库操作以及发送确认消息这个绑定在一块儿的事儿没干成功,那么也无所谓啊,因为这个时候,本地操作也会回滚,然后也不会继续发送确认消息给可靠消息服务了

如果本地事务回滚了之后,正常来说就会发送消息通知可靠消息服务,删除那条消息其实也不要紧的

(4)上游服务通知可靠消息服务,可以确认发送消息了

如果这个环节失败了,那么尴尬死了,上游服务的本地数据库操作都成功了,结果没发个消息给可靠消息服务,什么鬼!那就不会通知下游服务了,直接数据不一致

问题就大了,也就是说,上游服务通过spring cloud feign调用可靠消息服务,通知确认/删除消息的时候,服务调用失败,异常,因为此时会导致可靠消息服务的数据库里,留着一条状态为“待确认”的消息

(5)可靠消息服务将这个消息的状态修改为“已发送”,并且将消息发送到MQ中间件里去,这个环节是必须包裹在一个事务里的

如果发送MQ失败报错,那么可靠消息服务更新本地数据库里的消息状态为“已发送”的操作也必须回滚,反之如果本地数据库里的消息状态为“已发送”,那么必须成功投递消息到MQ里去

如果这个环节失败了,更加尴尬了,上游服务的本地数据库操作成功了,而且也成功的通知了可靠消息服务了,结果可靠消息服务将消息发送到MQ里去居然失败了,那还是没能成功把消息发出去啊,兄弟!

数据不一致了

分成两种情况来说,如果上游服务操作本地数据库失败了,会通知可靠消息服务去删除消息,此时可靠消息服务删除消息的操作失败了,会导致有问题,有一个消息“待确认”始终停留在可靠消息服务的数据库里

如果上游服务操作本地数据库成功了,会通知可靠消息服务去确认和投递消息,但是确认消息+投递消息(绑定在一起),现在一起失败了,消息还是“待确认”的状态,而且没有投递到MQ里去,此时也是不对的

(6)下游服务从MQ里监听到一条消息

下游服务如果压根儿不知道为啥没有从MQ里消费到消息,那没关系,现在的MQ都很强大的,后面我们给大家讲解,这个MQ如何可靠的投递消息。如果你没消费成功,人家MQ一定会确认给你重试投递的

因为你是手动ack的,只要MQ没有收到ack的通知,会重新投递消息的

(7)下游服务根据消息,在自己本地操作数据库

如果下游服务操作自己本地数据库失败了,那么本地事务就会回滚咯,然后就不会发送ack给MQ,那么MQ会继续不断的重试的,所以也没关系的,这是现在的MQ中间件都支持的功能

(8)下游服务对本地数据库操作完成之后,对MQ进行ack操作,确认这个消息处理成功

如果下游服务处理完了本地操作,给MQ发送ack的时候失败了,这个。。。你就得考虑下怎么处理了,其实我们可以考虑将下游服务的本地数据库操作和MQ的ack操作包裹在一个事务里,这样的话呢,如果MQ ack操作报错了,本地事务直接回滚

这样的话,MQ后续会再次重发消息的,所以没关系

rabbitmq也好,kafka也好,都是这样子,只要你关闭了自动ack以后,你迟迟的没有进行手动ack,人家会进行消息的重发,成功的手动ack之后,然后才会让mq将这条消息给删除掉不再重新投递

kafka,手动提交offset,关闭自动提交offset,变成手动提交offset rabbimq,关闭自动ack,手动进行ack,就可以了

就算不用rabbitmq手动ack重新投递消息的机制,只要依靠可靠消息服务的完善,我们其实也可以把这块做的很完善,可靠消息服务本身最重要的一点就是去重新投递消息

(9)下游服务对MQ进行ack之后,再给可靠消息服务发送个请求,通知该服务说,ok,我这里处理完毕了,可靠消息服务收到通知之后,将消息的状态修改为“已完成”

如果人家下游服务都ok了,MQ也ack了,结果下游服务发送给可靠消息服务通知,以及修改消息状态为“已完成”的时候,居然出错了,那么就麻烦了,无论是怎么回事,总之在可靠消息服务的数据库里,那个消息的状态是“已发送”,但是不是“已完成”!

这样整个系统状态是有问题的!

可靠消息服务的数据库里的消息的状态一直是“已发送”,所以这个状态是不对的

 

 

 

最大努力通知        有些操作无关紧要,可以成功也可以失败,而且还比较耗时

 

(1)失败了之后,按照一定的间隔,连续重试指定的次数,比如失败之后,每隔5分钟重试一次,连续重试10次

(2)失败了之后,每次间隔都增加个5分钟,连续重试5次,举个例子啊,第一次重试是5分钟以后,第二次是10分钟以后,第三次是15分钟以后,第四次是20分钟以后,第五次是25分钟以后

为啥要支持上游服务自己在发送消息的时候定义重试规则呢?因为很简单啊,不同的业务场景可能重试的要求不同,得由上游服务来定义,有些人觉得应该这样,有些人觉得应该那样,那你自己定义不就得了

最大努力通知服务,如果一次请求没成功,那么就将消息存到数据库里去,然后记录下来他的重试规则,以及上一次重试的时间,是第几次重试,然后搞一个后台线程不停的扫描,每次扫出来就根据规则去重新调用下游服务

如果超出了规则范围之外还是不行,就状态标记为:失败,就ok了

 

 

 

 

 

Seata AT 集成

 

1.第一次集成的是 eureka 注册中心 ,配置是file的方式

 

2.低版本seata 需要自己注入数据源bean(创建一个datasourceconfiguration 的 bean 然后在启动类的注入bean,手动排除自动默认的数据源)

高版本就不用了 直接开启seata注解即可开启数据源自动代理功能

 

3.注册中心:

 

 

seata 的注册中心默认 file, 为了高可用(seata server 集群) 会用第三方,seata client 会通过注册中心去获取server

 

 

4.配置中心:

配置中心不影响高可用,但是影响动态配置和服务可用检测,不具备集群内服务的健康检查机制当tc(seata-server)不可用时无法自动剔除列表

5.模式比对

 

AT 模式是无侵入的分布式事务解决方案,适用于不希望对业务进行改造的场景

 

TCC 模式是高性能分布式事务解决方案,适用于核心系统等对性能有很高要求的场景。

 

Saga 模式是长事务解决方案,适用于业务流程长且需要保证事务最终一致性的业务系统,Saga 模式一阶段就会提交本地事务,无锁,长流程情况下可以保证性能,多用于渠道层、集成层业务系统。事务参与者可能是其它公司的服务或者是遗留系统的服务,无法进行改造和提供 TCC 要求的接口,也可以使用 Saga 模式。

 

 

XA模式是分布式强一致性的解决方案,但性能低而使用较少。

xa 的效率低:xa协议提交事务是在rm侧,一阶段 执行sql后并不会提交,需要等到tc 的commit指令才会提交(所有的事务才会提交)也就是在二阶段才提交

at为什么效率高:因为at是异步提交(和别的事务不是同时提交),在一阶段是就提交了(如果失败 基于undo log 回滚),锁都是放在tc侧

 

 

 

集成流程:

1.修改seata server registry.conf type(注册中心和配置中心 配置)    注册中心:nacos eureka zk redis  配置中心 : nacos zk apollo file

2.修改seata server file.conf mode="db"(日志持久化)

3.seata\conf 打开README文档 获取 db 脚本 执行sql(建表)(db模式存储事务日志需要建表)  三种:file,db,redis

4.修改事务组配置:vgroup_mapping   client 和 server 要一致 不然事务会异常

6.执行client脚本 (每个业务库中,还要创建undo_log表 事务依赖)

7.运行nacos脚本 seata-server 需要的配置提交到 nacos上面

原理:

1.本地生成undo log ,执行事务,不提交,向TC注册(全局锁 数据 分支事务id 全局事务id 对应的数据主键) 并获取全局锁(对应主键id的全局锁),再提交

2.后面的事务会等前面事务释放全局锁,然后获取到全局锁在提交

3.(一阶段RM成功会发送commit给TC:2pc协议 流程)tc 收到 所有事务的 commit请求后 开始发送二节段的 commit请求给RM ,RM 把请求放异步任务队列,马上返回提交成功结果给tc

4.将异步任务队列的请求批量执行 去删除udlog

 

 

 

 

 

 

遇到的问题:

sleuth + zipkin 

zipkin(collector 收集器组件:收集请求跟踪信息 生成调用链,转成内部信息格式,storage 存储组件 收集器收集到的信息 存储到redis mysql es, web后台监控)

zipkin client端(集成在服务中)收集数据 通过mq 或者http 发送给server端 用来后台展示监控

1.这两个类中都有创建 Bean:feignHystrixBuilder

奇怪的是当时就一个it manage 会异常,其他项目不会。后来排查包冲突找到 sleuth 和 Seata 冲突

sleuth 和 Seata 冲突,两个包的一个类都会注入相同名称的bean。(当时一个同事正在弄链路追踪引入了这个包)只能留一个的话 就会导致另外一个框架功能用不了

seata是用这个类来传输事务全局id,最后通过拦截器来传输xid,然后用了springboot的注解在启动类那边排除了冲突的类。

seata是自己封装了一个feignhystrixbuilder 这个类中会把txid保存在请求的header中,SetSeataInterceptor 新建了一个拦截器 把 当前的事务id 放在request的header中。 在request中传递事务id。

 

2.Could not register branch into global session xid

一开始以为是自己的拦截器获取全局事务id 没有获取正确,导致分支提交时 主分支找不到全局事务,导致最后tc 主动异步提交了。

解决:跟踪发现分支和主分支的全局id确实一样。

后来怀疑是请求重试 分支的请求重复响应,第二次响应会找不到全局事务因为事务已经结束。

通过分析排除这个原因,因为 错误异常的 是主分支找不到事务,所以不是分支的原因

确定了主分支后, 有可能是事务超时,自动异步提交了。查看时间好像也不会,seata默认超时时间60s 也排除

最后确定的是 事务嵌套了, seata的事务被本地事务嵌套,seata不允许这样做。。。

看了官方faq:允许嵌套,但是是seata注解在外层或者是同级

 

3.事物组 seate server 没有配置 找不到事物

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值