著者のWeChatパブリックアカウントをフォローすることを歓迎します:プログラミングココナッツ
決済センターの存在の目的
決済センターシステムは、社内で各事業分野に統一された決済・返金・その他のサービスを提供し、外部で第三者決済や銀行サービスと連携して資金の流れを実現しています。以下に示すように:
ほとんどの企業は基本的にこの構造を持っており、次の利点があります。
- 統一された支払いサービスを形成して、ビジネスラインへのアクセスコストと反復的な研究開発コストを削減します。
- 会社のビジネスの急速な発展のための条件を提供するための革新的なビジネスのためのより良いそしてより速いサポート。
- 安全で安定したスケーラブルな支払いシステムを構築するのに役立ちます。
- これは、コア決済データの沈殿と統一された利用に役立ちます。
支払いプロセス
上の図は、ユーザー支払いの主なプロセスを示しています。これは3つのステップに分かれています。
- ユーザーは、ビジネス注文確認ページのレジ係ページを呼び出します。
- ユーザーは、キャッシャーページで支払い方法を選択し、支払いを確認し、サードパーティの支払いページを表示し、パスワードを入力して、実際の支払い動作を実行します。
- システムはユーザーの支払い結果を処理し、ユーザーとさまざまな関連システムに通知します。
3つのステップの詳細を以下に示します。
1.商人のキャッシャーを呼び起こす
- ユーザーは、注文確認ページの[支払いに移動]ボタンをクリックして、レジ係の支払い注文インターフェースを呼び出します。
- キャッシャーは注文情報をキャッシュして保存し、注文IDをキャッシャーのURLにアセンブルして、注文システムに返します。
- 注文システムはレジ係の住所を受け取り、レジ係のページにジャンプします。
上の写真は、2つのビジネスライン(風光明媚なビジネスライン、ホテルのビジネスライン)によって引き起こされるキャッシャーページを示しています。これらは大きく3つの領域に分けることができます。
ページの上部には、支払いの残り時間と未払い額が表示されます。
真ん中の部分は注文情報であり、レジ係が定義したデータ形式に従ってビジネスラインによって動的に送信されます。
残りの部分では、支払チャネルを示します。支払チャネルは、支払バックグラウンド管理システムのビジネスラインによって、それぞれのニーズに応じて構成されます。必要な支払方法とその順序をカスタマイズできます。
2.ユーザーが支払いを確認する
- ユーザーは、キャッシャーページで支払い方法(Alipay支払い、WeChat支払い、銀行カード支払いなど)を選択し、[今すぐ支払う]ボタンをクリックします。
- ペイメントセンターの注文作成インターフェースを呼び出すと、ペイメントセンターはサードパーティのペイメント作成インターフェースを呼び出し、支払い情報を同期的に返します。ペイメントセンターは、返されたパラメーターを処理してキャッシャーに返します。
- キャッシュレジスターは、ペイメントセンターから返されたパラメーターを運び、スリーパーティインターフェイスを呼び出し、スリーパーティキャッシュレジスターを呼び出します。
- ユーザーはパスワードを入力し、すぐに支払います。
3.支払い結果の処理
- 三者間システムは控除処理を実行し、キャッシャーで結果を返します(現在、WeChat支払いは支払いを返し、Alipayは最終的な支払いステータス(支払い成功または支払い失敗)を返します)、
次の手順は、順不同で非同期に実行されます。
- 收银台拿到三方返回的结果,确认用户已经支付,则分配定时任务轮询查询(注意超时时间)后台支付结果,拿到终态之后跳转到相应页面,
- 三方系统支付成功后会通知支付中心结果,支付中心做好自身逻辑处理,异步通知订单系统,然后返回三方系统通知结果,
- 如果长时间未收到三方支付结果的通知,为了防止掉单,支付中心会发起主动查询来获取支付最终结果,以保证支付结果的及时更新。
- 当然业务线订单系统为了防止支付系统出现异步通知问题,也可以定时轮询支付中心的支付状态,防止掉单。(图中未画)
支付中心系统一些问题及解决方案
1. 支付订单超时关闭问题
如果用户长时间没有支付,一般都会有一个超时时间(如上图商户收银台的支付剩余时间),到达这个超时时间支付单会自动关闭。实现此需求有很多方式,比如:
1. 轮询 DB
定时轮询DB,取出达到超时时间且在支付中的数据,然后执行关闭逻辑。
缺点:1. 存在延迟,取决于定时任务的频率。2. 影响数据库性能。
2. JDK 延时队列(DelayQueue)和时间轮算法
这两种的算法的实现方式自行搜索。
共同的缺点是 1. 数据易丢失,由于数据存储在内存中,服务重启后数据全部消失。2. 有内存限制,如果数据量过大,会出现OOM异常。
3. RocketMQ 延时队列
RocketMQ 支持消息延时发送,社区版不支持任意等级的延迟,目前默认支持18个延时等级:
1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
复制代码
比如支付单30分钟过期,在支付单创建成功后发送延迟消息(延时等级为 16),消费者在30分钟后会拉取到该消息然后执行关闭逻辑。
RocketMQ 延时队列,无论在数据安全性和及时性都有明显的优势,但是目前社区版没有支持任意级别的延迟。
目前我们使用的是 RocketMQ 延时队列实现的订单关闭。
2. 保证支付结果实时性
三方支付系统支付成功后99.9%的情况下都会回调通知我们,但也难免有意外,比如三方延迟回调或者三方系统宕机,为了保证支付结果的实时性,三方支付也要求我们不能完全依赖于回调接口,所以我们需要定时的调用主动查询接口来查询三方的支付结果。这里我们也是使用的 RocketMQ 延时队列实现的:
- 调用三方支付创单成功后,发送<支付主动查询>延时MQ消息。
- 消费消息,判断支付状态是否到达终态,如果到达终态,则返回处理成功,否则调用三方支付查询接口,如果支付成功则处理成功业务,返回处理成功。
- 如果客户未支付则判断是否达到最大的重试次数,如果达到最大重试次数则停止<支付主动查询>的重试,否则解析重试规则,发送下一轮的延时消息。
有三个重要参数,这些参数可以放到配置中心或者配置库中,
// 初始延迟级别,对应RocketMQ延时等级,比如3对应的延时时间就是10s
private Integer queryInitLevel = 3;
// 重试次数
private Integer queryCount = 6;
// 重试级别,对应RocketMQ延时等级,5s,10s,30s,1m,10m,20m
private String queryDelayLevels = 2,3,4,5,14,15;
复制代码
支付创单成功后发送延时消息:
public void payQueryTask(String orderNo) {
PayQueryMessage payQueryMessage = new PayQueryMessage();
payQueryMessage.setOrderNo(orderNo);
RetryMessage<PayQueryMessage> retryMessage = new RetryMessage<>();
retryMessage.setTotalCount(queryCount);
retryMessage.setDelayLevels(queryDelayLevels);
retryMessage.setTopic(TopicConst.PAY_QUERY_TOPIC);
retryMessage.setEventType(RetryEventTypeEnum.PAY_QUERY);
retryMessage.setEventDesc(RetryEventTypeEnum.PAY_QUERY.getDesc());
retryMessage.setData(payQueryMessage);
log.info("{} - 发送消息, retryMessage: {}", LOG_DESC, retryMessage);
rocketMqProducer.asyncSend(retryMessage.getTopic(), JsonUtil.toJson(retryMessage),
CodeEnum.codeOf(RocketMQDelayLevelEnum.class, queryInitLevel).orElse(RocketMQDelayLevelEnum.FiveSeconds), LOG_DESC);
}
复制代码
判断的是否继续执行任务:
public void sendDelayRetry(RetryMessage<?> retryMessage) {
int currentCount;
retryMessage.setCurrentCount(currentCount = retryMessage.getCurrentCount() + 1);
// 重试达到最大次数
if (currentCount > retryMessage.getTotalCount()) {
log.warn("{} - 达到最大次数-{}, 停止重试! retryMessage: {}", retryMessage.getEventDesc(), retryMessage.getTotalCount(), JsonUtil.toJson(retryMessage));
return;
}
log.info("{} - 发送重试消息-{}/{}, retryMessage: {}", retryMessage.getEventDesc(), retryMessage.getCurrentCount(), retryMessage.getTotalCount(), JsonUtil.toJson(retryMessage));
int delayLevel = Integer.parseInt(retryMessage.getDelayLevels().split(",")[retryMessage.getCurrentCount() - 1]);
rocketMqProducer.asyncSend(retryMessage.getTopic(), retryMessage,
CodeEnum.codeOf(RocketMQDelayLevelEnum.class, delayLevel).orElse(RocketMQDelayLevelEnum.FiveSeconds), retryMessage.getEventDesc()+", 发送重试消息");
}
复制代码
3. 支付结果通知上游容错
在回调通知上游系统支付结果时,可能会回调失败,比如网络异常或上游系统发生短时故障,如果发生这种情况我们单靠简单的重试是无法完全解决问题的。为了尽可能的通知成功,我们需要针对没有通知成功的数据,每隔一段时间通知一次,那这个需求和我们上一个问题差不多,所以可以复用我们的延时重试框架。
流程和保证支付结果实时
的差不多,不再赘述。
支付中心系统中设计模式的应用
模板方法
模板方法模式思想:定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。
模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
简单的理解就是定义一个模版方法,然后子类实现模版方法中的抽象方法实现个性化的需求。
就支付而言,无论何种支付产品,都是走的同一个支付流程,那我们就可以定义一个支付流程的模板,然后每种支付产品实现这个模板中特定步骤来实现自己的特定需求。
策略
策略模式主要思想:定义一系列的算法,把它们一个个封装起来,并且使它们可相互替换。
在支付系统中,支付结果主动查询需要查询不同的渠道,比如支付宝,微信,银联等,每个渠道查询的方式和参数不尽相同,可以将每种渠道查询封装成不同的策略类,然后根据查询条件来调用不同的策略类。
查询策略有两个策略接口,callChannel
功能是组装查询参数和查询三方,execute
是处理三方返回的结果统一为支付中心状态。(因callChannel
有其他地方共用所以分开了两个方法)。
Spring 下使用策略模式,在项目启动时,将所有的策略类加载到Map中,然后使用时直接在Map中获取。
@Component
public class PayQueryStrategyContext {
private final Map<String, PayQueryStrategy> payQueryStrategyMap = Maps.newConcurrentMap();
public PayQueryStrategyContext(Map<String, PayQueryStrategy> payQueryStrategyMap) {
this.payQueryStrategyMap.clear();
payQueryStrategyMap.forEach(this.payQueryStrategyMap::put);
}
public PayQueryStrategy getPayQuery(@NotNull String channelCode) {
return this.payQueryStrategyMap.get(OperationTypeConst.Pay_Query + channelCode);
}
}
复制代码
我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿。