分布式事务基于消息中间件实现最终一致性
举个例子:不同服务不同库的情况下,当我们在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);
}
}
}