这篇文章是通过学习哔哩哔哩中的视频“阿里架构师如何30分钟基于MQ解决分布式事务问题”,原视频连接https://www.bilibili.com/video/BV15p4y1D7d7?p=2
本人水平有限,如有误导,欢迎斧正,一起学习,共同进步!
这是一次简单的模拟分布式事务的:假设我们现在需要支付宝向余额宝转账100元。支付宝(系统A)向余额宝(系统B)转账100元,支付宝余额-100,余额宝余额加100。这涉及到两个系统,需要用到分布式事务。
系统A(支付宝系统)完成相关业务(此次是扣款操作)后,向MQ中发送消息,同时往系统A(支付宝系统)的消息表中插入一条消息,状态是未确认(为什么要往消息表插入数据后面会有解释);系统B(余额宝系统)从MQ中拿到消息,完成相应的业务(此次是加款操作)后,往系统B(余额宝系统)的消息表插入一条消息,并通知系统A(支付宝系统),当前消息已经被成功操作(我这加款成功啦,你可以放心啦),系统A(支付宝系统)监听到系统B(余额宝系统)发送来的消息后,修改系统A(支付宝系统)本地的消息表的这条消息的状态,改为已确认;利用定时任务去定时的扫描系统A的消息表,若是有状态是未确认的消息,则再次将该消息发送到MQ中,等待消息消费者的消费。
1、消息丢失:假设消息生产者(此处是系统A,支付宝系统)向MQ发送消息正常发送;消息消费者(此处是系统B,余额宝系统)正常监听到了MQ中的消息,但是由于某种原因,消息消费者未执行相应的业务逻辑(此处就是余额宝未成功+100,说极端点,比如我余额宝数据库直接宕机了,虽然可能性比较小,但是要考虑到这种情况),但是MQ认为消息消费者已经正常的消费了消息,就把消息删除了。这就是典型的消息丢失问题。
2、重复消费消息(保证消息的幂等性):假设消息生产者(系统A,支付宝系统)向MQ发送消息成功,消息消费者(系统B,余额宝系统)成功监听到了消息并成功消费(就是说余额宝成功+100了)。但是消息消费者向消息生产者发送通知的时候,由于网络原因或其他原因,发送失败(余额宝向支付宝说,我钱增加啦,你放心吧,这个步骤发送失败了。这一步也是通过MQ来实现通信的)。此时,由于系统A迟迟未收到系统B的确认消息,那么系统A的本地消息表的状态就一直是未确认,定时器定时扫描的时候,就会把这条消息继续发送到MQ中,这就会导致支付宝扣了一次款(-100),余额宝增加了两次款(+100,第二次又+100)。是不是做梦都要笑醒了~
1、解决第一个问题,就是在系统A中,增加一个消息表(这个消息表是系统A的),只要我相关业务执行成功了(支付宝-100成功了),那么就往这个消息表增加一条数据(消息表核心字段有ID,状态。这个ID类似于Dubbo的服务追踪的穿透ID,流水ID,是俩系统沟通的凭证,有点类似于token一样的,因为我需要通过这个ID来定位到某一个消息)。只有我监听到系统B给我的反馈了以后(系统B给消息A发送消息的时候带着这个消息的ID,可以定位到这个消息),我才将这个消息的状态改为“已消费”,否则就是默认的未消费。定时器会定时的扫描这个表,将状态是未消费的消息,重新发送到MQ中。
2、解决第二个问题,就是在系统B中,也增加一个消息表(这个消息表是系统B自己的),只要我成功的执行完了我的业务代码(余额宝+100成功)以后,我就在这个消息表增加一个消息。即使某种原因,系统A未收到系统B的确认消息,无非就是把同样的消息在重新发送到MQ中,但是系统B本地有已经消费过的消息存根,系统B监听到消息以后,先去系统B的消息表(消息本地存根)中根据监听到的消息ID查,若是能查到,则证明此条消息已经被消费过了,则不在消费,直接给系统A发送消息,说我成功消费了,你改这个消息的状态吧,别在给我发这条消息了。
我个人的github链接(仅供参考): https://github.com/zhengtianliang/distributed_transaction_rabbitmq.
1、系统A(支付宝系统)的核心业务代码
package com.alipay.service.impl;
import com.alipay.dao.AccountMapper;
import com.alipay.dao.AlipayMessageMapper;
import com.alipay.entity.Account;
import com.alipay.entity.AlipayMessage;
import com.alipay.entity.MessageStatus;
import com.alipay.rabbitmq.RabbitMqSender;
import com.alipay.service.OrderService;
import com.alipay.util.json.JsonUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionTemplate;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.UUID;
/**
* @author ZhengTianLiang
* @date 2020/9/12 22:00
* @desc service
*/
@Service
public class OrderServiceImpl implements OrderService {
@Resource
private AccountMapper accountMapper;
@Resource
private AlipayMessageMapper alipayMessageMapper;
@Resource
private TransactionTemplate transactionTemplate;
@Resource
private RabbitMqSender rabbitMqSender;
/**
* @author ZhengTianLiang
* @date 2020/9/12 21:57
* @desc 更新余额操作 系统1(支付宝系统的)的扣款操作
*/
// @Transactional /**
// * 不能使用这个注解的原因是,mq发送消息是比较占用资源的,但是这个又不是必须成功的。也就是说步骤1、2
// * 是必须同步的。要么同事成功,要么同事失败。而步骤3是可以失败的。因为我要是没往ma发送成功消息,无非就是消息存根表
// * 的状态一直是未确认,那我定时任务会再次发送的。但是步骤1和步骤2是必须要么同时成功,要么同时失败。
// * 所以我们要尽量避免使用声明式事务,可以使用编程式事务(因为往mq发送消息比较消耗内存,又不是事务,无须与前俩绑定的)
// */
@Override
public void updateAmount(int amount, String userId) {
// 创建一个消息存根对象,用来插入到消息存根表、mq发送消息也用这个对象
AlipayMessage message = new AlipayMessage(UUID.randomUUID().toString(), userId, LocalDateTime.now(), MessageStatus.NOCONFIRM);
// 使用编程式事务
Integer execute = transactionTemplate.execute(new TransactionCallback() {
@Override
public Integer doInTransaction(TransactionStatus transactionStatus) {
// 1、account进行扣款操作
Account account = new Account(userId, amount, LocalDateTime.now());
int i = accountMapper.updateAmountById(account);
// 2、第一步执行成功的话,往消息存根表中插入消息
if (i == 1) {
return alipayMessageMapper.insertMessage(message); // 消息存根表成功插入了数据
}
return 0; // 未成功插入数据
}
});
if (execute > 0){ // 3、步骤1,2都成功的话,往mq中发送消息
rabbitMqSender.sendMessage("","",message);
}
}
/**
* @author ZhengTianLiang
* @date 2020/9/13 10:19
* @desc 支付宝监听到了余额宝发来的消息以后,修改消息存根表的状态,改为“已确认”
*/
/**
* 有些童鞋可能要问了,这明明是一个单表操作,为啥要加上@Transactional呢?
* 因为如果不加事务的话,整个连接池就会有两个链接,也就是说,在同一个类中,有的是用了@Transactional注解,
* 有的类没有用@Transactional注解,那么就会导致,有两个连接池对象,一个是spring维护的,一个是mybatis维护的
* 假设没有用@Transactional注解,又用到了mybatis的update,那么mybatis的源码就会从连接池中获取一个链接对象,而
* 这个链接对象是由mybatis维护、管理的。
* 而如果使用了@Transactional注解(或者编程式事务),即使用到了mybatis的update方法,那么也是从spring中拿的连接池对象,
* 这一切都是由spring维护、管理的。
* 总结:下面这个方法,加不加@Transactional注解,执行的效果是一样的,但是加了的话,连接池对象会从2个变成1个,
* 降低了整个系统的开销。本来设计的初衷就是为了提高系统的吞吐量,要是有两个连接池对象,与设计初衷不符,所以加了此注解
*/
@Transactional
@Override
public void updateMessageStatus(String message) { // 余额宝传过来的消息的状态是confirm已确认的
AlipayMessage alipayMessage = JsonUtils.jsonToBean(message, AlipayMessage.class);
alipayMessageMapper.updateMessageSatus(alipayMessage);
}
}
上面这块代码有2点需要注意的,第一点是最好不要使用声明式事务@Transactional,而用编程式事务替代它。因为这三个步骤中(第一步扣款、第二步消息存根表插入数据、第三步往mq中发送消息),前两步是不许一起的,而第三步并非是与前两步绑定一起的(前两步必须同时成功或失败,但是第三步没必要,我即使失败了,没往ma发送成功消息,无非就是消息存根表的状态一直是未确认,那我定时任务会再次发送的。但是步骤1和步骤2是必须要么同时成功,要么同时失败)。第二点则是spring管理的连接池对象和mybatis管理的连接池对象的区别,解释在下面:。
/**
* 有些童鞋可能要问了,这明明是一个单表操作,为啥要加上@Transactional呢?
* 因为如果不加事务的话,整个连接池就会有两个链接,也就是说,在同一个类中,有的是用了@Transactional注解,
* 有的类没有用@Transactional注解,那么就会导致,有两个连接池对象,一个是spring维护的,一个是mybatis维护的
* 假设没有用@Transactional注解,又用到了mybatis的update,那么mybatis的源码就会从连接池中获取一个链接对象,而
* 这个链接对象是由mybatis维护、管理的。
* 而如果使用了@Transactional注解(或者编程式事务),即使用到了mybatis的update方法,那么也是从spring中拿的连接池对象,
* 这一切都是由spring维护、管理的。
* 总结:下面这个方法,加不加@Transactional注解,执行的效果是一样的,但是加了的话,连接池对象会从2个变成1个,
* 降低了整个系统的开销。本来设计的初衷就是为了提高系统的吞吐量,要是有两个连接池对象,与设计初衷不符,所以加了此注解
*/
2、RabbitMQ的相关配置
package com.alipay.rabbitmq;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author ZhengTianLiang
* @date 2020/9/12 22:27
* @desc rabbitmq的配置类
*/
@Configuration
public class RabbitMqConfig {
/**
* rabbitmq中,消息发送者只需要知道交换机的名称就行(还需携带路由键),不需要知道队列的名称
* rabbitmq中,消息消费者只需要知道队列名称就行,不需要知道交换器和路由键的名称
*/
/**
* @author ZhengTianLiang
* @date 2020/9/12 22:42
* @desc 往rabbitmq的broker里面创建一个队列
*/
@Bean(name = "message")
public Queue getQueue() {
return new Queue("zheng.alipay.message");
}
/**
* @author ZhengTianLiang
* @date 2020/9/12 22:46
* @desc 创建交换器
*/
@Bean
public TopicExchange getExchange(){
return new TopicExchange("zheng.alipay.exchange");
}
@Bean
Binding bindingExchangeMessage(@Qualifier("message") Queue getQueue,TopicExchange getExchange){
return BindingBuilder.bind(getQueue).to(getExchange()).with("zheng.alipay.routkey");
}
}
-b.RabbitMqSender
package com.alipay.rabbitmq;
import com.alipay.entity.AlipayMessage;
import com.alipay.util.json.JsonUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* @author ZhengTianLiang
* @date 2020/9/12 22:29
* @desc 消息发送者
*/
@Slf4j
@Component
public class RabbitMqSender {
@Resource
private AmqpTemplate amqpTemplate;
/**
* @author ZhengTianLiang
* @date 2020/9/12 22:29
* @desc 从支付宝系统发送消息到mq中
* @param exchange 交换机名称
* @param routingKey 路由键名称
* @param message 要发送的消息
*/
public void sendMessage(String exchange, String routingKey, AlipayMessage message) {
log.info("支付宝系统往mq中发送了消息:" + message.getMessageId());
amqpTemplate.convertAndSend(exchange, routingKey, JsonUtils.toJson(message));
}
}
package com.alipay.rabbitmq.listener;
import com.alipay.entity.AlipayMessage;
import com.alipay.service.OrderService;
import com.alipay.util.json.JsonUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* @author ZhengTianLiang
* @date 2020/9/13 12:08
* @desc 支付宝系统的mq的监听器,
*/
@Slf4j
@Component
public class MessageListener {
@Resource
private OrderService orderService;
/**
* @author ZhengTianLiang
* @date 2020/9/13 12:09
* @desc 监听余额宝发送的消息,用来改变支付宝自己的消息存根表的消息状态
*/
@RabbitListener(queues = "zheng.moneypay.message") // 它监听的是余额宝的消息队列
public void process(String message){
orderService.updateMessageStatus(message); // 更新消息的状态
}
}
3、系统B(余额宝系统)的核心业务逻辑(是写在mq的监听器里面了)
package com.rabbitmq.listener;
import com.entity.AlipayMessage;
import com.entity.MessageStatus;
import com.entity.MoneypayMessage;
import com.rabbitmq.RabbitMqSender;
import com.service.OrderService;
import com.util.json.JsonUtils;
import com.util.mapper.MapperUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionTemplate;
import javax.annotation.Resource;
/**
* @author ZhengTianLiang
* @date 2020/9/13 11:08
* @desc mq的监听器, 也是核心的业务代码
*/
@Slf4j
@Component
public class MessageListener {
@Resource
private OrderService orderService;
@Resource
private TransactionTemplate transactionTemplate;
@Resource
private RabbitMqSender rabbitMqSender;
@Value("${moneypay.exchange}")
private String exchange;
@Value("${moneypay.routkey}")
private String routkey;
/**
* @author ZhengTianLiang
* @date 2020/9/13 11:40
* @desc 监听消息发送者(支付宝系统)发送来的消息,并作出相应的业务操作
*/
@RabbitListener(queues = "zheng.alipay.message") // 消息发送者的队列,就是支付宝发送到的队列的队列名
public void process(String jsonMessage) {
AlipayMessage alipayMessage = JsonUtils.jsonToBean(jsonMessage, AlipayMessage.class);
MoneypayMessage moneypayMessage = MapperUtils.mapperBean(alipayMessage, MoneypayMessage.class);
// 1、去余额宝自己的消息存根中查,看看能不能根据支付宝系统传过来的消息id来查到数据,能查到则说明消费过了;查不到则是未消费过
Integer count = orderService.queryMessageCountById(alipayMessage.getMessageId());
Boolean exec = transactionTemplate.execute(new TransactionCallback<Boolean>() {
@Override
public Boolean doInTransaction(TransactionStatus transactionStatus) {
if (count == 0) {
//此消息未消费过
orderService.updateAmount(moneypayMessage.getAmount(), moneypayMessage.getUserId());// 加款操作
orderService.insertMessage(moneypayMessage); // 往余额宝自己的消息本地存根中插入数据
}
return true;
}
});
if (count > 0) {
// 此消息已经被消费过了,则直接通过mq去通知支付宝系统,说这个消息消费过了
log.info("此消息已经被消费过了,不做任何操作");
}
if (exec){
// 去mq中發消息,通知支付宝,说此消息已经被消费过了
alipayMessage.setStatus(MessageStatus.CONFIRM);
rabbitMqSender.sendMessage(exchange,routkey,alipayMessage);
}
}
}
4、mq的相关配置
package com.rabbitmq;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author ZhengTianLiang
* @date 2020/9/12 22:27
* @desc rabbitmq的配置类
*/
@Configuration
public class RabbitMqConfig {
/**
* rabbitmq中,消息发送者只需要知道交换机的名称就行(还需携带路由键),不需要知道队列的名称
* rabbitmq中,消息消费者只需要知道队列名称就行,不需要知道交换器和路由键的名称
*/
/**
* @author ZhengTianLiang
* @date 2020/9/12 22:42
* @desc 往rabbitmq的broker里面创建一个队列
*/
@Bean(name = "message")
public Queue getQueue() {
return new Queue("zheng.moneypay.message");
}
/**
* @author ZhengTianLiang
* @date 2020/9/12 22:46
* @desc 创建交换器
*/
@Bean
public TopicExchange getExchange(){
return new TopicExchange("zheng.moneypay.exchange");
}
@Bean
Binding bindingExchangeMessage(@Qualifier("message") Queue getQueue,TopicExchange getExchange){
return BindingBuilder.bind(getQueue).to(getExchange()).with("zheng.moneypay.routkey");
}
}
-b.RabbitMqSender
package com.rabbitmq;
import com.util.json.JsonUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* @author ZhengTianLiang
* @date 2020/9/12 22:29
* @desc 消息发送者
*/
@Slf4j
@Component
public class RabbitMqSender {
@Resource
private AmqpTemplate amqpTemplate;
/**
* @param exchange 交换机名称
* @param routingKey 路由键名称
* @param message 要发送的消息
* @author ZhengTianLiang
* @date 2020/9/12 22:29
* @desc 从支付宝系统发送消息到mq中
*/
public void sendMessage(String exchange, String routingKey, Object message) {
log.info("支付宝系统往mq中发送了消息:" + message);
amqpTemplate.convertAndSend(exchange, routingKey, JsonUtils.toJson(message));
}
}
5、余额宝系统的service
package com.service.impl;
import com.dao.AccountMapper;
import com.dao.MoneypayMessageMapper;
import com.entity.Account;
import com.entity.MoneypayMessage;
import com.service.OrderService;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.UUID;
/**
* @author ZhengTianLiang
* @date 2020/9/13 11:27
* @desc service
*/
@Service
public class OrderServiceImpl implements OrderService {
@Resource
MoneypayMessageMapper messageMapper;
@Resource
AccountMapper accountMapper;
/**
* @author ZhengTianLiang
* @date 2020/9/13 11:31
* @desc 这个是账户的新增操作
*/
@Override
public void updateAmount(Integer amount, String userId) {
Account account = new Account(userId,amount, LocalDateTime.now());
accountMapper.updateAmountById(account);
}
/**
* @author ZhengTianLiang
* @date 2020/9/13 11:27
* @desc 根据消息id查到消息的数量(用来判断该消息是否被消费过)
*/
@Override
public Integer queryMessageCountById(String messageId) {
return messageMapper.queryCountByMessageId(messageId);
}
/**
* @author ZhengTianLiang
* @date 2020/9/13 11:27
* @desc 往余额宝系统的消息存根表中插入数据
*/
@Override
public void insertMessage(MoneypayMessage message) {
messageMapper.insertMessage(message);
}
}
二、分布式事务的三种比较常见的解决方案:
1、基于XA协议的两阶段提交
①XA规范中分布式事务由AP,RM,TM三部分组成。具体点就是,应用程序(AP),事务管理器(TM),资源管理器(RM),通信资源管理器(CRM)四个部分。一般,常见的事务管理器(TM)是交易中间件,常见的资源管理器(RM)是数据库,常见的通信资源管理器(CRM)是消息中间件
AP:定义事务边界(定义事务开始和结束),并访问事务边界内的资源
RM:资源管理器,管理计算机共享的资源,许多软件都可以去访问这些资源,资源包含比如数据库,文件系统,打印机服务器等
TM:负责管理全局事务,分配事务唯一标识,监控事务的执行进度,并负责事务的提交、回滚、失败恢复等
②两阶段协议:
第一阶段:TM要求所有的RM准备提交对应的事务分支,询问RM是否有能力保证成功的提交事务分支,RM根据自己的情况,如果判断自己进行的工作可以被提交,那就对工作的内容就行持久化,并给TM回执OK,否则给TM回执NO,RM在发送了否定答复并回滚了已经的工作后,就可以丢弃这个事务分支信息了。
第二阶段:TM根据阶段1各个RM prepare的结果,决定是提交还是回滚事务,如果所有的RM都prepare成功,那么TM通知所有的RM进行提交,如果有RM prepare回执NO的话,则TM通知所有的RM回滚自己的事务分支
③XA协议两阶段提交的优缺点:
优点:进来保持了数据的强一致性,适合对数据强一致性要求跟高的领域(并非100%保证强一致性)
缺点:实现复杂,牺牲了可用性,对性能影响比较大,不适合高并发高性能的场景
2、TCC补偿机制
TCC其实就是采用的补偿机制,其核心思想是,针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作,他分为三个阶段:
·Try阶段,主要是对业务系统做检测已经资源御灵
·Confirm阶段主要是对业务系统做确认提交,Try阶段执行成功并开始执行Confirm阶段时,默认Confirm阶段是不会出错的。即:主要Try成功,Confirm一定成功
·Cancel阶段主要是业务执行错误,需要回滚的状态下执行的业务取消,预留资源释放
②TCC补偿机制的优缺点:
优点:相比于两阶段提交,可用性比较强(因为两阶段涉及到了锁的概念)
缺点:数据的一致性要差一些,TCC属于应用层的一种补偿方式,所以需要程序员在实现的时候多写很多补偿的代码。在一些场景中,一些业务流程可能用TCC不太好定义处理
3、消息最终一致性
消息最终一致性应该是业界使用最多的,其核心思想是将分布式事务拆分为本地事务进行处理。
①基本流程:
假设有两个事务,第一个事务先写入业务数据,然后在写入消息数据(此时会额外建立一个消息表来记录我要发送的消息内容),写完消息数据之后,在将数据发送给MQ(第一阶段到此结束)。第二个事务要用到这个数据第二个事务就从MQ中拿到对应的数据,拿到这个消息,在写入业务数据。如果说事务成功了,就修改消息表(就是事务1新建的,用来存储要发送的消息内容的消息表)中的状态。如果说事务操作这个消息失败了,也要给事务1说一下,然后事务1在调用一些补偿的代码来执行一些数据库回滚的操作。若写入业务事务成功,(事务1)发送到MQ失败了,会怎么样?则消息表会存积大量的未处理的消息数据,此时,会有一个另外的线程去定时的去扫描这个消息表,若发现有大量的未处理的消息,则在进行一些对应的补偿逻辑、
②消息最终一致性的优缺点:
优点:一种非常经典的实现,避免了分布式事务,实现了最终一致性
缺点:消息表会耦合到业务系统中,若没有封装好的解决方案,会有很多的杂活。
其实分布式的事务原理肯定都一样:利用一个总的事务管理器来管理一个个的零散的本地事务,假如全部成功,则成功;有任意一个失败,则回滚。所以分布式事务几乎都是非常影响系统性能的。
现在比较常用的有TX-LCN、seata等。还有之前的基于XA两阶段提交、TCC补偿机制等等。