一、理论基础
1.1、基于MQ解决分布式事务原理
(1)确保生产者(支付服务)一定将增加积分的消息投递到MQ中——使用确认机制;
(2)确保消费者消费消息一定成功——使用手动ack应答模式 。
如果没有出现异常则通知MQ删除该消息,有异常则重试消费。
注意:重试过程中需要注意幂等性问题:
(3)如果MQ异步代码执行之后,代码出现异常的情况下,如何保证分布式事务问题?——采用补偿机制。
1.2、采用HTTP协议调用积分服务接口增加积分,为什么不好?
因为如果调用采用同步的话,异步回调接口响应是很容易延迟的,可能会导致第三方支付系统进行重试。所以,支付服务接口调用积分服务接口采用MQ异步形式
疑问1:积分服务增加积分时,如何保证幂等性问题?——在积分表中,通过支付ID和积分保持唯一即可,使支付ID全局唯一
疑问2:采用MQ异步传输,肯定存在分布式事务问题,怎么解决?——把消息发到MQ增加完积分,后面代码突然报错,积分加成功了,但是支付状态还是未支付——只需要通过MQ的最终一致性解决即可
1.3、 支付服务——>积分服务,增加积分原理
支付服务投递增加积分消息到 “积分队列” 中,积分服务监听积分队列。一旦有新的积分消息,一旦有新的积分消息,则插入到积分表中。先走增加积分,补偿队列是查支付状态,有没有支付过,如果
1.4、面试官问在项目中什么场景用过多线程
在支付回调当中,为了能够更快地响应给第三方支付系统,防止有重试的机制产生,我们把比较耗时间的业务逻辑统一采用异步形式,利用多线程结合MQ进行处理。比如基于MQ增加积分操作
二、以支付服务和积分服务为例介绍分布式事务
2.1、具体支付模板异步回调代码中调用积分服务增加积分
2.2、生产者——支付服务
MQ实现增加积分,异步传输
生产者使用消息确认机制 :
生产者往服务器端发送消息的时候,采用应答机制,失败,则采用递归循环重试机制——Confirm模式的 消息确认机制
生产者代码:
(1)封装接收参数;
(2)模板方式rabbitTemplate.setConfirmCallback(this),开启Confirm消息确认机制
(3)查看setConfirmCallback源码可以知道它调用了confirm方法,重写confirm即可实现消息确认机制
@Component
public class IntegralProducer implements RabbitTemplate.ConfirmCallback {
@Autowired
private RabbitTemplate rabbitTemplate;
//JSON: {"paymentId":202002201101,"userId":1101,"integral":100}
@Transactional
public void send(JSONObject jsonObject) {
String jsonString = jsonObject.toJSONString();
System.out.println("jsonString:" + jsonString);
String paymentId = jsonObject.getString("paymentId");
// 封装消息
Message message = MessageBuilder.withBody(jsonString.getBytes())
.setContentType(MessageProperties.CONTENT_TYPE_JSON).setContentEncoding("utf-8").setMessageId(paymentId)
.build();
// 构建回调返回的数据(消息id)
rabbitTemplate.setMandatory(true);//Mandatory为true时,消息通过交换器无法匹配到队列会返回给生产者
* 为false时,匹配不到会直接被丢弃
//开启消息确认机制
rabbitTemplate.setConfirmCallback(this);
CorrelationData correlationData = new CorrelationData(jsonString);
rabbitTemplate.convertAndSend("integral_exchange_name", "integralRoutingKey", message, correlationData);
}
// 生产消息确认机制 生产者往服务器端发送消息的时候,采用应答机制
//失败,则采用递归循环重试机制
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
String jsonString = correlationData.getId();
System.out.println("消息id:" + correlationData.getId());
if (ack) {
System.out.println("消息发送确认成功");
} else {
JSONObject jsonObject = JSONObject.parseObject(jsonString);
//失败,则采用递归循环重试机制
send(jsonObject);
System.out.println("消息发送确认失败:" + cause);
}
}
}
2.2、银联支付回调中,增加积分,并且用MQ处理了分布式事务
上一节“银联支付回调模版实现”中,增加一个“增加积分”功能:调用生产者的send方法
/**
* @description: 银联支付回调模版实现
*/
@Component
public class UnionPayCallbackTemplate extends AbstractPayCallbackTemplate {
@Autowired
private PaymentTransactionMapper paymentTransactionMapper;
@Autowired
private IntegralProducer integralProducer;
....省略
// 异步回调中网络尝试延迟,导致异步回调重复执行 可能存在幂等性问题
@Override
@Transactional
public String asyncService(Map<String, String> verifySignature) {
String orderId = verifySignature.get("orderId"); // 获取后台通知的数据,其他字段也可用类似方式获取
String respCode = verifySignature.get("respCode");
// 判断respCode=00、A6后,对涉及资金类的交易,请再发起查询接口查询,确定交易成功后更新数据库。
System.out.println("orderId:" + orderId + ",respCode:" + respCode);
// 1.判断respCode是否为已经支付成功断respCode=00、A6后,
if (!(respCode.equals("00") || respCode.equals("A6"))) {
return failResult();
}
// 根据日志 手动补偿 使用支付id调用第三方支付接口查询
PaymentTransactionEntity paymentTransaction = paymentTransactionMapper.selectByPaymentId(orderId);
if (paymentTransaction.getPaymentStatus().equals(PayConstant.PAY_STATUS_SUCCESS)) {
// 网络重试中,之前已经支付过
return successResult();
}
// 2.将状态改为已经支付成功
paymentTransactionMapper.updatePaymentStatus(PayConstant.PAY_STATUS_SUCCESS + "", orderId, "yinlian_pay");
// 3.调用积分服务接口增加积分(处理幂等性问题) MQ
addMQIntegral(paymentTransaction); // 使用MQ
int i = 1 / 0; // 支付状态还是为待支付状态但是 积分缺增加
return successResult();
}
/**
* 基于MQ增加积分
*/
@Async
private void addMQIntegral(PaymentTransactionEntity paymentTransaction) {
JSONObject jsonObject = new JSONObject();
jsonObject.put("paymentId", paymentTransaction.getPaymentId());
jsonObject.put("userId", paymentTransaction.getUserId());
jsonObject.put("integral", 100);
integralProducer.send(jsonObject);//调用生产者
}
}
2.2、RabbitMQ配置
@Component
public class RabbitmqConfig {
// 添加积分队列
public static final String INTEGRAL_DIC_QUEUE = "integral_queue";
// 补单队列,
public static final String INTEGRAL_CREATE_QUEUE = "integral_create_queue";
// 积分交换机
private static final String INTEGRAL_EXCHANGE_NAME = "integral_exchange_name";
// 1.定义订单队列
@Bean
public Queue directIntegralDicQueue() {
return new Queue(INTEGRAL_DIC_QUEUE);
}
// 2.定义补订单队列
@Bean
public Queue directCreateintegralQueue() {
return new Queue(INTEGRAL_CREATE_QUEUE);
}
// 2.定义交换机
@Bean
DirectExchange directintegralExchange() {
return new DirectExchange(INTEGRAL_EXCHANGE_NAME);
}
// 3.积分队列与交换机绑定
@Bean
Binding bindingExchangeintegralDicQueue() {
return BindingBuilder.bind(directIntegralDicQueue()).to(directintegralExchange()).with("integralRoutingKey");
}
// 3.补单队列与交换机绑定
@Bean
Binding bindingExchangeCreateintegral() {
return BindingBuilder.bind(directCreateintegralQueue()).to(directintegralExchange()).with("integralRoutingKey");
}
}
2.3、消费者——积分服务
积分服务骨架图:
消费者手动签收 channel.basicNack()与channel.basicAck()区别:
一个通知MQ消息被正确消费了,一个通知消费时发生了异常——用来防止数据丢失,保证幂等性
/**
* @description: 消费者——积分服务
*/
@Component
@Slf4j
public class IntegralConsumer {
@Autowired
private IntegralMapper integralMapper;
@RabbitListener(queues = "integral_queue")
public void process(Message message, @Headers Map<String, Object> headers, Channel channel) throws IOException {
try {
String messageId = message.getMessageProperties().getMessageId();
String msg = new String(message.getBody(), "UTF-8");
// log.info(">>>messageId:{},msg:{}", messageId, msg);
JSONObject jsonObject = JSONObject.parseObject(msg);
String paymentId = jsonObject.getString("paymentId");
if (StringUtils.isEmpty(paymentId)) {
// log.error(">>>>支付id不能为空 paymentId:{}", paymentId);
basicNack(message, channel);
return;
}
// 使用paymentId查询是否已经增加过积分 网络重试间隔
IntegralEntity resultIntegralEntity = integralMapper.findIntegral(paymentId);
if (resultIntegralEntity != null) {
// log.error(">>>>paymentId:{}已经增加过积分", paymentId);
// 已经增加过积分,通知MQ不要在继续重试。
basicNack(message, channel);
return;
}
Integer userId = jsonObject.getInteger("userId");
if (userId == null) {
// log.error(">>>>paymentId:{},对应的用户userId参数为空", paymentId);
basicNack(message, channel);
return;
}
Long integral = jsonObject.getLong("integral");
if (integral == null) {
// log.error(">>>>paymentId:{},对应的用户integral参数为空", integral);
return;
}
IntegralEntity integralEntity = new IntegralEntity();
integralEntity.setPaymentId(paymentId);
integralEntity.setIntegral(integral);
integralEntity.setUserId(userId);
integralEntity.setAvailability(1);
// 插入到数据库中
int insertIntegral = integralMapper.insertIntegral(integralEntity);
if (insertIntegral > 0) {
// 手动签收消息,通知mq服务器端删除该消息
basicNack(message, channel);
}
// 采用重试机制
} catch (Exception e) {
// log.error(">>>>ERROR MSG:", e.getMessage());
basicNack(message, channel);
}
}
// 消费者获取到消息之后 手动签收 通知MQ删除该消息
private void basicNack(Message message, Channel channel) throws IOException {
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
}
// 什么场景下 适合于重试 网络连接、空指针 参数错误
}
上一篇:支付功能-模板+多线程+RabbitMQ实现支付回调
若对你有帮助,欢迎关注!!点赞!!评论!!