分布式事务基于消息中间件实现最终一致性

分布式事务基于消息中间件实现最终一致性



举个例子:不同服务不同库的情况下,当我们在a服务的业务表中扣除100元,又要在b服务的业务表中增加100元。解决分布式事务的方案有各种,如果只需要最终一致性,且项目中有使用消息中间件,那么可以借助消息中间件解决

首先画张图来梳理下步骤:

在这里插入图片描述
我们对图上几个关键步骤进行说明:

步骤1:从db1的业务表中扣减100元,然后将相关业务信息存到对应的业务消息表中备份(消息表中需要有一个消息状态的字段,初始含义为未被服务B成功消费),业务扣钱和消息表备份的操作需要在一个事务中。
为什么服务A需要一个消息表做备份呢?
第一我们将扣减信息发送至mq,由于网络,mq都有可能出现异常,导致服务B并不一定能正常接收到服务A发送的加钱的消息,则无法在db2的业务表增加100元。
第二服务B在接收到消息之后,由于数据库异常或者代码bug,也无法保证相应的业务代码一定能运行成功。
如果进行了消息备份,则服务A可以对消息进行重发。


步骤4,5,6:服务B首先去消息表查询是否有消费过消息。如果没有消费过,则在db2的业务表增加100元,并将消费记录在消息表中(业务增加钱和在消息表记录消息要在一个事务中),将充值成功的结果发送给服务A。如果消息表中有记录该消息,则说明已经消费过,则直接将充值成功的结果发送给服务A。
为什么服务B又需要一个消息表记录呢?
第一:假设服务B第一次接受到服务A发送过来的消息,并且加钱成功了,但是由于网络,mq都有可能出现异常,导致服务A不能正常接收到服务B加钱成功的消息,则服务A并不会去更新自己消息表的状态为已经被B成功消费过,那么服务A就会重发需要加钱的消息给服务B,导致服务B再次加钱。
第二:即使服务A接收到服务B加钱成功的消息,由于数据库异常或者代码bug,也无法保证将服务A消息表的消息状态字段含义更新已经被服务B成功消费了的操作一定能运行成功。导致服务A重发消息给服务B,务B再次加钱。


由于服务B并不一定能成功消费服务A的消息,所以服务A需要有一个定时任务,从服务A的消息表中查询出还没被服务B消费的消息,将该消息重新发送给服务B。
服务A还需要监听从服务B发送过来加钱成功的消息,将服务A消息表中的消息状态字段的含义更新为已经被服务B成功消费了。

上一段简短的代码,方便理解

try {
	transactionTemplate.execute((status) -> {
		//服务A扣款
		accountMapper.reduceAmount(amount, userId);
		//服务A记录消息进行备份
	    accountMessageMapper.insert(accountMessage);
		return null;
		});
	}catch (Exception e){
		log.error("扣钱失败",e);
		return;
	}

	//服务A将需要服务B加钱的消息发送给mq
	rocketMQTemplate.convertAndSend("accountRequestTopic",accountMessage);
String messageKey = message.getMessageKey();
if(!StringUtils.isNotBlank(messageKey)) {
	//查询是否已经充值过
	int i = accountMessageMapper.selectCountByMessageKey(messageKey);
	//未充值过,或者充值失败过
	if (i == 0) {
		transactionTemplate.execute((status) -> {
			//进行充值
			accountMapper.addAmount(message.getAmount(), message.getUserId());
			//对充值过的消息进行记录
			accountMessageMapper.insert(message);
			return null;
		});
	}
	else {
		log.error("重复消费{}", messageKey);
	}
	//发送充值成功的消息给服务A
	rocketMQTemplate.convertAndSend("accountResponseTopic",messageKey);
}
@Component
@Slf4j
@RocketMQMessageListener(consumerGroup = "accountGroup", topic = "accountResponseTopic")
public class AccountConsumer implements RocketMQListener<String> {

	@Autowired
	AccountMessageMapper accountMessageMapper;

	@Override
	public void onMessage(String messageKey) {
		//监听服务B充值成功的消息,将消息的状态更新为已经被服务B成功消费过
		accountMessageMapper.updateConfirmed(messageKey);
	}

}
@Component
public class AccountScheduled {

	@Autowired
	AccountMessageMapper accountMessageMapper;

	@Autowired
	RocketMQTemplate rocketMQTemplate;

	//根据实际情况填写表达式
	@Scheduled(cron = "0 */10 * * * ?")
	public void scheduled() {
		//查询未被服务B成功消费的消息
		List<AccountMessage> accountMessages = accountMessageMapper.selectUnconfirmedMessgae();
		for (AccountMessage accountMessage : accountMessages) {
			//消息进行重发
			rocketMQTemplate.convertAndSend("accountRequestTopic",accountMessage);
		}
	}
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值