一致性类型
强一致性
引入一个协调者,通过协调者来协调所有参与者来进行提交或者回滚
基于XA规范的二阶段及三阶段提交
支持2阶段提交的第三方框架,如Seata
TCC也是一种强一致性的方案
最终一致性
基于可靠消息的最终一致性(本地消息表、事务消息)
- 借助支持事务消息的中间件,通过发送事务消息的方式来保证最终一致性
- 一般来说,事务消息和本地消息表比较适合于对一致性要求没那么高,不要求强一致,但是也不能丢的一些场景,比如用户下单后给用户增加积分。
最大努力通知
- 最大努力通知不需要保证消费者一定能接收到,只是尽自己最大的努力去通知就行了。最多就是在发消息的地方加一个重试的机制
- 只适合用在消息丢了也无所谓的场景。比如说下单后邮件通知、开通后发送欢迎短信之类的业务场景。
借助Seata等分布式事务框架实现
弱一致性
柔性事务
- 柔性事务,是业内解决分布式事务的主要方案。所谓柔性事务,相比较与数据库事务中的ACID这种刚性事务来说,柔性事务保证的是“基本可用,最终一致。”这其实就是基于BASE理论,保证数据的最终一致性。
- 最主要的有以下三种类型:异步确保型、补偿型、最大努力通知型。
选择依据
实现成本
根据项目开发和维护的难度、成本等方面来选择合适的分布式事务方案。这几种方案中,TCC和2PC的实现成本最高,业务侵入性也比较大
一致性要求
2PC和TCC属于是可以保证强一致性的,而其他的几种方案是最终一致性的方案。
- 根据业务情况,比如下单环节中,库存扣减和订单创建可以用强一致性来保证。而订单创建和积分扣减就可以用最终一致性即可。而对于一些非核心链路的操作,如核对等,可以用最大努力通知即可。
可用性要求
根据CAP理论,可用性和一致性是没办法同时保证的,所以对于需要保证高可用的业务,建议使用最大努力通知等最终一致性方案;对于可用性要求不高,但是需要保证高一致性的业务,可使用2PC等方案。
数据规模
消息中间方案不适合业务量特别大的场景,有可能出现消息堆积导致一致性保障不及时。对于数据量大的场景,可以考虑Seata方案
二阶段提交与三阶段提交
XA规范
X/Open 组织(即现在的 Open Group )定义了分布式事务处理模型。 模型中主要包括应用程序( AP )、事务管理器( TM )、资源管理器( RM )、通信资源管理器( CRM )等四个角色。
- 常见的事务管理器( TM )是交易中间件
- 见的资源管理器( RM )是数据库
- 常见的通信资源管理器( CRM )是消息中间件
全局事务,是指分布式事务处理环境中,一个操作涉及多个数据库,每个数据库为一个本地事物,所有的本地事物全部成功才最终成功执行提交,否则全部回滚
二阶段提交
两个阶段是指:第一阶段:准备阶段(投票阶段)和第二阶段:提交阶段(执行阶段)
- 准备阶段(Prepare Phase)
- 协调者向所有参与者发送准备(Prepare)消息。参与者收到消息后,执行事务操作并将结果保存在临时存储中。然后,参与者回复协调者一个表示事务操作是否成功的“同意”(YES)或“放弃”(NO)消息
- 提交阶段(Commit Phase)
- 协调者根据参与者的回复来决定事务是否可以提交。如果所有参与者都回复了“同意”消息,协调者向参与者发送提交(Commit)消息。参与者收到消息后,将事务操作的结果持久化,并释放锁定的资源。如果有任何参与者回复了“回滚”消息,协调者向参与者发送回滚(Rollback)消息。参与者收到消息后,回滚事务操作并释放锁定的资源
问题
- 同步阻塞问题
- 二阶段提交协议的第一阶段准备阶段执行事务操并没有进行commit还是rollback。事务执行之后,在没有执行commit或者rollback之前,资源是被锁定的。这会造成阻塞
- 单点故障
- 由于协调者的重要性,一旦协调者发生故障。参与者会一直阻塞下去。尤其在第二阶段,协调者发生故障,那么所有的参与者还都处于锁定事务资源的状态中,而无法继续完成事务操作。(如果是协调者挂掉,可以重新选举一个协调者,但是无法解决因为协调者宕机导致的参与者处于阻塞状态的问题)
- 数据不一致
- 假设协调者发出了事务 Commit 的通知,但是由于网络问题该通知仅被一部分参与者所收到并执行 Commit,其余的参与者没有收到通知,一直处于阻塞状态,那么,这段时间就产生了数据的不一致性。
- 超时机制
- 对于协调者来说如果在指定时间内没有收到所有参与者的应答,则可以自动退出 WAIT 状态,并向所有参与者发送 rollback 通知。对于参与者来说如果位于 READY 状态,但是在指定时间内没有收到协调者的第二阶段通知,则不能武断地执行 rollback 操作,因为协调者可能发送的是 commit 通知,这个时候执行 rollback 就会导致数据不一致。
- 互询机制
- 让参与者 A 去询问其他参与者 B 的执行情况。如果 B 执行了 rollback 或 commit 操作,则 A 可以大胆的与 B 执行相同的操作;如果 B 此时还没有到达 READY 状态,则可以推断出协调者发出的肯定是 rollback 通知;如果 B 同样位于 READY 状态,则 A 可以继续询问另外的参与者。只有当所有的参与者都位于 READY 状态时,此时两阶段提交协议无法处理,将陷入长时间的阻塞状态。
- 参与者执行commit后挂了协调者不知道啥情况,超时回滚导致数据不一致
三阶段提交
所以3PC把2PC的准备阶段再次一分为二,这样三阶段提交就有CanCommit、PreCommit、DoCommit三个阶段。
- 为什么要把投票阶段一分为二?
假设有1个协调者,9个参与者。其中有一个参与者不具备执行该事务的能力。
协调者发出prepare消息之后,其余参与者都将资源锁住,执行事务。
协调者收到响应之后,发现有一个参与者不能参与。所以,又出一个roolback消息。其余8个参与者,又对消息进行回滚。这样子,是不是做了很多无用功?
所以,引入can-Commit阶段,主要是为了在预执行之前,保证所有参与者都具备可执行条件,从而减少资源浪费。
问题
- 如果参与者接收到PreCommit消息后,与协调者无法正常通信,参与者仍然会进行事务的提交,协调者超时认为失败回滚,则会导致数据不一致性。
改进
- 3PC引入了超时机制,并在协调者和参与者中都使用超时机制,在2PC中,只有协调者拥有超时机制
- 在doCommit阶段,如果参与者无法及时接收到来自协调者的doCommit或者rebort请求时,会在等待超时之后,会继续进行事务的提交
- 基于概率考虑,因为前面已经同意且具备提交,认为要执行提交操作,可以解决网络问题收不到提交命令导致的不一致问题
- 执行了提交后无法
TCC
TCC是Try-Confirm-Cancel的缩写,它是一种分布式事务解决方案,采用了基于业务逻辑的补偿机制,将整个分布式事务分解为若干个子事务,每个子事务都有一个try、confirm和cancel三个操作,通过这些操作来实现分布式事务的执行和回滚
Try
- 在try阶段,参与者尝试执行本地事务,并对全局事务预留资源。如果try阶段执行成功,参与者会返回一个成功标识,否则会返回一个失败标识。
Confirm
- 如果所有参与者的try阶段都执行成功,则协调者通知所有参与者提交事务,那么就要执行confirm阶段,这时候参与者将在本地提交事务,并释放全局事务的资源。
Cancel
- 如果任何一个参与者在try阶段执行失败,则协调者通知所有参与者回滚事务。那么就要执行cancel阶段,还有就是,如果某个参与者在try阶段执行成功,但是在confirm阶段执行失败,则需要执行cancel操作,将之前预留的资源释放掉。
举例
假设有一个转账服务,需要从A账户中转移到B账户中100元、C账户中200元
- Try阶段
- 转账服务首先尝试将A账户的金额冻结300元
- Confirm阶段
- 如果所有的try操作都执行成功,转账服务将尝试执行解冻并转账,将金额转到B账户和C账户中。
- Cancel阶段
- 如果try过程中,某个转账事务执行失败。那么将执行解冻,将300元解冻。如果在confirm过程中,A->C的转账成功,但是A->B的转账失败,则再操作一次C->A的转账,将钱退回去。
优缺分析
优点
- 灵活性
- TCC适用于不同类型的业务场景,例如账户转账、库存扣减等,能够根据业务逻辑实现精细的事务控制。
- 高可用性
- TCC使用分布式锁来保证分布式事务的一致性,即使其中一个节点出现故障,也不会影响整个系统的运行。
- 可扩展性
- TCC采用分阶段提交的方式,支持横向扩展,可以适应更多的并发访问和业务场景。
- 性能
- TCC相对于2PC来说,具有更好的性能表现
缺点
- 实现复杂
- TCC需要实现Try、Confirm和Cancel三个操作,每个操作都需要实现正确的业务逻辑和补偿机制,代码实现比较复杂。
- 存在悬挂事务问题
- 在调用TCC服务的一阶段Try操作时,可能会出现因网络拥堵而导致的超时,此时事务协调器会触发二阶段回滚,调用TCC服务的Cancel操作;在此之后,拥堵在网络上的一阶段Try数据包被TCC服务收到,出现了二阶段Cancel请求比一阶段Try请求先执行的情况。举一个比较常见的具体场景:一次分布式事务,先发生了Try,但是因为有的节点失败,又发生了Cancel,而下游的某个节点因为网络延迟导致先接到了Cancel,在空回滚完成后,又接到了Try的请求,然后执行了,这就会导致这个节点的Try占用的资源无法释放,也没人会再来处理了,就会导致了事务悬挂。
- 空回滚问题
- TCC中的Try过程中,有的参与者成功了,有的参与者失败了,这时候就需要所有参与者都执行Cancel,这时候,对于那些没有Try成功的参与者来说,本次回滚就是一次空回滚。需要在业务中做好对空回滚的识别和处理,否则就会出现异常报错的情况,甚至可能导致Cancel一直失败,最终导致整个分布式事务失败。
- 业务代码侵入性
- TCC需要将事务操作拆分为Try、Confirm和Cancel三个步骤,对业务代码有一定的侵入性,需要针对不同的业务场景进行实现。
实际情况
在实际的应用过程中,很多公司用的TCC并不是完全严格的按照上述的过程的。有的时候是这么用的:Try的时候预占用资源,如果成功了,就执行Confirm,失败了就执行Cancel,但是如果Confirm失败了或者Cancel失败了,则进行重试,直至成功。
主要是因为在Try的过程中已经锁定了资源,那么在Confirm的时候,大概率是可以成功,而如果Confirm失败就执行Cancel,就会导致可能只是因为网络原因导致的时候就使得整个事务都Cancel了,而且这时候如果Cancel再失败怎么办呢?整个方案就会变得更加复杂了。
对比2pc
二者的实现机制不同
- 2PC使用协调者和参与者的方式来实现分布式事务,而TCC采用分阶段提交的方式。
处理方式不同
- 2PC采用预写式日志的方式,在提交和回滚阶段需要协调者和参与者之间进行多次网络通信,整个事务处理过程较为复杂。TCC则只需要在Try、Confirm和Cancel阶段执行相应的业务逻辑。
异常处理不同
- 2PC需要处理网络、节点故障等异常情况,可能会导致整个事务无法提交或回滚,处理异常情况的复杂度较高。而TCC只需要处理业务异常情况,异常处理相对简单。
适用场景不同
- 2PC适用于对事务一致性要求较高的场景,例如银行转账等,需要保证数据一致性和完整性。而TCC适用于对事务一致性要求不那么高的场景,例如电商库存扣减等,需要保证数据最终一致性即可。
Cancel失败了怎么办?
记录日志&发送报警
- 将错误信息记录下来,方便后续分析和处理。并及时通知相关人员进行处理。
自动重试
- 在一定程度上,可以通过自动重试的方式尝试多次执行Cancel操作,直到成功为止。
人工干预
- 如果重试多次还是不成功, 可以报警,然后进行人工干预,可以尝试手动执行Cancel操作或者进行数据修复等。
本地消息实现分布式事物
一般在发送消息之前先创建一条本地消息,并且保证写本地业务数据的操作和写本地消息记录的操作在同一个事务中。这样就能确保业务数据和消息数据一致。
然后再基于本地消息,调用MQ发送远程消息。
消息发出去之后,等待消费者消费,在消费者端,接收到消息之后,做业务处理,处理成功后再修改本地消息表的状态。
失败处理
1、2如果失败,因为在同一个事务中,所以事务会回滚,3及以后的步骤都不会执行。数据是一致的。
3如果失败,那么就需要有一个定时任务,不断的扫描本地消息数据,对于未成功的消息进行重新投递。
4、5如果失败,则依靠消息的重投机制,不断地重试。
6、7如果失败,那么就相当于两个分布式系统中的业务数据已经一致了,但是本地消息表的状态还是错的。这种情况也可以借助定时任务继续重投消息,让下游幂等消费再重新更改消息状态,或者本系统也可以通过定时任务去查询下游系统的状态,如果已经成功了,则直接推进消息状态即可。
优缺点
优点
- 可靠性高
- 基于本地消息表实现分布式事务,可以将本地消息的持久化和本地业务逻辑操作,放到一个事务中执行进行原子性的提交,从而保证了消息的可靠性。
- 可扩展性好
- 基于本地消息表实现分布式事务,可以将消息的发送和本地事务的执行分开处理,从而提高了系统的可扩展性。
- 适用范围广
- 基于本地消息表实现分布式事务,可以适用于多种不同的业务场景,可以满足不同业务场景下的需求。
缺点
- 实现复杂度高
- 基于本地消息表实现分布式事务,需要设计复杂的事务协议和消息发送机制,并且需要进行相应的异常处理和补偿操作,因此实现复杂度较高。
- 系统性能受限
- 基于本地消息表实现分布式事务,需要将消息写入本地消息表,并且需要定时扫描本地消息表进行消息发送,因此对系统性能有一定影响。
- 会带来消息堆积扫表慢、集中式扫表会影响正常业务、定时扫表存在延迟问题等问题。
- 解决
- 消息堆积,扫表慢
- 加索引
- 在state字段上增加一个索引,虽然这个字段的区分度不高,但是一般来说,这张表中,SUCCESS的数据量占90%,而INIT的数据量只占10%,而我们扫表的时候只关心INIT即可,所以增加索引后,扫表的效率是可以大大提升的。
- 区分度不高的字段增加对于小比例的数据还是能走索引提升效率的,比如男女比例95:5,筛选女性能走索引,而男性筛选则可能走全表扫描
- 多线程并发扫表
- 下游消息要做好业务幂等
- 数据分段防止重复扫描,每个线程扫描的数据分段不同
- 数据不连续可以采用连续的业务id分片
- 集中式扫表会影响正常业务
- 不扫主库,而是扫描备库
- 数据库做集群分摊压力
- 定时扫表存在延迟问题
- 考虑延迟消息,基于延迟消息来做定时执行
Seata解析
Seata是一个阿里开源的分布式事务解决方案,目前支持4种模式,分别是:AT模式、TCC模式、Saga模式、XA模式
开发者认为一个分布式事务是有若干个本地事务组成的。所以他们给Seata体系的所有组件定义成了三种,分别是Transaction Coordinator(TC)、Transaction Manager(TM)和Resource Manager(RM)
TC
- 这是一个独立的服务,是一个独立的 JVM 进程,里面不包含任何业务代码,它的主要职责:维护着整个事务的全局状态,负责通知 RM 执行回滚或提交;
TM
- 在微服务架构中可对应为聚合服务,即将不同的微服务组合起来成一个完成的业务流程,TM 的职责是开启一个全局事务或者提交或回滚一个全局事务;
RM
- RM 在微服务框架中对应具体的某个微服务为事务的分支,RM 的职责是:执行每个事务分支的操作。
举例
大致流程(不同模式流程有差别)
1、TM在接收到用户的下单请求后,会先调用TC创建一个全局事务,并且从TC获取到他生成的XID。
2、TM开始通过RPC/Restful调用各个RM,调用过程中需要把XID同时传递过去。
3、RM通过其接收到的XID,将其所管理的资源且被该调用锁使用到的资源注册为一个事务分支(Branch Transaction)
4、当该请求的调用链全部结束时,TM根据本次调用是否有失败的情况,如果所有调用都成功,则决议Commit,如果有超时或者失败,则决议Rollback。
5、TM将事务的决议结果通知TC,TC将协调所有RM进行事务的二阶段动作,该回滚回滚,该提交提交。
RocketMQ事务消息
发送半消息
应用程序向RocketMQ Broker发送一条半消息,该消息在Broker端的事务消息日志中被标记为“prepared”状态。
执行本地事务
RocketMQ会通知应用程序执行本地事务。如果本地事务执行成功,应用程序通知RocketMQ Broker提交该事务消息。
- broker未收到状态消息会反向发送检查请求,检查请求也没响应则消息被标记为“UNKNOW”状态,后续服务端有了响应还可以继续发送Commit或Rollback,如果超过过期时间则回滚