支付中心系统对内为各个业务线提供统一的支付、退款等服务,对外对接三方支付或银行服务实现资金的流转。如下图:
大部分公司基本都是这样的架构,主要有以下几方面的优点:
上图展示了用户支付的主要流程,分为三个步骤:
下面详细说下这三个步骤:
上图展示了两个业务线(景区业务线,酒店业务线)唤起的收银台页面,大概可以分为三个区域:
页面上部分显示的是支付剩余时间和应付金额;
中间部分是订单信息,根据收银台定义的数据格式,业务线动态传递过来的;
剩余部分展示的是支付渠道,支付渠道也是业务线根据自己的需求在支付后台管理系统配置的,想要哪些支付方式以及它们的顺序都可以自定义。
以下几个步骤是异步执行的,不分先后。
如果用户长时间没有支付,一般都会有一个超时时间(如上图商户收银台的支付剩余时间),到达这个超时时间支付单会自动关闭。实现此需求有很多方式,比如:
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 延时队列实现的订单关闭。
三方支付系统支付成功后99.9%的情况下都会回调通知我们,但也难免有意外,比如三方延迟回调或者三方系统宕机,为了保证支付结果的实时性,三方支付也要求我们不能完全依赖于回调接口,所以我们需要定时的调用主动查询接口来查询三方的支付结果。这里我们也是使用的 RocketMQ 延时队列实现的:
有三个重要参数,这些参数可以放到配置中心或者配置库中,
// 初始延迟级别,对应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 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()+", 发送重试消息");
}
复制代码
在回调通知上游系统支付结果时,可能会回调失败,比如网络异常或上游系统发生短时故障,如果发生这种情况我们单靠简单的重试是无法完全解决问题的。为了尽可能的通知成功,我们需要针对没有通知成功的数据,每隔一段时间通知一次,那这个需求和我们上一个问题差不多,所以可以复用我们的延时重试框架。
流程和保证支付结果实时
的差不多,不再赘述。
模板方法模式思想:定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。
模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
简单的理解就是定义一个模版方法,然后子类实现模版方法中的抽象方法实现个性化的需求。
就支付而言,无论何种支付产品,都是走的同一个支付流程,那我们就可以定义一个支付流程的模板,然后每种支付产品实现这个模板中特定步骤来实现自己的特定需求。
策略模式主要思想:定义一系列的算法,把它们一个个封装起来,并且使它们可相互替换。
在支付系统中,支付结果主动查询需要查询不同的渠道,比如支付宝,微信,银联等,每个渠道查询的方式和参数不尽相同,可以将每种渠道查询封装成不同的策略类,然后根据查询条件来调用不同的策略类。
查询策略有两个策略接口,callChannel
功能是组装查询参数和查询三方,execute
是处理三方返回的结果统一为支付中心状态。(因callChannel
有其他地方共用所以分开了两个方法)。
Spring 下使用策略模式,在项目启动时,将所有的策略类加载到Map中,然后使用时直接在Map中获取。
@Component
public class PayQueryStrategyContext {
private final Map payQueryStrategyMap = Maps.newConcurrentMap();
public PayQueryStrategyContext(Map payQueryStrategyMap) {
this.payQueryStrategyMap.clear();
payQueryStrategyMap.forEach(this.payQueryStrategyMap::put);
}
public PayQueryStrategy getPayQuery(@NotNull String channelCode) {
return this.payQueryStrategyMap.get(OperationTypeConst.Pay_Query + channelCode);
}
}