こんにちは、三男ですリピート決済を防ぐ方法 でオーダー取り下げによるリピート決済についてお話しましたが、今回は取り下げを防ぐ方法についてお話しましょう。
十分に支払いましたが、なぜ注文が落ちたのですか?
注文、購入、注文の取り消しについて聞いたことがあります... 注文の取り下げとは何ですか?
いわゆるドロップオーダーとは、ユーザーが支払いを注文し、支払いがウォレットで完了したことを意味し、その結果、ユーザーが電子商取引アプリに戻ると、注文はまだ未払いです...
ユーザーが間違いなく爆破することは間違いなく、結果は顧客のクレームか悪いレビューのどちらかです。
では、ドロップアウトはどのようにして発生したのでしょうか。
まず、注文の支払いの完全なプロセスを見てみましょう。
- ユーザーが e コマース アプリケーションから支払いをクリックし、クライアントがサーバーへの支払い要求を開始します。
- 支払いサービスはサードパーティの支払いチャネルへの支払いを開始し、支払いチャネルは対応する URL に応答します。
- APP を例にとると、クライアントは通常、対応するウォレットをプルアップし、ユーザーは対応するウォレットにジャンプします。
- ユーザーはウォレットで支払いを完了します
- ユーザーが支払いを完了したら、対応する電子商取引アプリに戻ります
- クライアントは注文サービスをポーリングして注文ステータスを取得します
- 決済チャネルが決済サービスにコールバックし、決済結果を通知
- 支払いサービスは、注文サービスに注文ステータスを更新するよう通知します
支払指図の場合、大まかに以下の状態に分けられます。
- 未払い: ユーザーがクリックして支払いを行った後、決済サービスが支払いチャネルを要求する前に、ユーザーは未払いの状態になります
- 支払い: ユーザーが支払いを開始した後、ユーザーは支払いウォレットにジャンプし、支払いを完了し、支払いサービスは最終的な支払い結果を取得します. ユーザーの支払いは不確実です.
- 支払いの成功/失敗/キャンセル/終了: 電子商取引システムは、サードパーティのウォレットでのユーザーの支払いの最終結果を最終的に決定します
問題ないようですが、なぜ注文が落ちたのですか?簡単に言えば、支払いのステータスが同期されていないか、時間内に同期されていません。
-
支払いチャネルの支払いコールバック
なんらかの例外が発生したため、支払いサービスが支払いチャネルからのコールバック通知を受信できませんでした
-
決済サービス通知注文サービス
サービス内で例外が発生し、支払い状況が注文サービスに同期されない
-
クライアントは注文ステータスを取得します
クライアントは通常、ポーリングによってステータスを取得しますが、ポーリング時間内に注文ステータスを取得できない場合があり、その結果、ユーザーは支払いが支払われていないことがわかります。
このうち、1は外部ドロップ、2と3は内部ドロップと呼べる。
次に、取りこぼし問題を未然に防ぐ方法を見ていきましょう。
内部ドロップオーダーを防ぐ方法
まず、システム内の注文の取り下げから始めましょう. もちろん、システム内では、安定性を確保するのは簡単であり、注文の取り下げの可能性はまだ比較的小さいです.
注文の取りこぼしを防ぐためのサーバー側
決済サービスと注文サービスの間で注文の取りこぼしを防ぐには、決済通知の注文決済結果をできるだけ成功させることが重要であり、通常はこの 2 つの方法を使用します。
-
同期呼び出し再試行メカニズム
支払いサービスが注文サービスを呼び出すとき、ネットワーク ジッターの場合に呼び出しが失敗しないように、失敗を再試行する必要があります。
-
非同期メッセージの信頼できる配信
同期は安全ではありません。別の非同期を追加してください。支払いサービスは支払い成功メッセージを配信し、注文サービスは支払い成功メッセージを消費します. プロセス全体は可能な限り信頼できる必要があります. たとえば, 注文サービスは注文ステータスの更新が完了した後にメッセージ消費の完了を確認する必要があります.
同期+非同期の両手戦略は、基本的にサーバーの内部注文落ちを防ぐことができます。
一貫した状態を確保するための分散トランザクション (トランザクション メッセージ、Seata) の導入については、必要ないと思います。
クライアントが注文をドロップしないようにする方法
ユーザーの支払いが完了したら、電子商取引システムに戻り、クライアントが注文のステータスをポーリングします. 通常、2 ~ 3 秒以内に、注文の支払いの結果が得られます.このプロセスの問題は非常に低いです。
ただし、わずかな可能性で、クライアントが一定期間ポーリングを行い、結果を取得していない可能性も否定できません。そのため、ポーリングを終了して未払いの支払いをユーザーに表示することしかできません。
この場合、問題は通常サーバー側にあり、注文のステータスは時間内に更新されません. 最も重要なことは、サーバー側で取り下げられた注文に対処して、サーバー側がステータスを同期できるようにすることです.時間内の支払い注文の。
ただし、サーバー側の注文ステータスが変更されると、ユーザーが常に未払いの支払いを見ることができないように、可能な限りクライアント側に同期する必要があります。
クライアントとサーバー間の同期状態は、プッシュとプルにすぎません。
-
クライアントのポーリング
クライアントは、ユーザーが支払いをしていないと判断した後、通常、注文をカウントダウンします。
ここでもう一度言及しますか?このカウントダウンはどのように達成されたと思いますか? 純粋なクライアント グループ コンポーネントのカウントダウン?
——そうではありません。通常、クライアント コンポーネントはカウント ダウンし、定期的にサーバーにカウント ダウン時間をチェックするように要求します。また、この場合、クライアントは支払い状況も確認できます。
-
サーバープッシュ
真剣に, サーバー側のプッシュは非常に良いソリューションのようです. Websocket は Web 側で使用できます.カスタムプッシュは APP 側で使用できます. Web リアルタイムメッセージを実現するための 7 つのソリューションがあることがわかります.一押し、7種類!. しかし実際には、プッシュの成功率はしばしば理想的とは言えません。
外部ドロップオーダーを防ぐ方法
内部ドロップオーダーと比較して、外部ドロップオーダーの確率ははるかに高く、結局のところ、外部チャネルとの接続には制御不能な要素が多くあります。
外部からの注文が取り消されるのを防ぐために、コアは次の 4 つの単語です。「主动查询
」、サード パーティのコールバック通知を待つだけの場合、リスクは依然として比較的大きく、支払いサービスはサード パーティからの支払いステータスを積極的に確認する必要があります。異常がある場合は、到着時に感知できます。
主に 2 つの形式のアクティブなクエリ:
スケジュールされたタスクのクエリ
間違いなく、最も単純なのは間違いなくスケジュールされたタスク、支払いサービスで支付中
あり、一定期間内に支払い注文を定期的にクエリし、サードパーティチャネルから支払い結果をクエリし、クエリが最終状態に達した後、支払い注文のステータスを更新し、注文サービスに通知します。
実装も非常にシンプルです. 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;
}
スケジュールされたタスクの最大の利点は間違いなくシンプルですが、いくつかの問題もあります。
-
クエリの結果はリアルタイムではありません
スケジュールされたタスクの頻度の設定は、常に決定するのが難しいものです. 短い間隔はデータベースに大きな負荷をかけ、長い間隔はリアルタイムではなく、発生しやすい.その結果、支払いの成功ステータスをポーリングできません。
実際、ユーザーがウォレットにジャンプした後、通常はすぐに支払いが完了しますが、短期間で支払いが完了しない場合、通常は再度支払われることはありません。したがって、実際には、支払いの開始から、第三者から支払い結果を照会する頻度は減少するはずです。
-
データベースへのプレッシャー
テーブルをスキャンするスケジュールされたタスクは、間違いなくデータベースに負荷をかけます.テーブルをスキャンすると、データベースの監視に小さなスパイクが見られることがよくあります.データ量が多い場合、影響は大きくなる可能性があります.
別の支払いフロー メーターを作成し、スケジュールされたタスクでこのテーブルをスキャンし、支払いの最終状態を取得した後に対応するレコードを削除できます。
遅延メッセージ クエリ
時限タスクにはいくつか問題があるので、他の方法はありますか? 答えは遅延メッセージです。
-
決済開始後、遅延メッセージを送信する 前述のとおり、ユーザーはウォレットにジャンプし、通常はすぐに支払いを行うため、決済状況を問い合わせるステップはこのルールに準拠していることを願っていますので、決済が完了することを願っています。 within 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); }
PS: ここでは、あらゆるレベルの遅延メッセージをサポートする RocketMQ クラウド サーバー バージョンを使用します. RocketMQ のオープン ソース バージョンは、固定レベルの遅延メッセージのみをサポートします. 強力な開発チームは、オープンソースに基づいて二次開発を行うことができます。
-
遅延メッセージを消費した後、第三者からの支払い注文のステータスを照会します。支払いがまだ進行中の場合は、次の遅延メッセージの送信を続行し、遅延間隔はキュー構造から取得されます。最終状態が得られたら、支払い注文ステータスを更新し、注文サービスに通知します。
@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 を使用するため、それほど複雑には見えませんが、オープン ソース ソリューションを使用すると、それほど単純ではありません。
エピローグ
この記事では、ユーザーを悩ませ、顧客サービスを悩ませ、開発者を困惑させる問題 - 注文の取り下げについて、取り下げの理由と取り下げの防止方法を含めて紹介します。
その中で、内部ドロップオーダーの確率は比較的小さく、ドロップオーダーの主な理由は、いわゆる外部ドロップオーダーです。
外部ドロップ オーダー ソリューションの重要なポイントは、主动查询
一般的に使用される 2 つのソリューションがあることです。前者はよりシンプル定时任务查询
で延时消息查询
、後者はより機能的です。
参照する:
[2].欠品問題を解決する
⭐スラグ反撃シリーズ:
- クズの逆襲: Javaの基礎に関する53の質問
- Slag Counterattack: Java コレクションに関する 30 の質問
- フェイススカムの反撃: JVM の古典的な 50 の質問、これで面接は安定しました!
- Slag Counterattack: Java の並行性に関する 60 の質問
- ミアンスラッグの逆襲:春 35問、40,000語+50枚の写真で詳しく解説!
- ミアンスラッグの逆襲:22枚の写真、8,000の単語、20の質問、完全にMyBatisを手に入れよう!
- ミアン・スラグの逆襲: コンピュータ・ネットワークに関する 62 の質問 クイックコレクション!
- インタビュー バイト、オペレーティング システムから要求されて電話を切った
- スラグの反撃: RocketMQ 23 の質問
⬇️⬇️⬇️をフォローして「666」と返信すると、700ページ以上の独占・オリジナルインタビューマニュアルがもらえる!