How to prevent payment slip?

Java Assault website: http://www.susan.net.cn, strongly recommended collection.

I paid well, why did I drop the order?

I have heard of placing an order, buying an order, and canceling an order... What is a dropped order?

The so-called lost order means that the user places an order and pays, completes the payment in the wallet, and returns to the e-commerce app to see that the order is still unpaid...

There is no doubt that users will explode, and the result is either customer complaints or bad reviews.

Users feel cheated

So how did the order drop come about?

Let's take a look at the complete process of order payment:

The complete process of wallet payment

  1. The user clicks to pay from the e-commerce application, and the client initiates a payment request to the server

  2. The payment service will initiate payment to the third-party payment channel, and the payment channel will respond to the corresponding url

  3. Taking APP as an example, the client usually pulls up the corresponding wallet, and the user jumps to the corresponding wallet

  4. The user completes the payment in the wallet

  5. After the user completes the payment, jump back to the corresponding e-commerce APP

  6. The client polls the order service to obtain the order status

  7. The payment channel calls back the payment service to notify the payment result

  8. The payment service notifies the order service to update the order status

For payment orders, it can be roughly divided into the following states:

Payment status

  • Unpaid: After the user clicks to pay but before the payment service requests the payment channel, the user is in an unpaid state

  • Payment: After the user initiates the payment, jumps to the payment wallet, and then completes the payment, the payment service obtains the final payment result, and belongs to the payment state. In this state, it can be said to be a foggy state. The user's payment is indeterminate

  • Payment success/failure/cancellation/close: the e-commerce system finally determines the final result of the user's payment in the third-party wallet

It seems to be ok, why did you drop the order? Simply put, the status of the payment is not synchronized, or not synchronized in time.

Lost orders occur

  1. Payment callback of payment channel

    Some exception occurred, resulting in the payment service not receiving the callback notification from the payment channel

  2. Payment Service Notification Order Service

    An exception occurred inside the service, resulting in the payment status not being synchronized to the order service

  3. The client gets the order status

    The client usually polls to obtain the status, and may not obtain the order status within the polling time, and the user sees that the payment has not been made

Among them, 1 can be called external orders, and 2 and 3 can be called internal orders.

Next, let's see how to prevent the problem of dropping orders.

How to prevent internal order drop

Let's start with the order drop within the system. Of course, within the system, stability is easier to guarantee, and the probability of order drop is relatively small.

The server prevents order dropping

The key to preventing dropped orders between the payment service and the order service is to ensure that the payment result of the payment notification order is as successful as possible. We generally use these two methods.

The server prevents order dropping

  1. Synchronous call retry mechanism

    When the payment service calls the order service, it needs to retry the failure to prevent the call from failing due to network jitter.

  2. Reliable delivery of asynchronous messages

    If synchronization is not secure, then add another asynchrony. The payment service delivers a payment success message, and the order service consumes the payment success message. The whole process must ensure reliability as much as possible. For example, the order service must complete the message consumption after completing the order status update.

The synchronous + asynchronous two-handed strategy can basically prevent internal orders from the server.

As for the introduction of distributed transactions (transaction messages, Seata) to ensure consistent state, I don't think it is necessary.

How does the client prevent dropped orders

After the user completes the payment and jumps back to the e-commerce system, the client will poll the status of the order. Usually, within two to three seconds, the result of the payment of the order will be obtained. The probability of problems in this process is very low.

However, it is not ruled out that in a very small probability, the client polls for a period of time and does not get a result, so it can only end the polling and show the user that the payment has not been made.

In this case, the problem usually lies in the server, which fails to update the status of the order in time. The most important thing is to deal with the order drop on the server to ensure that the server can synchronize the status of the payment order in time.

However, once the order status of the server changes, it should be synchronized to the client as much as possible, so that the user cannot always see the outstanding payment.

Between the client and the server, the synchronization status is nothing more than push and pull:

  1. client polling

    After the client judges that the user has not paid, it usually counts down the order.

    countdown

    Here again? How do you think this countdown is achieved? Pure client group component countdown?

    ——Certainly not, usually the client component counts down, periodically requests to the server, and checks the countdown time. Similarly, in this case, the client can also check the payment status.

  2. server push

    To be honest, server-side push seems to be a very beautiful solution. Websocket can be used on the Web side, and custom Push can be used on the APP side. You can see that I have 7 solutions to realize web real-time message push, 7 kinds ! . But in reality, the success rate of pushing is often not so ideal.

How to prevent external drop orders

Compared with internal orders, the probability of external orders is much higher. After all, there are more uncontrollable factors in the connection with external channels.

To prevent external orders from being dropped, the core is four words: " 主动查询". If you just wait for the callback notification from the third party, the risk is still relatively high. The payment service should actively query the payment status from the third party, and even if there is any abnormality, it can be sensed in time. arrive.

There are two main forms of active query:

Timed task query

Undoubtedly, the simplest thing is definitely a scheduled task, payment service, regularly query 支付中payment orders within a period of time, query payment results from third-party channels, and after querying the final state, update the status of the payment order and notify the order service:

Regularly check the payment status

The implementation is also very simple, just use a scheduled task framework such as xxl-job, scan the table regularly, and query the third party. The approximate code is as follows:

    @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;
    }

The biggest benefit of timing tasks is definitely simplicity, but it also has some problems:

  1. The result of the query is not real-time

    The setting of the timing task frequency is always a difficult thing to determine. A short interval will put a lot of pressure on the database, and a long interval will not be real-time. It is easy to happen. The above-mentioned situation where the user returns to the APP, but the payment success status cannot be polled.

    In fact, after the user jumps to the wallet, the payment is usually completed quickly. If the payment is not completed within a short time, then the payment will generally not be made again. So in fact, starting from the initiation of the payment, the frequency of querying the payment result from the third party should be decreasing.

  2. pressure on the database

    Scheduled tasks to scan the table will definitely put pressure on the database. When scanning the table, you will often see a small spike in the monitoring of the database. If the amount of data is large, the impact may be even greater.

    You can create a payment flow table separately, scan this table with a scheduled task, and delete the corresponding record after obtaining the final state of payment.

Delayed message query

There are some problems with timing tasks, so is there any other way? The answer is delayed messages.

Delayed message query payment status

  • After the payment is initiated, a delay message is sent. As mentioned earlier, the user jumps to the wallet and usually pays soon, so we hope that the step of querying the payment status conforms to this rule, so we hope that it will be in 10s, 30s, 1min, 1min30s , 2min, 5min, 7min... This frequency is used to query the status of the payment order. Here we can use a queue structure to store the time interval of the next query.

    The approximate code is as follows:

            //……
            //控制查询频率的队列,时间单位为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);
            }
    

    PS: The RocketMQ cloud server version is used here, which supports any level of delayed messages. The open source version of RocketMQ only supports fixed-level delayed messages, and it has to be recharged to become stronger. A strong development team can carry out secondary development on the basis of open source.

  • After consuming the delayed message, query the status of the payment order from the third party. If the payment is still in progress, continue to send the next delayed message. The delay interval is taken from the queue structure. If the final state is obtained, update the payment order status and notify the order service.

    @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;
        }
    }
    

    Compared with the regular polling scheme, the delay message scheme:

    But you can also see that my implementation here uses the money-charging version of RocketMQ, so it doesn't look too complicated, but if you use an open source solution, it's not that simple.

    It can be solved by charging money

    • better timeliness

    • No need to scan tables, less pressure on the database

epilogue

This article introduces a problem that irritates users, annoys customer service, and makes developers scratch their heads—order drop, including why it happens and how to prevent it.

Among them, the probability of internal order loss is relatively small, and the main reason for order loss is the so-called external order loss.

The key point of solving external order drop is that 主动查询there are two commonly used solutions: 定时任务查询and 延时消息查询, the former is simpler, and the latter is more functional.


reference:

  • [1]. The most complete solution to the abnormal payment order

  • [2]. Solve the problem of missing payment orders

Guess you like

Origin blog.csdn.net/qq_45635347/article/details/131461082