支付功能实战(5):使用RabbitMQ解决分布式事务

一、理论基础

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实现支付回调

若对你有帮助,欢迎关注!!点赞!!评论!!

  • 1
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值