支付系统设计:轮询扣款设计二

文章目录

  • 前言
  • 一、相关表改造
    • 1. payCore支付核心
    • 2. payRouter支付路由系统
      • 2.1 增加轮询规则表
      • 2.2 后台配置页面
  • 二、执行逻辑调整
    • 1. payCore支付核心
      • 1.1 请求payRouter报文调整
      • 1.2 增加可重试组件
    • 2. payRouter支付路由系统
      • 2.1 单笔路由逻辑变更
      • 2.2 新增轮询规则功能
  • 三、核心问题
    • 1. 处理流程
    • 2. 重试组件编写
      • 2.1 Chain中增加重试Command
      • 2.2 RetryConfig
      • 2.3 Retryable
      • 2.4 AbstractRetryLoop
      • 2.5 RoundRetryLoop
    • 3. 异步通知
  • 总结


前言

在上篇文章(支付系统设计:轮询扣款设计一)中我们大概介绍了下未改造前系统代扣流程,存在的问题是请求支付路由系统返回的最优支付渠道失败后就失败了,然而这种处理机制业务方是不能接受的,对于一些公司的业务可能存在客户逾期的风险,为了满足业务方需求需求并增强系统功能对原来代扣流程进行改造,使之支持轮询扣款功能,当最优支付渠道失败后进行其他渠道重试。

本篇将进一步详细分析设计。


一、相关表改造

如果自己公司交易表都是单表(只有一个order表)设计,那么吊的了,想要改造为支持轮询,那么很大的工作量会在先将原系统进行改造为可扩展的系统,所以可见初期系统设计很烂的话后期功能扩展很大的工作量不是在扩展新功能上,而是在填补初期的设计债。

1. payCore支付核心

order表增加路由轮询相关字段。
支付系统设计:轮询扣款设计二_第1张图片

  `failed_channel` varchar(128) DEFAULT NULL COMMENT '已失败渠道',
  `round_time` int(2) unsigned DEFAULT '0' COMMENT '轮询次数',
  `is_complete` tinyint(1) unsigned DEFAULT '0' COMMENT '是否轮询完成,1-是,0-否,默认0',
  `round_extend` varchar(512) DEFAULT NULL COMMENT '轮询信息'

failed_channel: 记录已经失败的渠道,在请求支付路由获取最优支付渠道时候需要上送,在路由侧进行渠道筛选时(命中集合中排除接口上送的已经失败渠道集合)去除已经失败的支付渠道信息;

payCpcn,payAli,payWx

round_time: 轮询次数,记录此笔订单已经重试的次数,在轮询循环前置判断中和round_extend中roundNum进行比较是否达到最大轮询次数;
is_complete: 否轮询完成,1-是,0-否,默认0;
round_extend: 用于记录存储请求支付路由系统获取的轮询规则信息;

{“roundChannel”:“payCpcn,payAli,payWx”,“roundNum”:3,“timeLimitSec”:1000,“channelType”:“WHITE”,“ruleId”:“74”}

改造后的对应关系:Order : Detail=1 : N,Detail表不用动,每次重试一个新的支付渠道都要生成一条新的Detail,Order的交易状态是和最新的Detail关联的。

2. payRouter支付路由系统

2.1 增加轮询规则表

round_rule表结构
支付系统设计:轮询扣款设计二_第2张图片

2.2 后台配置页面

新增后台配置页面,用于轮询规则的配置:
支付系统设计:轮询扣款设计二_第3张图片支付系统设计:轮询扣款设计二_第4张图片

二、执行逻辑调整

1. payCore支付核心

1.1 请求payRouter报文调整

请求payRouter获取最优支付渠道时,上送的报文添加Order表中 failed_channel已经失败渠道round_extend.roundChannel 轮询渠道

1.2 增加可重试组件

在请求支付渠道同步返回/paygw异步通知/定时查询失败后进入可重试组件,在重试组件中调用轮询规则路由接口获取轮询渠道。

2. payRouter支付路由系统

2.1 单笔路由逻辑变更

接口增加上送参数failed_channel已经失败渠道roundChannel 轮询渠道,在命中后与接口上送的轮询渠道取交集,筛除已经失败渠道后再执行原流程逻辑。

如下是路由系统核心逻辑:

支付系统设计:轮询扣款设计二_第5张图片

2.2 新增轮询规则功能

支付系统在请求路由系统返回的最优支付渠道并返回失败(同步返回/异步通知/定时查询)后,进入循环体内,在循环体内首先判断是否已经请求过轮询规则路由接口,即Order表中round_extend字段是否有值,如果没有则调用新增的轮询接口获取是否存在轮询规则,不存在则流程终止,不进行重试;如果存在则在loop体内循环调用。
注:配置的轮询渠道并不一定支持此笔交易,所以获取到轮询规则后,在调用支付渠道之前需要调用单笔路由接口,调用时候需要将已经失败的支付渠道和调用轮询规则接口返回的支付渠道集合上送到支付路由,如果路由系统单笔路由接口返回了支付渠道信息,说明配置的轮询渠道中还存在可尝试的支付渠道,则再生成一条Detail详情单并请求支付渠道完成交易。
在请求轮询规则路由接口时,上送的字段除了原请求单笔路由接口的字段外,还有渠道上一笔支付渠道响应码(转换后的平台响应码),因为轮询重试的重要条件是渠道返回了什么样的响应码才进行其他支付渠道的重试,如果支付渠道返回余额不足,针对这类响应直接终止了,客户卡余额不足,使用其他支付渠道还是余额不足的,所以这类响应时不用重试的。

三、核心问题

1. 处理流程

很多人看到改造方案后会有一个很大的疑问,为什么支付路由系统渠道路由接口不干脆返回支持的支付渠道集合(按照优先级排序好),让业务方自己去循环调用,干嘛要那么复杂的流程,一次重试还要和支付路由系统交互多次。

返回一个可用集合,让支付系统自行重试去,不需要和支付路由系统交互那么多次,表面看着没毛病,如果这么设计实现是简单,但是重试是一个使用率很低的功能,可能1000笔交易才有1笔需要重试,那么其余999笔支付路由系统都需要对命中的支付渠道集合都要进行全流程过滤,并且这999笔做的工作业务方并没有使用,白白徒增了支付路由系统的工作,所以系统设计需要充分考虑实际现状,而不是怎么简单怎么来。

并且重试次数,什么条件下重试,重试超时时间也需要在各处需要重试的地方自行维护,没个集中管理的地方,不符合系统设计原则。

2. 重试组件编写

2.1 Chain中增加重试Command

支付系统设计:轮询扣款设计二_第6张图片

2.2 RetryConfig

/**
 * @author Kkk
 * @Description: 重试配置类
 */
public class RetryConfig {

	//轮询初始开始次数
	private Integer round = 1;

	//重试次数
	private Integer retry;

	//最大交易时间(秒)
	private long timeLimitSec;

	//每次重试等待时间
	private long delayMilli = 100L;

	public RetryConfig() {
	}

	public RetryConfig(Integer retry, long timeLimitSec) {
		this.retry = retry;
		this.timeLimitSec = timeLimitSec;
	}

	public RetryConfig(Integer round, Integer retry, long timeLimitSec) {
		this.round = round;
		this.retry = retry;
		this.timeLimitSec = timeLimitSec;
	}


	public Integer getRetry() {
		return retry;
	}

	public void setRetry(Integer retry) {
		this.retry = retry;
	}

	public long getDelayMilli() {
		return delayMilli;
	}

	public void setDelayMilli(long delayMilli) {
		this.delayMilli = delayMilli;
	}

	public long getTimeLimitSec() {
		return timeLimitSec;
	}

	public void setTimeLimitSec(long timeLimitSec) {
		this.timeLimitSec = timeLimitSec;
	}

	public Integer getRound() {
		return round;
	}

	public void setRound(Integer round) {
		this.round = round;
	}
}

2.3 Retryable

/**
 * @author Kkk
 * @Description: 重试循环体抽象类
 */
public interface Retryable<T> {

	/**
	 * 循环执行
	 * @param round
	 * @param millis
	 * @return
	 * @throws Exception
	 */
	T proceed(T t, int round, long millis) throws Exception;

	/**
	 * 循环前置处理
	 * @param round
	 * @param millis
	 * @return
	 */
	default boolean preCondition(T t, int round, long millis) {
		return true;
	}

	/**
	 * 循环后置处理
	 * @param t
	 * @param round
	 * @param millis
	 * @return
	 */
	default boolean postCondition(T t, int round, long millis) {
		return true;
	}

	/**
	 * 结束时执行
	 * @param t
	 * @param round
	 * @param millis
	 * @return
	 */
	default T whenFinish(T t, int round, long millis) {
		return t;
	}

	/**
	 * 错误时执行
	 * @param e
	 * @param round
	 * @param millis
	 * @return
	 */
	default boolean whenError(Throwable e, T t, int round, long millis) {
		return false;
	}

	/**
	 * 超时处理
	 * @param t
	 * @param millis
	 * @return
	 */
	default boolean whenTimeout(T t, long millis) {
		return false;
	}
}

2.4 AbstractRetryLoop

/**
 * @author Kkk
 * @Description: 重试循环体抽象类
 */
public abstract class AbstractRetryLoop {

	protected long startMillis = System.currentTimeMillis();

	protected RetryConfig retryConfig;

	protected AbstractRetryLoop(RetryConfig retryConfig) {
		this.retryConfig = retryConfig;
	}


	public <T> T proceed(T t, Retryable<T> retryable) {
		return loop(t, retryable);
	}

	protected long diff() {
		long time = System.currentTimeMillis() - startMillis;
		return time;
	}

	protected boolean sleepIfInterrupt() {
		try {
			TimeUnit.MILLISECONDS.sleep(retryConfig.getDelayMilli());
		} catch (InterruptedException e) {
			throw new PayCoreException(SystemErrorCode.SYSTEM_ERROR);
		}
		return false;
	}

	/**
	 * 抽象循环体
	 */
	protected abstract <T> T loop(T t, Retryable<T> retryable);

}

2.5 RoundRetryLoop

/**
 * @author Kkk
 * @Description: 轮询重试循环实现
 */
public class RoundRetryLoop extends AbstractRetryLoop {
	private Logger logger = LoggerFactory.getLogger(RoundRetryLoop.class);

	protected RoundRetryLoop(RetryConfig retryConfig) {
		super(retryConfig);
	}
	
	@Override
	protected <T> T loop(T t, Retryable<T> retryable) {
		startMillis = System.currentTimeMillis();

		int round = retryConfig.getRound() >= 1 ? retryConfig.getRound() : 1;
		try {
			while(true) {
				//1.前置校验、处理
				logger.info("轮询交易前置第({})次校验、处理", round);
				if (!retryable.preCondition(t, round, diff())) {
					logger.info("轮询交易前置第({})次校验、处理失败(false)", round);
					break;
				}
				//2.业务处理
				logger.info("轮询交易业务第({})次处理", round);
				t = retryable.proceed(t, round, diff());
				//3.后置校验、处理
				logger.info("轮询交易后置第({})次校验、处理", round);
				if (!retryable.postCondition(t, round, diff())) {
					logger.info("轮询交易后置第({})次校验、处理失败(false)", round);
					break;
				}
				//轮询次数增加
				round++;
				//延迟执行
				if (sleepIfInterrupt()) {
					break;
				}
			}
			//4.轮询完处理
			retryable.whenFinish(t, round, diff());
			logger.info("轮询交易完成");
		} catch (PayCoreException e) {
			logger.info("轮询交易异常已处理", e);
			//5.异常处理
			if (retryable.whenError(e, t, round, diff())) {
				logger.info("轮询交易异常已正常处理");
			} else {
				throw e;
			}
		} catch (Exception e) {
			logger.error("轮询交易异常", e);
			throw new PayCoreException(SystemErrorCode.SYSTEM_ERROR);
		}
		return t;
	}
}

如上核心接口已经类定义,核心处理思路是增加PayRoundCommand命令,在这个Command里先判断是否需要重试,主要判断条件Order是否已经为终态,如果为终态则退出,判断Detail是否为终态,如果非失败状态退出。

然后判断是否已经请求过路由的重试接口了,即判断Order表round_extend是否有值,没有的话请求支付路由获取重试渠道信息,有的话则构建循环体并进入,Retryable接口定义了preCondition、proceed、postCondition、whenFinish、whenError方法分别完成前置判断,重试、后置判断、完成处理、异常处理等。核心是在proceed调用paySendGwCommand.execute(domain),以此达到了一个循环的目标。

3. 异步通知

异步通知和定时回调虽然是另外两个获取订单终态的触发点,但是最终这两块逻辑最终都是走的同一块逻辑,无非就是将两处的对象转化为一个对象传入一个方法中,所以我们就简单看下异步通知处怎么做的就行了。

   //轮询扣款
   domain.setNewThread(false);//设置false,表示主线程发起轮询
   boolean roundFlag = deductRoundCommand.execute(domain);

在回调处理流程中加入循环Command就行了,可见前期系统设计的好不好对后期功能迭代进度有很大影响。


总结

拙技蒙斧正,不胜雀跃。

你可能感兴趣的:(支付系统设计,数据库,java,开发语言)