分布式事务
分布式协议
- XA规范
XA(eXtended Architecture)标准是X/Open 组织针对分布式事务(DTP)处理的规范,它描述了全局事务管理器和本地资源管理器之间的接口,允许多个资源在同一分布式事务中访问。DTP 模型中包含一个全局事务管理器(TM,Transaction Manager)和多个资源管理器(RM,Resource Manager)。全局事务管理器负责管理全局事务状态与参与的资源,协同资源一起提交或回滚;资源管理器则负责具体的资源操作。
AP定义事务边界以及那些组成事务的特定于应用程序的操作
分布式事务方案
2PC(强一致性)
2PC即两阶段提交协议(Two Phase Commit),是对XA规范的实现,从字面意思理解就是将提交分为两个阶段:
- 准备阶段
TM通知各个RM准备提交它们的事务分支,如果RM本地事务执行成功,则返回成功;如果RM的本地事务执行失败,则返回失败。 - 提交阶段
如果TM收到所有分支事务返回成功的消息,则向每个RM发送提交消息;否则发送回滚消息。RM根据TM的指令执行提交或者回滚本地事务操作,释放本地事务处理过程所用的所有资源。
以创建订单业务场景为例分析2PC存在的问题
- 锁资源无法释放
当订单服务操作订单库和调用库存服务后,在准备阶段执行完成后,TM宕机了,此时数据库资源被占用,导致RM一直阻塞,无法释放锁资源 - 数据不一致
在提交阶段,TM给所有RM发送提交请求,如果某些RM由于网络抖动或者请求过程中协调者发生故障,只有部分RM收到了commit请求,接收到commit请求的RM会正常提交分支事务,没有收到commit请求的RM则无法执行事务提交操作,最后导致数据不一致
JTA/XA规范实现
以MySQL为例(MySQL实现了XA规范),模拟两个分支事务组成的分布式事务,伪代码:
- 创建两个资源管理器操作接口,分别代表两个分支事务,他们都拥有各自的资源。具体为XAConnection和XAResource。
- AP向TM请求创建一个分布式事务表示开始事务,并得到全局事务id。
- 分别执行两个分支事务
- 准备阶段:TM通过XAResource.prepare()询问两个分支事务,是否可以提交
- 提交阶段:如果两个分支事务的prepare()都返回0(成功)则通知各个RM提交事务,否则回滚。
seata AT模式
-
seata三大角色:
- TC - Transaction Coordinator
维护全局和分支事务的状态,驱动全局事务的提交或回滚 - TM - Transaction Manager
定义事务的边界:开启一个全局事务,提交或回滚一个全局事务 - RM - Resource Manager
管理分支事务的操作资源,向TC注册分支事务和报告分支事务的状态,驱动分支事务的提交或回滚
- TC - Transaction Coordinator
-
AT模式流程图:
TCC(补偿事务/柔性事务)
TCC即Try-Confirm-Cancel
- Try:完成所有业务检查,预留必须的业务资源。注意判断:
a.幂等判断:通过查询日志记录,判断本次分布式事务Try逻辑是否已经有执行记录
b.悬挂处理:通过查询日志记录,判断本次分布式事务Confirm或者Concel是否又执行记录 - Confirm:真正执行的业务逻辑,不做任何业务处理,只是用Try阶段预留的资源。因此,Try能执行成功则Confirm必须能成功。注意Confirm操作的幂等判断
- Cancel:释放Try阶段预留的业务资源。同样Cancel操作也需要幂等判断
空回滚:如果Try操作没有执行,则Cancel操作也不能执行。
TCC分布式事务流程:
- 主业务服务开启本地事务
- 主业务流程获取分布式事务唯一id
- 主业务服务发起所有从业务服务的Try调用
- 当所有从业务服务的Try接口调用成功,主业务服务提交本地事务;若有失败,则本地事务回滚。
- 若主业务服务提交本地事务,则TCC模型分别调用所有从业务服务的Confirm接口;若主业务服务回滚本地事务,则调用所有从业务服务的Cancel接口
TCC开源框架
- Tcc-Transaction
- Hmily
- Seata TCC
- …
TCC demo
以转账为例,设计一个转账接口,假设A账户转账10元给B账户:
- A账户接口伪代码
try:
判断A账户余额是否大于10
减少10元
调用B账户增余额接口
Confirm:
Cancel:
增加10元
- B账户接口伪代码
try:
增加10元
Confirm:
Cancel:
减少10元
咋一看似乎逻辑很严谨没有什么问题,但是这里有很多的问题:
1.A账户Try操作没有做幂等判断和悬空判断
2.A账户Cancel操作没有幂等判断和空回滚处理
3.B账户Try操作增加10元,放在Confirm操作处理更简单,并且加上Confirm幂等处理
4.B账户Cancel操作为空
较为合理的伪代码:
- A账户接口伪代码
try:
幂等判断(判断Try操作本次事务是否已经执行)
悬挂处理(判断本次事务的Confirm或者Cancel操作是否已经执行过,如已经执行过就不能执行Try操作)
判断A账户余额是否大于10
减少10元
调用B账户增余额接口
Confirm:
空
Cancel:
幂等校验
判断Try操作是否执行过,如没有执行就不允许Cancel操作执行
增加10元
- B账户接口伪代码
try:
空
Confirm:
幂等校验
增加10元
Cancel:
空
小结
允许空回滚:由于网络丢包导致Try操作失败,必会触发Cancel操作,此时Cancel必须识别出这是空回滚,返回成功。
防悬挂:由于网络堵塞丢包,Try操作超时,此时分布式事务回滚触发Cancel操作,故出现Cancel比Try操作先执行。
幂等控制:Try/Confirm/Cancel都必须做幂等判断,防止重复执行。
可靠消息
本地消息表
- 用户注册
用户发送注册请求,user_service会将用户注册和赠送优惠券作为本地事务同时入库,保证原子性和一致性 - 定时任务
定时任务扫描本地消息日志表,将优惠券消息发送到mq,并在消息中间返回成功后删除本地日志表,否则等到下一次重试。 - 消息消费
优惠券服务监听mq消息,在接收到消息并处理成功后,使用mq的应答机制给mq发送ACK确认。此外定要保证消息消费的幂等性。
可能会出现的问题:
user_service发送消息给mq时,可能会因为网络堵塞、网络丢包或者mq发送ACK失败导致user_service没有收到ACK确认重复发送消息;mq投递消息给消费者时同样没有收到ACK确认消息重复推送。以上都会导致消息重复消费,故需做幂等判断,通过消息id判断是否已经消费过,如果已经消费则丢弃。
Rocketmq事务消息最终一致性
1.生产者将半消息发送个Rocketmq服务
2.Rocketmq服务收到半消息后,给生产者响应半消息发送成功;但此时的半消息只是保存起来不会被消费者消费
3.生产者收到成功后,执行本地事务
4.生产者根据本地事务执行结果向Rocketmq提交二次确认结果(提交或回滚)
- 提交:本地事务执行成功,提交二次确认结果,此时Rocketmq服务将半事务消息的"暂不能投递"状态改为"可投递"状态,并投递给消费者。
- 回滚:本地事务执行失败,回滚二次确认结果,将半事务消息改为不投递状态。
5.Rocketmq自动执行回查事务状态
6.生产者收到回查消息后,去检查本地事务是否执行成功
7.再次提交二次确认结果,参照4.
伪代码:
- 生产者
//1.发送事务消息
public class BusiService {
public void sendMsg() {
message = MessageBuilder.withPayload(object.toJSONString()).build();
rocketMQTemplate.sendMessageInTransaction(message);
}
}
//监听器
@RocketMQTransactionListener
public class A implements RocketMQLocalTransactionListener {
//3.执行本地事务:使用Rocketmq注解监听等待Rocketmq回调
@Override
@Transactional
public RocketMQLocalTransactionState executeLocalTransaction(Message message, Object o) {
try {
//(1)幂等判断
if (事务日志id是否存在) {return;}
//(2)执行业务
...
//(3)添加事务日志
事务日志入库
//4.提交二次确认
return RocketMQLocalTransactionState.COMMIT;
} catch (Exception e) {
return RocketMQLocalTransactionState.ROLLBACK;
}
}
//6.检查本地事务执行状态
@Override
public RocketMQLocalTransactionState checkLocalTransaction(Message message) {
if (检查本地事务是否已执行) {
return RocketMQLocalTransactionState.COMMIT;
} else {
return RocketMQLocalTransactionState.ROLLBACK;
}
}
}
- 消费者
@RocketMQMessageListener(consumerGroup = "xxx",topic = "xxx")
public class BConsumer implements RocketMQListener<String> {
@Override
public void onMessage(String message) {
//幂等判断
if (事务日志id是否存在) {return;}
//执行业务
BService.xxx();
//添加事务日志
}
}
最大努力通知
最大努力通知是柔性事务,最大努力通知的使用业务场景适用于对最终一致性时间敏感度比较低的业务。如:银行通知,商户通知。
最大努力通知型的实现方案,一般符合以下特点:
1、不可靠消息:业务活动主动方,在完成业务处理之后,向业务活动的被动方发送消息,直到通知N次后不再通知,允许消息丢失(不可靠消息)。
2、定期校对:业务活动的被动方,根据定时策略,向业务活动主动方查询(主动方提供查询接口),恢复丢失的业务
消息。
使用场景:
- 短信平台
- 充值业务