学成在线day13 支付通知

支付通知

1、业务流程&技术选型

在调用支付宝API完成支付后,需要通知其他相关微服务该条订单已完成,在项目中使用RabbitMQ实现。

(1) xuecheng-plus-orders支付成功接收到回调通知或手动进行支付结果查询。

(2)xuecheng-plus-orders服务向RabbitMQ写入消息,发送课程ID和课程类型至消息队列。

(3)xuecheng-plus-learning服务监听消息队列,获取课程ID和课程类型。

(4)xuecheng-plus-learning服务修改xc_choose_course表该条课程的状态为已支付,并且向xc_course_tables表插入一条记录。

在本项目中,xuecheng-plus-orders服务作为生产方向消息队列写数据,xuecheng-plus-learning服务作为消费方从消息队列中读取数据。若生产方的数据未送达交换机,或交换机未转发至队列、消费者因为系统错误或宕机未进行消费、RabbitMQ宕机,都有造成消息丢失的可能性。

针对上面的情况,我们可以采用持久化消息,生产者,消费者确认机制,以及重试机制实现消息可靠投递。

spring:
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: guest
    password: guest
    virtual-host: /
    publisher-confirm-type: correlated #correlated 异步回调,定义ConfirmCallback,MQ返回结果时会回调这个ConfirmCallback
    publisher-returns: false #开启publish-return功能,同样是基于callback机制,需要定义ReturnCallback
    template:
      mandatory: false #定义消息路由失败时的策略。true,则调用ReturnCallback;false:则直接丢弃消息
    listener:
      simple:
        prefetch: 1  #每次只能获取一条消息,处理完成才能获取下一个消息
        acknowledge-mode: none #auto:出现异常时返回unack,消息回滚到mq;没有异常,返回ack ,manual:手动控制,none:丢弃消息,不回滚到mq
        retry:
          enabled: true #开启消费者失败重试
          initial-interval: 1000ms #初识的失败等待时长为1秒
          multiplier: 1 #失败的等待时长倍数,下次等待时长 = multiplier * last-interval
          max-attempts: 3 #最大重试次数
          stateless: true #true无状态;false有状态。如果业务中包含事务,这里改为false

 D表示交换机&消息队列被持久化

生产方配置ReturnCallback

@Slf4j
@Configuration
public class PayNotifyConfig implements ApplicationContextAware {

    //交换机
    public static final String PAYNOTIFY_EXCHANGE_FANOUT = "paynotify_exchange_fanout";
    //支付结果通知消息类型
    public static final String MESSAGE_TYPE = "payresult_notify";
    //支付通知队列
    public static final String PAYNOTIFY_QUEUE = "paynotify_queue";

    //声明交换机,且持久化
    @Bean(PAYNOTIFY_EXCHANGE_FANOUT)
    public FanoutExchange paynotify_exchange_fanout() {
        // 三个参数:交换机名称、是否持久化、当没有queue与其绑定时是否自动删除
        return new FanoutExchange(PAYNOTIFY_EXCHANGE_FANOUT, true, false);
    }
    //支付通知队列,且持久化
    @Bean(PAYNOTIFY_QUEUE)
    public Queue course_publish_queue() {
        return QueueBuilder.durable(PAYNOTIFY_QUEUE).build();
    }

    //交换机和支付通知队列绑定
    @Bean
    public Binding binding_course_publish_queue(@Qualifier(PAYNOTIFY_QUEUE) Queue queue, @Qualifier(PAYNOTIFY_EXCHANGE_FANOUT) FanoutExchange exchange) {
        return BindingBuilder.bind(queue).to(exchange);
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        // 获取RabbitTemplate
        RabbitTemplate rabbitTemplate = applicationContext.getBean(RabbitTemplate.class);
        //消息处理service
        MqMessageService mqMessageService = applicationContext.getBean(MqMessageService.class);
        // 设置ReturnCallback
        rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {
            // 投递失败,记录日志
            log.info("消息发送失败,应答码{},原因{},交换机{},路由键{},消息{}",
                    replyCode, replyText, exchange, routingKey, message.toString());
            MqMessage mqMessage = JSON.parseObject(message.toString(), MqMessage.class);
            //将消息再添加到消息表
            mqMessageService.addMessage(mqMessage.getMessageType(),mqMessage.getBusinessKey1(),mqMessage.getBusinessKey2(),mqMessage.getBusinessKey3());

        });
    }
}

生产方配置confirmCallBack

 /**
     * 发送通知结果
     *
     * @param message
     */
    @Override
    public void notifyPayResult(MqMessage message) {
        //1、消息体,转json
        String msg = JSON.toJSONString(message);
        //设置消息持久化
        Message msgObj = MessageBuilder.withBody(msg.getBytes(StandardCharsets.UTF_8))
                .setDeliveryMode(MessageDeliveryMode.PERSISTENT)
                .build();
        // 2.全局唯一的消息ID,需要封装到CorrelationData中
        CorrelationData correlationData = new CorrelationData(message.getId().toString());
        // 3.添加callback 回调函数
        correlationData.getFuture().addCallback(
                result -> {
                    if(result.isAck()){
                        // 3.1.ack,消息成功
                        log.debug("通知支付结果消息发送成功, ID:{}", correlationData.getId());
                        //删除消息表中的记录
                        mqMessageService.completed(message.getId());
                    }else{
                        // 3.2.nack,消息失败
                        log.error("通知支付结果消息发送失败, ID:{}, 原因{}",correlationData.getId(), result.getReason());
                    }
                },
                ex -> log.error("消息发送异常, ID:{}, 原因{}",correlationData.getId(),ex.getMessage())
        );
        // 发送消息 因为是广播模式所以路由键为空
        rabbitTemplate.convertAndSend(PayNotifyConfig.PAYNOTIFY_EXCHANGE_FANOUT, "", msgObj,correlationData);
    }

  • confirm callback(确认回调):当消息成功到达 Exchange 时,Broker 会发送一个确认给生产者。如果消息发送失败,则会触发 confirm callback 中的相应逻辑。确认回调通常用于确保消息被成功发送到 Exchange,以便生产者可以知道是否需要重新发送消息。
  • return callback(返回回调):当消息被路由到 Exchange 但无法路由到 Queue 时,Broker 会发送一个返回消息给生产者。返回回调通常用于处理无法路由到 Queue 的消息,生产者可以在返回回调中执行相应的逻辑,比如记录日志或者采取其他措施。

在本项目中,生产者消息投递成功到交换机时,执行confirmCallBack删除mq_message表中的记录。如果消息从交换机投递到队列失败时,则把消息再次添加回mq_message表,并且进行重试(重试次数在配置文件中配置)。

spring:
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: guest
    password: guest
    virtual-host: /
    publisher-confirm-type: correlated #correlated 异步回调,定义ConfirmCallback,MQ返回结果时会回调这个ConfirmCallback
    publisher-returns: true #开启publish-return功能,同样是基于callback机制,需要定义ReturnCallback
    template:
      mandatory: true #定义消息路由失败时的策略。true,则调用ReturnCallback;false:则直接丢弃消息
    listener:
      simple:
        prefetch: 1  #每次只能获取一条消息,处理完成才能获取下一个消息
        acknowledge-mode: manual #auto:出现异常时返回unack,消息回滚到mq;没有异常,返回ack ,manual:手动控制,none:丢弃消息,不回滚到mq
        retry:
          enabled: true #开启消费者失败重试
          initial-interval: 1000ms #初识的失败等待时长为1秒
          multiplier: 1 #失败的等待时长倍数,下次等待时长 = multiplier * last-interval
          max-attempts: 3 #最大重试次数
          stateless: true #true无状态;false有状态。如果业务中包含事务,这里改为false
      direct:
        acknowledge-mode: manual

Q1:如果消费者未收到消息或者出现系统错误时,是否会触发回调函数?

在 RabbitMQ 中,当消费者未收到消息或者出现系统错误时,不会触发回调函数。回调函数是在消息生产者发送消息到 RabbitMQ 服务器后触发的,用于确认消息是否成功发送到 Exchange 或者是否被正确路由到 Queue。

如果消费者未收到消息,可能有以下几种原因:

  • 消息丢失:在消息生产者发送消息到 RabbitMQ 服务器后,由于网络问题或者其他原因导致消息丢失,这种情况下消费者是不会收到消息的。解决这个问题需要确保消息生产者发送消息时能够成功连接到 RabbitMQ 服务器,并且消息能够正确发送。
  • 消费者无法处理消息:如果消费者无法正确处理消息,可能是因为消费者代码出现了错误或者处理消息的逻辑不完善,这种情况下 RabbitMQ 不会触发回调函数。解决这个问题需要检查消费者代码逻辑是否正确,并确保能够处理各种异常情况。
  • 队列配置问题:如果队列配置不正确,比如消费者没有正确绑定到队列,或者队列设置了错误的参数,可能会导致消费者无法接收到消息。解决这个问题需要检查队列的配置,并确保消费者能够正确订阅到消息。

2、接口定义

生产者业务层:



    /**
     * 请求支付宝查询支付结果
     *
     * @param payNo 支付记录id
     * @return 支付记录信息
     */
    @Override
    public PayRecordDto queryPayResult(String payNo) {
        //获得初始化的AlipayClient
        AlipayClient alipayClient = new DefaultAlipayClient(AlipayConfig.URL,
                APP_ID,APP_PRIVATE_KEY,
                "json",
                AlipayConfig.CHARSET,
                ALIPAY_PUBLIC_KEY,
                AlipayConfig.SIGNTYPE);
        AlipayTradeQueryRequest request = new AlipayTradeQueryRequest();
        JSONObject bizContent = new JSONObject();
        bizContent.put("out_trade_no", payNo);
        request.setBizContent(bizContent.toString());
        AlipayTradeQueryResponse response = null;
        try {
            response = alipayClient.execute(request);
        } catch (AlipayApiException e) {
            log.error("{}:查询支付宝支付结果错误!",payNo);
            return null;
        }
        if (!response.isSuccess()) {
            log.error("{}:查询支付宝支付结果失败!",payNo);
            return null;
        }
        String resultJson = response.getBody();
        //转map
        Map resultMap = JSON.parseObject(resultJson, Map.class);
        Map alipay_trade_query_response = (Map) resultMap.get("alipay_trade_query_response");
        //交易状态
        String tradeStatus = (String) alipay_trade_query_response.get("trade_status");
        //支付宝交易号
        String tradeNo = (String) alipay_trade_query_response.get("trade_no");
        PayStatusDto payStatusDto = new PayStatusDto();
        payStatusDto.setTrade_status(tradeStatus);
        payStatusDto.setTrade_no(tradeNo);
        payStatusDto.setOut_trade_no(payNo);
        payStatusDto.setApp_id(APP_ID);
        //处理订单状态
        return this.handlePayStatus(payStatusDto);
    }

  /**
     * 处理订单状态,更新xc_pay_record表
     * @return
     */
    public PayRecordDto handlePayStatus(PayStatusDto dto) {
        PayRecordDto payRecordDto = new PayRecordDto();
        String payNo = dto.getOut_trade_no();
        String tradeNo = dto.getTrade_no();
        String tradeStatus = dto.getTrade_status();

        XcPayRecord xcPayRecord = xcPayRecordMapper.selectOne(new LambdaQueryWrapper<XcPayRecord>().eq(XcPayRecord::getPayNo, payNo));
        if (null == xcPayRecord ){
            log.error("{}:查询订单记录不存在!",tradeNo);
            XueChengPlusException.cast("查询订单记录不存在!");
        }
        if (xcPayRecord.getStatus().equals("601002")){
            BeanUtils.copyProperties(xcPayRecord,payRecordDto);
            return payRecordDto;
        }

        //修改xc_pay_record和xc_orders 交易状态
        switch (tradeStatus) {
            case "TRADE_CLOSED":
                xcPayRecord.setStatus("601003");
                break;
            case "TRADE_SUCCESS":
            case "TRADE_FINISHED":
                xcPayRecord.setStatus("601002");
                xcPayRecord.setPaySuccessTime(LocalDateTime.now());
                break;
        }
        xcPayRecord.setOutPayNo(tradeNo);
        xcPayRecord.setOutPayChannel("alipay");
        xcPayRecordMapper.updateById(xcPayRecord);
        BeanUtils.copyProperties(xcPayRecord,payRecordDto);
        //通过orderId查询订单表信息
        Long orderId = xcPayRecord.getOrderId();
        XcOrders xcOrders = ordersMapper.selectOne(new LambdaQueryWrapper<XcOrders>().eq(XcOrders::getId, orderId));
        if (null == xcOrders ){
            log.error("{}:查询订单不存在!",tradeNo);
            XueChengPlusException.cast("查询订单不存在!");
        }
        //存入mq_message表
        MqMessage message = mqMessageService.addMessage("payresult_notify", xcOrders.getOutBusinessId(), xcOrders.getOrderType(), null);
        //将消息发送至队列,通知learning服务
        this.notifyPayResult(message);
        return payRecordDto;
    }

    /**
     * 发送通知结果
     *
     * @param message
     */
    @Override
    public void notifyPayResult(MqMessage message) {
        //1、消息体,转json
        String msg = JSON.toJSONString(message);
        //设置消息持久化
        Message msgObj = MessageBuilder.withBody(msg.getBytes(StandardCharsets.UTF_8))
                .setDeliveryMode(MessageDeliveryMode.PERSISTENT)
                .build();
        // 2.全局唯一的消息ID,需要封装到CorrelationData中
        CorrelationData correlationData = new CorrelationData(message.getId().toString());
        // 3.添加callback 回调函数,接收方接受成功或失败后回调
        correlationData.getFuture().addCallback(
                result -> {
                    if(result.isAck()){
                        // 3.1.ack,消息成功
                        log.debug("通知支付结果消息发送成功, ID:{}", correlationData.getId());
                        //删除消息表中的记录
                        mqMessageService.completed(message.getId());
                    }else{
                        // 3.2.nack,消息失败
                        log.error("通知支付结果消息发送失败, ID:{}, 原因{}",correlationData.getId(), result.getReason());
                    }
                },
                ex -> log.error("消息发送异常, ID:{}, 原因{}",correlationData.getId(),ex.getMessage())
        );
        // 发送消息 因为是广播模式所以路由键为空
        rabbitTemplate.convertAndSend(PayNotifyConfig.PAYNOTIFY_EXCHANGE_FANOUT, "", msgObj,correlationData);
    }

支付宝异步回调接口同理也调用了handlePayStatus方法

消费者业务层:

@Service
@Slf4j
@Transactional
public class ReceviceOrderMessageServiceImpl implements ReceviceOrderMessageService {

    /*
    选课记录表
     */
    @Autowired
    private XcChooseCourseMapper chooseCourseMapper;

    /*
    课程表
     */
    @Autowired
    private XcCourseTablesMapper courseTablesMapper;

    @Autowired
    private MyCourseTables myCourseTables;

    @Autowired
    private ReceviceOrderMessageService receviceOrderMessageService;

    @Resource
    private RedisTemplate<String,Integer> redisTemplate;

    @Resource
    private RedissonClient redissonClient;

    private static final String QUEUE_KEY = "learning:";

    private static final String REDISSON_KEY = "redisson:";

    @Override
    @RabbitListener(queues = PayNotifyConfig.PAYNOTIFY_QUEUE)
    public void receiveMessage(Message message, Channel channel)  {
        byte[] body = message.getBody();
        long deliverTag = message.getMessageProperties().getDeliveryTag();
        String jsonStr = new String(body);
        MqMessage mqMessage = JSON.parseObject(jsonStr, MqMessage.class);
        //得到选课id
        String businessKey1 = mqMessage.getBusinessKey1();
        //得到课程类型
        String businessKey2 = mqMessage.getBusinessKey2();
        //处理课程信息
        Integer count = 0;
        try {
            count = redisTemplate.opsForValue().get(QUEUE_KEY + businessKey1);
            if (!ObjectUtils.isEmpty(count) && count == 3){
                log.error("消息已达到最大重试次数:{},作丢弃处理",count);
                channel.basicNack(deliverTag,false,false);
                return;
            }
            this.receviceOrderMessageService.handleCourseDBData(businessKey1,businessKey2);
            int i = 1/0;
            channel.basicAck(deliverTag,true);
            log.info("消息手动确认成功!");
        } catch (Exception e) {
            try {
                channel.basicNack(deliverTag,false,true);
                RLock lock = redissonClient.getLock(REDISSON_KEY + businessKey1);
                if (lock.tryLock()){
                    try {
                        redisTemplate.opsForValue().set(QUEUE_KEY + businessKey1, count == null ? 1 : count + 1,60, TimeUnit.SECONDS);
                    } finally {
                        lock.unlock();
                    }
                }
            } catch (IOException ex) {
                log.error("重新放入队列失败,失败原因:{}",e.getMessage(),e);
            }
            log.error("消费者出错,mq参数:{},错误信息:{}",message,e.getMessage(),e);
        }
    }

    /**
     * 处理课程表,选课记录表
     * @param businessKey1 选课表id
     * @param businessKey2 课程类型
     * @return
     */
    public boolean handleCourseDBData(String businessKey1, String businessKey2) {
        //根据课程id查询选课记录表
        XcChooseCourse xcChooseCourse = chooseCourseMapper.selectOne(new LambdaQueryWrapper<XcChooseCourse>().
                eq(XcChooseCourse::getId, businessKey1));
        if (ObjectUtils.isEmpty(xcChooseCourse)){
            log.error("根据课程ID:{}查询到的选课记录为空!",businessKey1);
            return false;
        }
        if (!businessKey2.equals("60201")){
            return false;
        }
        //修改状态为701001选课成功
        xcChooseCourse.setStatus("701001");
        int i = chooseCourseMapper.updateById(xcChooseCourse);
        if (i<1){
            log.error("更新选课记录表失败!选课ID:{}",businessKey1);
            XueChengPlusException.cast("更新选课记录表失败!");
        }
        //向课程表插入记录
        this.myCourseTables.initCourseTableData(xcChooseCourse);
        return true;
    }
}

此处消费者使用手动确认消息+异常重试机制。异常重试超过3次就丢弃消息,也可以使用手动确认消息+死信队列模式。

Q2:在配置文件开启了消费者手动确认,如果消费者拒绝签收,那么生产者的ConfirmCallback和ReturnCallBack回调方法会触发吗,这条消息会怎么样?

如果消费者拒绝签收消息(通过调用basicNack或basicReject方法),生产者的confirmCallback和returnCallback回调方法不会触发。因为消息已经被消费者处理完毕,不会再返回给生产者。

当消费者拒绝签收消息时,这条消息的处理方式取决于拒绝签收的方式和设置。具体情况如下:

1. basicNack方法:如果消费者使用`basicNack`方法拒绝签收消息,并且将`requeue`参数设置为`true`,则消息会重新放回队列,等待重新消费。如果将`requeue`参数设置为`false`,则消息会被丢弃。
   
2.basicReject方法:如果消费者使用`basicReject`方法拒绝签收消息,并且将`requeue`参数设置为`true`,则消息会重新放回队列,等待重新消费。如果将`requeue`参数设置为`false`,则消息会被丢弃。

Q3:我使用basicNack,把requeue设成了true,并且在配置文件中设置了重试次数为3次,但是因为程序中存在异常,无论重试多少次都不会成功,会怎么样?

消息会被放回队列。某种程度上手动ack和配置重试次数是互斥的。为了避免无限重试,可以设置重试n次后丢弃消息,或是放入死信队列的方式。


3、测试

生产者下单成功发送消息

消息发送到交换机,触发回调函数

消费者接收消息并且手动确认成功


消费者方手动制造一个异常

消息成功发送至队列,回调函数触发

 消费者系统错误,调用nack,消息重新放入队列。

由于程序存在错误,重试会一直失败,次数到达3次后会丢弃消息,避免无限制重试。

使用try...catch处理异常会导致spring事务失效的问题。且处理xc_choose_course,xc_course_tables表和xc_pay_record表是在两个不同的微服务中,上述写法只为演示rabbitMQ,不能保证事务问题。后续使用seata进行分布式事务控制优化。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值