돈은 차감되지만 주문은 지불되지 않고 사용자는 폭격 - 지불이 주문을 떨어 뜨리는 것을 방지하는 방법에 대해 이야기하십시오.

안녕하세요 여러분 저는 주문반복결제 방지방법에서 주문중단으로 인한 반복결제에 대해 말씀드린바 있는데 이번 글에서는 주문중단방지방법에 대해 알아보겠습니다.

잘 지불했는데 주문이 왜 떨어졌습니까?

주문하기, 주문하기, 주문 취소하기... 주문 취소란 무엇입니까?

이른바 드롭 오더(dropped order)는 사용자가 결제를 주문하고 지갑에서 결제가 완료되는 것을 의미하며, 결과적으로 사용자가 전자상거래 APP으로 복귀했을 때 주문이 여전히 미지급 상태로...

사용자는 분명히 폭발 할 것이고, 그 결과는 고객 불만 또는 나쁜 리뷰가 될 것입니다.

사용자는 속임수를 느낌

그렇다면 탈락은 어떻게 된 것일까요?

먼저 주문 결제의 전체 프로세스를 살펴보겠습니다.

지갑 결제의 전체 과정

  1. 사용자가 전자 상거래 애플리케이션에서 클릭하여 지불하고 클라이언트가 서버에 지불 요청을 시작합니다.
  2. 결제 서비스는 타사 결제 채널에 대한 결제를 시작하고 결제 채널은 해당 URL에 응답합니다.
  3. APP를 예로 들면 클라이언트는 일반적으로 해당 지갑을 꺼내고 사용자는 해당 지갑으로 이동합니다.
  4. 사용자가 지갑에서 결제 완료
  5. 사용자가 결제를 완료한 후 해당 전자상거래 APP로 다시 이동합니다.
  6. 클라이언트는 주문 서비스를 폴링하여 주문 상태를 가져옵니다.
  7. 결제 채널은 결제 서비스를 콜백하고 결제 결과를 알려줍니다.
  8. 결제 서비스는 주문 서비스에 주문 상태를 업데이트하도록 알립니다.

지불 주문의 경우 대략 다음과 같은 상태로 나눌 수 있습니다.

지불 상태

  • 미지급: 사용자가 결제를 클릭한 후, 결제 서비스가 결제 채널을 요청하기 전, 사용자는 미지급 상태입니다.
  • 결제: 사용자가 결제를 시작한 후 사용자가 결제 지갑으로 이동하여 결제를 완료하고 결제 서비스가 최종 결제 결과를 얻음 사용자의 결제가 불확실
  • 결제 성공/실패/취소/폐쇄 : 전자상거래 시스템이 최종적으로 제3자 지갑에서 사용자의 결제 최종 결과를 결정합니다.

문제가 없는 것 같은데 왜 주문이 떨어졌나요? 간단히 말해서 지불 상태가 동기화되지 않았거나 제때 동기화되지 않았습니다.

드롭 주문

  1. 결제 채널에 대한 결제 콜백

    일부 예외가 발생하여 결제 서비스가 결제 채널에서 콜백 알림을 수신하지 못했습니다.

  2. 결제 서비스 알림 주문 서비스

    서비스 내부에 예외가 발생하여 결제 상태가 주문 서비스와 동기화되지 않음

  3. 클라이언트가 주문 상태를 가져옵니다.

    클라이언트는 일반적으로 폴링을 통해 상태를 얻으며 폴링 시간 내에 주문 상태를 얻지 못할 수 있으며 결과적으로 사용자는 결제가 완료되지 않은 것으로 보게 됩니다.

그 중 1은 외부 방울이라고 할 수 있고 2와 3은 내부 방울이라고 할 수 있습니다.

다음으로 주문 누락 문제를 방지하는 방법을 살펴보겠습니다.

내부 드롭 오더를 방지하는 방법

시스템 내부의 드롭 오더부터 시작해 보겠습니다.물론 시스템 내에서 안정성을 보장하기 쉽고 드롭 오더의 확률은 여전히 ​​상대적으로 낮습니다.

주문 중단을 방지하기 위한 서버 측

결제 서비스와 주문 서비스 간의 주문 누락을 방지하는 핵심은 최대한 결제 알림 주문 결제 결과가 성공하도록 하는 것인데, 우리는 일반적으로 이 두 가지 방법을 사용합니다.

주문 중단을 방지하기 위한 서버 측

  1. 동기 호출 재시도 메커니즘

    결제 서비스가 주문 서비스를 호출할 때 네트워크 지터의 경우 호출 실패를 방지하기 위해 실패를 재시도해야 합니다.

  2. 비동기 메시지의 안정적인 전달

    동기화가 안전하지 않은 경우 다른 비동기식을 추가하십시오. 결제 서비스는 결제 성공 메시지를 전달하고 주문 서비스는 결제 성공 메시지를 소비합니다. 모든 프로세스는 가능한 한 신뢰할 수 있어야 합니다. 예를 들어 주문 서비스는 주문 상태 업데이트를 완료한 후 메시지 소비 완료를 확인해야 합니다.

동기 + 비동기 양손 전략은 기본적으로 서버의 내부 주문 하락을 방지할 수 있습니다.

일관된 상태를 보장하기 위해 분산 트랜잭션(트랜잭션 메시지, Seata)의 도입은 필요하지 않다고 생각합니다.

고객이 주문을 중단하는 것을 방지하는 방법

사용자의 결제가 완료된 후 전자 상거래 시스템으로 다시 이동하면 클라이언트가 주문 상태를 폴링합니다.보통 2~3초 이내에 주문 결제 결과가 나타납니다. 이 과정에서 발생하는 문제는 매우 적습니다.

다만, 클라이언트가 일정 시간 동안 폴링을 하고 결과를 얻지 못한 경우가 적은 확률로 폴링을 종료하고 사용자에게 미지급금을 보여줄 수 밖에 없는 경우도 배제할 수 없다.

이 경우 문제는 일반적으로 서버 측에 있으며 주문 상태가 제 시간에 업데이트되지 않습니다. 가장 중요한 것은 서버 측에서 삭제 된 주문을 처리하여 서버 측이 상태를 동기화 할 수 있는지 확인하는 것입니다 시간에 지불 순서의.

다만, 일단 서버측의 주문 상태가 변경되면, 사용자가 미지급 대금을 상시 볼 수 없도록 최대한 클라이언트측에 동기화시켜야 한다.

클라이언트와 서버 간의 동기화 상태는 푸시 및 풀에 불과합니다.

  1. 클라이언트 폴링

    클라이언트는 사용자가 지불하지 않았다고 판단한 후 일반적으로 주문을 카운트다운합니다.

    카운트다운

    여기서 다시 언급하시겠습니까? 이 카운트다운이 어떻게 달성되었다고 생각합니까? 순수 클라이언트 그룹 구성 요소 카운트다운?

    ——물론 그렇지 않습니다. 일반적으로 클라이언트 구성 요소는 카운트다운을 하고 주기적으로 서버에 카운트다운 시간을 확인하도록 요청합니다. 또한 이 경우 클라이언트도 결제 상태를 확인할 수 있습니다.

  2. 서버 푸시

    엄밀히 따지면 서버측 푸시는 아주 좋은 솔루션인것 같습니다.웹소켓은 웹측에서 사용가능하고 커스텀 푸시는 APP측에서 사용가능합니다. 웹 실시간 메시지를 구현하기 위한 7가지 솔루션이 있음을 알 수 있습니다. 푸시, 7 종류! . 그러나 실제로는 푸시의 성공률이 이상적이지 않은 경우가 많습니다.

외부 드롭 오더를 방지하는 방법

내부 드랍 오더에 비해 외부 드랍 오더의 확률이 훨씬 높으며, 결국 외부 채널과 관련하여 제어할 수 없는 요인이 더 많습니다.

외부 주문이 떨어지는 것을 방지하기 위해 핵심은 네 단어입니다. " 主动查询", 제 3자의 콜백 알림을 기다리는 경우 위험은 여전히 ​​상대적으로 크며 결제 서비스는 제 3자의 결제 상태를 적극적으로 확인해야 합니다. 이상이 있으면 도착 시간에 감지할 수 있습니다.

주로 두 가지 형식의 활성 쿼리:

예약된 작업 쿼리

의심 할 여지없이 가장 간단한 것은 예약 된 작업, 지불 서비스, 일정 支付中기간 내에 정기적으로 지불 주문을 쿼리하고 타사 채널에서 지불 결과를 쿼리하고 쿼리가 최종 상태에 도달 한 후 지불 주문 상태를 업데이트하고 주문 서비스에 통지:

정기적으로 지불 상태 확인

구현도 매우 간단합니다.xxl-job과 같은 시간 제한 작업 프레임워크를 사용하여 테이블을 정기적으로 스캔하고 타사에 쿼리합니다.대략적인 코드는 다음과 같습니다.

    @XxlJob("syncPaymentResult")
    public ReturnT<String> syncPaymentResult(int hour) {
    
    
        //……
        //查询一段之间支付中的流水
        List<PayDO> pendingList = payMapper.getPending(now.minusHours(hour));
        for (PayDO payDO : pendingList) {
    
    
            //……
            // 主动去第三方查
            PaymentStatusResult paymentStatusResult = paymentService.getPaymentStatus(paymentId);
            // 第三方支付中
            if (PaymentStatusEnum.PENDING.equals(paymentStatusResult.getPayStatus())) {
    
    
                continue;
            }
            //支付完成,获取到终态
            //……
            // 1.更新流水
            payMapper.updatePayDO(payDO);
            // 2.通知订单服务
            orderService.notifyOrder(notifyLocalRequestVO);
        }
        return ReturnT.SUCCESS;
    }

예약된 작업의 가장 큰 장점은 확실히 단순하지만 몇 가지 문제도 있습니다.

  1. 쿼리 결과는 실시간이 아닙니다.

    예약된 작업의 빈도 설정은 항상 결정하기 어려운 일입니다. 짧은 간격은 데이터베이스에 많은 부담을 주고, 긴 간격은 실시간이 아니기 때문에 발생하기 쉽습니다. 위에서 언급한 사용자가 반환합니다. APP에 전송하고 결과는 결제 성공 상태를 폴링할 수 없다는 것입니다.

    실제로 사용자가 지갑으로 이동한 후 결제가 빠르게 완료되는 경우가 많으며, 짧은 시간 내에 결제가 완료되지 않으면 일반적으로 다시 결제되지 않습니다. 따라서 사실 결제 시작부터 제3자에게 결제 결과를 조회하는 빈도를 줄여야 합니다.

  2. 데이터베이스에 대한 압박

    테이블을 스캔하기 위해 예약된 작업은 확실히 데이터베이스에 부담을 줍니다. 테이블을 스캔할 때 데이터베이스 모니터링에서 작은 스파이크를 종종 볼 수 있습니다. 데이터 양이 많을 경우 영향이 더 클 수 있습니다.

    별도의 지불 흐름 측정기를 만들고 예약된 작업으로 이 테이블을 스캔하고 최종 지불 상태를 얻은 후 해당 레코드를 삭제할 수 있습니다.

지연된 메시지 쿼리

시간제 작업에 문제가 있는데 다른 방법이 없을까요? 답은 지연된 메시지입니다.

지연된 메시지 쿼리 결제 상태

  • 결제를 시작한 후 지연된 메시지를 보낸다 앞서 언급한 바와 같이 사용자는 지갑으로 점프하여 보통 빠르게 결제하므로 결제 상태를 조회하는 단계가 이 규칙을 따르기를 바라며 결제가 완료되기를 바랍니다. 10s, 30s, 1min, 1min30s, 2min, 5min, 7min... 이 빈도는 결제 주문 상태를 쿼리하는 데 사용됩니다. 여기에서 대기열 구조를 사용하여 다음 쿼리의 시간 간격을 저장할 수 있습니다.

    대략적인 코드는 다음과 같습니다.

            //……
            //控制查询频率的队列,时间单位为s
            Deque<Integer> queue = new LinkedList<>();
            queue.offer(10);
            queue.offer(30);
            queue.offer(60);
            //……
            //支付订单号
            PaymentConsultDTO paymentConsultDTO = new PaymentConsultDTO();
            paymentConsultDTO.setPaymentId(paymentId);
            paymentConsultDTO.setIntervalQueue(queue);
            //发送延时消息
            Message message = new Message();
            message.setTopic("PAYMENT");
            message.setKey(paymentId);
            message.setTag("CONSULT");
            message.setBody(toJSONString(paymentConsultDTO).getBytes(StandardCharsets.UTF_8));
            try {
          
          
                //第一个延时消息,延时10s
                long delayTime = System.currentTimeMillis() + 10 * 1000;
                // 设置消息需要被投递的时间。
                message.setStartDeliverTime(delayTime);
                SendResult sendResult = producer.send(message);
                //……
            } catch (Throwable th) {
          
          
                log.error("[sendMessage] error:", th);
            }
    

    추신: 여기에서는 모든 수준의 지연 메시지를 지원하는 RocketMQ 클라우드 서버 버전이 사용됩니다. RocketMQ의 오픈 소스 버전은 고정 수준 지연 메시지만 지원합니다. 강력한 개발팀은 오픈 소스를 기반으로 2차 개발을 수행할 수 있습니다.

  • 지연된 메시지를 소비한 후 제3자에게 지불 주문의 상태를 쿼리합니다.결제가 아직 진행 중이면 계속해서 다음 지연된 메시지를 보내고 대기열 구조에서 지연 간격을 가져옵니다. 최종 상태를 얻으면 지불 주문 상태를 업데이트하고 주문 서비스에 알립니다.

    @Component
    @Slf4j
    public class ConsultListener implements MessageListener {
          
          
        //消费者注册,监听器注册
        //……
      
        @Override
        public Action consume(Message message, ConsumeContext context) {
          
          
            // UTF-8解析
            String body = new String(message.getBody(), StandardCharsets.UTF_8);
            PaymentConsultDTO paymentConsultDTO= JsonUtil.parseObject(body, new TypeReference<PaymentConsultDTO>() {
          
          
            });
            if (paymentConsultDTO == null) {
          
          
                return Action.ReconsumeLater;
            }
            //获取支付流水
            PayDO payDO=payMapper.selectById(paymentConsultDTO.getPaymentId());
            //……
            //查询支付状态
            PaymentStatusResult paymentStatusResult=payService.getPaymentStatus(paymentStatusContext);
            //还在支付中,继续投递一个延时消息
            if (PaymentStatusEnum.PENDING.equals(paymentStatusResult.getPayStatus())){
          
          
                //发送延时消息
                Message msg = new Message();
                message.setTopic("PAYMENT");
                message.setKey(paymentConsultDTO.getPaymentId());
                message.setTag("CONSULT");
               //下一个延时消息的频率
                Long delaySeconds=paymentConsultDTO.getIntervalQueue().poll();        message.setBody(toJSONString(paymentConsultDTO).getBytes(StandardCharsets.UTF_8));
                try {
          
          
                    Long delayTime = System.currentTimeMillis() + delaySeconds * 1000;
                    // 设置消息需要被投递的时间。
                    message.setStartDeliverTime(delayTime);
                    SendResult sendResult = producer.send(message);
                    //……
                } catch (Throwable th) {
          
          
                    log.error("[sendMessage] error:", th);
                }
                return Action.CommitMessage;
            }
            //获取到最终态
            //更新支付订单状态
            //…… 
            //通知订单服务
            //……
            return Action.CommitMessage;
        }
    }
    

    시간이 지정된 폴링 방식과 비교하여 지연된 메시지 방식은 다음과 같습니다.

    • 더 나은 적시성
    • 테이블을 스캔할 필요가 없고 데이터베이스에 대한 부담이 적습니다.

    하지만 보시다시피 여기에서 구현한 것은 유료 버전의 RocketMQ를 사용하는 것이므로 너무 복잡해 보이지는 않지만 오픈 소스 솔루션을 사용하면 그렇게 간단하지 않습니다.

    값을 지불하라

발문

이 기사는 사용자를 짜증나게 하고, 고객 서비스를 짜증나게 하며, 개발자들이 머리를 긁적이게 만드는 문제를 소개합니다. 즉, 주문이 중단된 이유와 주문 중단을 방지하는 방법을 포함합니다.

그 중 내부 드랍 오더의 확률은 상대적으로 낮으며, 드랍 오더의 주요 원인은 소위 외부 드랍 오더입니다.

외부 drop order 솔루션의 핵심은 主动查询일반적으로 사용되는 두 가지 솔루션이 있다는 것입니다 . 전자는 더 간단 定时任务查询하고 延时消息查询후자는 더 기능적입니다.



인용하다:

[1]. 주문불량 이상에 대한 가장 완벽한 솔루션

[2]. 주문 누락 문제 해결


⭐슬래그 역습 시리즈:

⬇️⬇️⬇️를 팔로우하고 " 666 " 에 답장을 하면 700페이지가 넘는 독점 인터뷰 매뉴얼과 오리지널 인터뷰 매뉴얼을 받아가세요!

슬래그 역습 매뉴얼

추천

출처blog.csdn.net/sinat_40770656/article/details/126623821