大家好,我是隐墨星辰,专注境内/跨境支付架构设计十余年。
在前面介绍过支付系统的整体设计,有兴趣的读者可点击链接查看:图解支付系统整体设计,今天聊聊支付系统的一些关键设计细节。
内容主要包括一些支付系统常用的设计,比如领域建模,状态机,幂等,日志规范,业务ID生成规范,监控,资损防控,支付安全等。这些技术在互联网其它领域比如电商也是通用的。
这里只摘录了部分精华内容出来,但已经能表达最核心的设计理念。
1. 领域建模
领域驱动设计(DDD)思想在支付系统的设计中应用非常广泛,但需要对行业有非常深刻的理解,否则构建出来的模型稳定性极差,动不动就需要修改核心模型。
收单、结算、支付引擎、会员、商户服务、渠道网关等各关键域都有自己独特的业务,模型也是不一样的。给两个小示例:
比如会员的三户模型。
说明:
- 客户是一种社会属性,代表真实社会的一个实体。比如张三分别使用1388888888和1399999999两个手机号在微信支付进行注册,然后使用同一个身份证做了实名认证,那么这两个用户仍然被归属于同一个客户。
- 用户是一种业务属性,就是使用产品的身份。 比如张三分别使用1388888888和1399999999两个手机号在微信支付进行注册,那么就会存在2个用户,这2个用户的业务是独立的。
- 账户是一种金融属性,代表使用资金的身份。 比如张三使用1388888888开通了微信支付,经过实名认证后,可以开通余额,也可以开通基金账户。
如果没有金融属性,一般就没有账户,比如注册博客园后,就只有用户没有账户。
下图是会员的模型:
说明:
- 用户、客户、账户的关系,详见上面的三户模型说明。会员注册后,就会有一个用户(UserId),完成实名认证,就会有一个客户(CustomeId),开通余额,就会有账户(AccountNo)。
- 其它的关系,图中已经很清楚。
支付渠道的模型:
说明:
渠道:对外部渠道做一个抽象,比如国内微信、支付宝、银联等,国外的WPG、MGPS等。
业务能力:支付、退款、撤销、请款等能力详细描述。比如退款有效期、最小限额等。
接口能力:描述接口本身的能力,比如渠道的请求号生成规则。特殊情况下可能有短号问题。
2. 状态机
状态机,也称为有限状态机(FSM, Finite State Machine),是一种行为模型,由一组定义良好的状态、状态之间的转换规则和一个初始状态组成。它根据当前的状态和输入的事件,从一个状态转移到另一个状态。
下图就是收单子域设计中交易单的状态机设计。
从图中可以看到,一共4个状态,每个状态之间的转换由指定的事件触发。因为有组合支付的情况,所以全额支付成功才会推进到成功。
常见代码实现误区
- 经常看到工作多年的研发工程师实现状态机时,仍然使用if else或switch case来写。这是不对的,会让实现变得复杂,且容易出现问题。
- 直接在订单的领域模型里面使用String来定义,而不是把状态模式封装单独的类。
- 直接调用领域模型更新状态,而不是通过事件来驱动。
- 业务不做拆分,一个状态机包含了所有的业务状态,比如支付单把退款和撤销状态也耦合进去,导致状态机巨复杂。
下面是一个比较差的状态机设计:
限于篇幅,这里无法给出完整示例代码。有兴趣的可以去网上看看,良好的示例有很多。
/**
* 支付状态机
*/
public enum PaymentStatus implements BaseStatus {
INIT("INIT", "初始化"),
PAYING("PAYING", "支付中"),
PAID("PAID", "支付成功"),
FAILED("FAILED", "支付失败"),
;
// 支付状态机内容
private static final StateMachine<PaymentStatus, PaymentEvent> STATE_MACHINE = new StateMachine<>();
static {
// 初始状态
STATE_MACHINE.accept(null, PaymentEvent.PAY_CREATE, INIT);
// 支付中
STATE_MACHINE.accept(INIT, PaymentEvent.PAY_PROCESS, PAYING);
// 支付成功
STATE_MACHINE.accept(PAYING, PaymentEvent.PAY_SUCCESS, PAID);
// 支付失败
STATE_MACHINE.accept(PAYING, PaymentEvent.PAY_FAIL, FAILED);
}
// 状态
private final String status;
// 描述
private final String description;
PaymentStatus(String status, String description) {
this.status = status;
this.description = description;
}
/**
* 通过源状态和事件类型获取目标状态
*/
public static PaymentStatus getTargetStatus(PaymentStatus sourceStatus, PaymentEvent event) {
return STATE_MACHINE.getTargetStatus(sourceStatus, event);
}
}
3. 幂等
幂等是针对重复请求的,支付系统一般会面临以下几个重复请求的场景:
- 用户多次点击支付按钮:在网络较差或系统过载情况下,用户由于不确定交易是否完成而重复点击。
- 自动重试机制:系统在超时或失败时重试请求,可能导致同一支付多次尝试。
- 网络数据包重复:数据包在网络传输过程中,复制出了多份,导致支付平台收到多次一模一样的请求。
- 异常恢复:在系统升级或崩溃后,未决事务需要根据已有记录恢复和完成。内部系统重发操作。
幂等解决方案
所谓业务幂等,就是由各域自己把唯一性的交易ID作为数据库唯一索引,这样可以保证不会重复处理。
在数据库前面可以加一层缓存来提高性能,但是缓存只用于查询,查到数据认为就返回幂等成功,但是查不到,需要尝试插入数据库,插入成功后再刷新数据到缓存。
为什么要使用数据库的唯一索引做为兜底,是因为缓存是可能失效的。
在面临时经常有候选人只回答到“使用redis分布式锁来实现幂等”,这是不对的。因为缓存有可能失效,分布式锁只是用于防并发操作的一种手段,无法根本性解决幂等问题,幂等一定是依赖数据库的唯一索引解决。
大部分简单的支付系统只要有业务幂等基本也够用了。
更复杂的多机房容灾情况下,幂等也会非常复杂。比如在A机房处理了一笔业务,这时A机房挂了,流量切到了B机房,B机房如果没有相关的幂等数据,那就会幂等失败。
4. 日志规范
只要在公司写过代码,就一定打印过日志,但经常发现一些工作多年的工程师打印的日志也是乱七八糟的。我曾经在一家头部互联网公司接手过一个上线一年多的业务,相关日志一开始就没有设计好,导致很多监控无法实现,出了线上问题也不知道,最后只能安排工程师返工改造相关的日志。
我们要明白日志是用来做什么的。只是先弄明白做事的目的,我们才能更好把事情做对。在我看来,日志有两个核心的作用:1)监控,诊断系统或业务是否存在问题;2)排查问题。
对于监控而言,我们需要知道几个核心的数据:业务/接口的请求量、成功量、成功率、耗时,系统返回码、业务返回码,异常信息等。对于排查问题而言,我们需要有出入参、中间处理数据的上下文、报错的上下文等。
接下来,基于上面的分析,我们就清楚我们应该有几种日志:
- 接口摘要日志。监控接口的请求量、成功量、耗时、返回码等。使用固定格式,需要打印:时间、接口名称、结果(成功/失败)、返回码、耗时等基本信息就足够。
- 业务摘要日志。监控业务的请求量、成功量、核心业务信息、返回码等。使用固定格式,需要打印:时间、业务类型、上一步状态、当前状态、返回码、核心业务信息(不同业务有不同的核心业务信息,比如流入,就有支付金额/退款金额,卡品牌,卡BIN等)。
- 详细日志。用于排查问题,不用于监控。格式不固定。主要包括时间、接口、入参、出参、中间处理数据输入、异常的堆栈信息等。
- 系统异常日志。同时用于监控。格式固定。需要打印:时间、错误码、错误信息、堆栈信息等。
5. 金额处理规范
对于研发经验不足的团队而言,经常会犯以下几种错误:
- 没有定义统一的Money类,各系统间使用BigDecimal、double、long等数据类型进行金额处理及存储。
- 定义了统一的Money类,但是写代码时不严格遵守,仍然有些代码使用BigDecimal、double、long等数据类型进行金额处理。
- 手动对金额进行加、减、乘、除运算,单位(元与分)换算。
带来的后果,通常就是资金损失,再细化一下,最常见的情况有下面3种:
- 手动做单位换算导致金额被放大或缩小100倍。
-
- 比如大家规定传的是元,但是其中有位同学忘记了,以为传的是分,外部渠道要求传元,就手动乘以100。或者反过来。
- 还有一种情况,部分币种比如日元最小单元就是元,假如系统约定传的是分,外部渠道要求传元,就可能在网关处理时手动乘以100。
- 1分钱归属问题。比如结算给商家,或计算手续费时,碰到除不尽时,使用四舍五入,还是向零舍入,还是银行家舍入?这取决于财务策略。
- 精度丢失。在大金额时,double有可能会有精度丢失问题。
最佳实践:
- 制定适用于公司业务的Money类来统一处理金额。
- 在入口网关接收到请求后,就转换为Money类。
- 所有内部应用的金额处理,强制全部使用Money类运算、传输,禁止自己手动加减乘除、单位换算(比如元到分)。
- 数据库使用DECIMAL类型保存,保存单位为元。
- 在出口网关外发时,再根据外部接口文档要求,转换成使用指定的单位。有些是元,有些是分(最小货币单位)
6. 业务ID生成规则
数据库一般都会设计一个自增ID作为主键,同时还会设计一个能唯一标识一笔业务的ID,这就是所谓的业务ID(也称业务键)。比如收单域的收单单号。
也有人采用所谓雪花算法,但其实不适用于支付场景。
下面以32位的支付系统业务ID生成为例说明。实际应用时可灵活调整。
第1-8位:日期。通过单号一眼能看出是哪天的交易。
第9位:数据版本。用于单据号的升级。
第10位:系统版本。用于内部系统版本升级,尤其是不兼容升级的时候,老业务使用老的系统处理,新业务使用新系统处理。
第11-13位:系统标识码。支付系统内部每个域分配一段,由各域自行再分配给内部系统。比如010是收单核心,012是结算核心。
第14-15位:业务标识位。由各域内部定,比如00-15代表支付类业务,01支付,02预授权,03请款等。
第16-17位:机房位。用于全球化部署。
第18-19位:用户分库位。支持百库。
第20-21位:用户分表位。支持百表。
第22位:预发生产标识位。比如0代表预发环境,1代表生产环境。
第23-24位:预留。各域根据实际情况扩展使用。
第24-32位:序列号空间。一亿规模,循环使用。一个机房一天一亿笔是很大的规模了。如果不够用,可以扩展到第24位,到十亿规模。
7. 自动化渠道开关
外部渠道接多了,时不时有个渠道半夜宕机或非预期维护,人工运维成本高,搞个自动化开关。
说明:
- 渠道初始为完全打开。
- 当指定时间内成功率低于阈值或指定时间内连续失败次数大于阈值,就关闭渠道。
- 关闭渠道后,捞取最近成功的一笔发起查询探测,如果查询失败,仍然关闭。
- 如果查询成功,说明和渠道的通路是通的,且渠道能提供基本的服务,打开灰度25%。
- 如果灰度25%情况下,成功率不达标,仍然关闭。
- 如果灰度25%情况下,成功率达标,继续加大灰度比例,直到100%。
注:上述灰度打开算法还可以优化为:N*2算法,其中N初始为1。举个例子:先打开1%,符合要求后,依次打开:2%,4%,8%,16%,32%,64%,100%。通过7次操作后,100%打开。这个算法适合一些体量大的渠道,直接开25%如果仍然失败会影响很大批量的用户。对于小流量渠道,灰度1%可能很久也没有量进来,不如直接25%见效快。
8. 返回码设计与映射
外部商户对接支付平台,支付平台内部有自己的业务处理,同时还对接了外部的很多渠道,所以需要管理三套返回码:
- 提供给商户OpenAPI使用的返回码:这块可以直接参考微信支付、支付宝等机构的门户网站。
- 内部各应用使用的标准返回码:用于内部业务的处理。
- 渠道返回码:外部渠道提供的返回码,每个渠道都不一样,需要映射到内部标准返回码。
基本原则:
- 制定统一返回码规范:在团队或公司层面制定统一的返回码规范,明确各个返回码的含义,确保各模块一致性。
- 严格遵守返回码定义:研发人员在编码时,应严格按照规范返回对应的返回码,确保返回码与实际状态匹配。明确成功才推进成功,明确失败才推进失败,其它全部按“未知”处理。
- 区分接口/通信成功与业务成功。
- 流入到平台的(支付、充值等),谨慎映射到成功。从平台流出的(提现,代发等),谨慎映射到失败。
支付平台内部也分了不同域,建议使用一个共同的规范,比如:RS+子系统编号+错误级别+具体返回码。具体如下图所示:
说明:
1-2位:固定值RS,Result缩写。
3-5位:子系统编号。比如001:收银支付,002:会员等。可方便定位哪个系统出的问题。
6位:错误类或等级。比如:0:正常,1:业务级异常,2:系统级异常。
7-9位:各业务线自己定。比如:1xx:参数相关,2xx:数据库相关,3xx:账户状态/Token状态相关等。
这样的好处在于,每个子域或子系统既有全局的规范,又有自己的灵活性,减少沟通成本。
注意:上面只是写了resultCode,还需要有message,用于描述这个码代表什么语义。
踩过的坑很多,大致可以归为以下几类:
- 对客映射不准确,导致用户持续重试失败,影响用户体验。比如“余额不足”或“风控不过”,返回给用户“系统异常,请重试”,有些用户就疯狂地重试。
- 外部渠道没有明确成功或失败,内部映射成明确成功或失败,造成资损。比如:
-
- 支付同步请求渠道响应还没有回来,发起了查询,查询返回“订单不存在”,直接推进失败,但最后银行扣款成功。
- 退款同步请求渠道响应返回“系统异常”,直接推进到失败,但最后银行退款成功。
- 外部渠道有双层返回码,没有做完整判断。比如第1层只表示接口是否成功(通信层面),第2层才是表示业务是否成功,但是只判断了接口层面,就推进了内部订单的业务状态。
- 返回码制定过于笼统或太细。
9. 退款自动重试与支付自动重试
在支付系统日常运营工作中,处理退款重发的运营工作量会随着交易量的增长而线性增长。达到一定程度就需要考虑建设退款自动重试的能力。
下面两种情况是最常见的需要做退款重发的场景:
- 支付平台发给外部渠道超时。
- 外部渠道内部出现BUG或临时系统异常。
当交易量足够大时,完全人工重发,工作量也是很可观的。但是我们不能简单地做系统自动重发,因为退款有可能是会有资损发生的。比如:用户支付100元,退款50元,渠道不支持幂等,渠道已经内部退款成功,但是我们系统因为逻辑有缺陷又做了重发,那就是退款2次成功,资损50元。
有些外部渠道的设计不太好,比如只能通过支付请求号去退款,又支持多次部分退,那就相当于没有幂等能力。
下面是一个简化版的重发逻辑。
10. 使用多线程并发获取支付方式
每个用户在支付系统中绑定了很多支付方式,比如不同的银行卡,还有内部的余额,红包等。每次渲染收银台之前,都需要去获取这些支付系统,汇总后展示给用户。
一种方式就是串行去获取,但这明显会影响用户体验。最优的方案当然是使用多线程并行获取。
11. 同步受理异步处理
用户提交支付请求后,如果是外部银行通道,通常耗时都需要几百到几千毫秒,如果全链路都是同步接口,那么整个系统的线程很快就被消耗完,且一旦外部银行出现响应慢的情况,极其容易出现雪崩现象。
所以我们在调用外部银行扣款时,通常都使用“同步受理异步处理”的方案。简单地说,就是先受理用户的请求,做基础校验,校验通过后,保存到数据库,发起一个异步线程请求到外部银行,然后马上返回给用户,前端再发起定时轮询结果。
当然还可以优化为在渠道网关做异步化,因为这里对接渠道,Hold住的线程影响面最小。
常见误区
- IO密集型任务线程池大小设置为CPU核数的2倍
哪怕是IO密集型服务,我们也不能简单设置为CPU核数的2倍,我们仍然要考虑任务执行耗时,系统设计的最大并发数是多少等因数。
建议为:系统预期最大并发任务数 * 单任务平均耗时。
注意,这个耗时是指等待外部资源的耗时,不是CPU运算耗时。比如外发银行后,等待外部银行返回的过程,就是等待时间,基本不消耗CPU资源。
- 为什么设置了最大线程数不生效
曾经碰过一个线上问题:使用ThreadPoolExecutor,设置了核心线程数,最大线程数,但是线上出现很多超时未处理的任务,但是请求数没有超过最大线程数。排查很久才发现虽然设置了最大线程数,但是没有设置队列大小(LinkedBlockingQueue) ,那么它会默认为Integer.MAX_VALUE,这基本上可以认为是无界队列,也就是请求全部放到了队列中。
所以读者如果使用ThreadPoolExecutor来配置线程池,最好是根据自己的诉求,把参数设置完整,包括核心线程数,最大线程数,队列大小,拒绝策略等。比如有些业务超时后已经没有意义,那就把队列放小点,拒绝策略为直接拒绝。
具体的请参考JAVA官方文档。
- 直接new线程
因为简单,有些同学喜欢直接new线程。的确,这种方式在简单场景下是没有问题的,但是复杂场景下是很容易出问题,且不好排查,建议不要养成这样的习惯。如果场景真的非常简单,也建议使用创建固定大小线程池来做,比如ExecutorService executor = Executors.newFixedThreadPool(n)。
12. Spring事务模板
为什么不使用@Transaction注解
以前写管理平台的代码时,经常使用@Transaction注解,也就是所谓的声明式事务,简单而实用,但是在做支付后,基本上没有使用@Transaction,全部使用事务模板来做。主要有两个考虑:
1)事务的粒度控制不够灵活,容易出现长事务
@Transactional注解通常应用于方法级别,这意味着被注解的方法将作为一个整体运行在事务上下文中。在复杂的支付流程中,需要做各种运算处理,很多前置处理是不需要放在事务里面的。
而使用事务模板的话,就可以更精细的控制事务的开始和结束,以及更细粒度的错误处理逻辑。
@Transactional
public PayOrder process(PayRequest request) {
validate(request);
PayOrder payOrder = buildOrder(request);
save(payOrder);
// 其它处理
otherProcess(payOrder);
}
比如上面的校验,构建订单,其它处理都不需要放在事务中。
如果把@Transactional从process()中拿走,放到save()方法,也会面临另外的问题:otherProcess()依赖数据库保存成功后才能执行,如果保存失败,不能执行otherProcess()处理。全部考虑进来后,使用注解处理起来就很麻烦。
2)事务传播行为的复杂性
@Transactional注解支持不同的事务传播行为,虽然这提供了灵活性,但在实际应用中,错误的事务传播配置可能导致难以追踪的问题,如意外的事务提交或回滚。
而且经常有多层子函数调用,很容易子函数有一个耗时操作(比如RPC调用或请求外部应用),一方面可能出事长事务,另一方面还可能因为外调抛异步,导致事务回滚,数据库中都没有记录保存。
以前就在生产上碰到过类似的问题,因为在父方法使用了@Transactional注解,子函数抛出异常导致事务回滚,去数据库找问题单据,竟然没有记录,翻代码一行行看,才发现问题。
事务模板示例:
public class PaymentSystemTransactionTemplate {
public static <R> R execute(FlowContext context, Supplier<R> callback) {
TransactionTemplate template = context.getTransactionTemplate();
Assert.notNull(template, "transactionTemplate cannot be null");
PlatformTransactionManager transactionManager = template.getTransactionManager();
Assert.notNull(transactionManager, "transactionManager cannot be null");
boolean commit = false;
try {
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition()); // Corrected "TranscationStatus" to "TransactionStatus"
R result = null;
try {
result = callback.get();
} catch (Exception e) {
transactionManager.rollback(status);
throw e;
}
transactionManager.commit(status);
commit = true;
return result;
} finally {
if (commit) {
invokeAfterCommit(context);
}
}
}
private static void invokeAfterCommit(FlowContext context) {
try {
context.invokeAfterCommit();
} catch (Exception e) {
// 打印日志
... ...
}
}
}
13. 分库分表
当数据量大的时间,分库分表是再所难免的。一个经典的面试题是:如果分了100张表,按商户来分表,还是按商户订单号来分表?如果按商户分表怎么解决各表流水数据量平衡问题?如果是按商户订单号来分表,商户想按时间段查询怎么办?
解法有很多种。一种典型的解法,就是线上数据库按商户订单号分表,同时有一个离线库冗余一份按商户号分表的数据,甚至直接使用离线数据平台的能力,把商户的按时间段查询需求从在线库剥离出来。
网上资料很多,不赘述。
14. 分布式事务
分布式事务是个好东西,但是复杂度也高,还经常出现所谓的事务悬挂问题,且虽然各家都号称简单易用,对业务代码侵入少,但事实并非如此。
所以我个人更倾向于避免使用分布式事务解决方案,而是采用最终一致性来解决。对大部分中小公司来说,最终一致性已经够用。
网上资料很多,不赘述。
15. 流程引擎
在支付系统中,流程编排随处可见,比如收银台编排获取各种绑定资产,渠道网关调用外部的渠道进行支付等。可以考虑使用成熟的工具,也可以考虑自研。
下面是一个支付流程编排图:
Activiti和jBPM:配置文件非常繁琐。
liteflow:配置文件很简单,但是配置上只知道节点的流转,核心业务逻辑在代码里面,比如什么条件下推进到成功。
自研流程引擎使用这样的配置:
whenOrderState(CommonOrderState.INIT) // 初始条件:主单状态INIT
.onEvent(CommonEvent.CREATE) // 触发事件:创建
.transitionOrderStateTo(CommonOrderState.PROCESS) // 推进到:支付中
.request(CommonOperation.PAY) // 操作:外发银行
.when("subOrder.currentState == SubOrderState.S") // 银行返回成功推进主单成功
.transitionOrderStateTo(CommonOrderState.SUCCESS)
.when("subOrder.currentState == SubOrderState.F") // 银行返回失败推进主单失败
.transitionOrderStateTo(CommonOrderState.FAIL)
.when("subOrder.currentState == SubOrderState.U && subOrder.webForm != null") // 推进发消息
.notifyNode();
通过这个配置,知道初始状态,触发条件,如何推进。
16. 支付安全
支付安全核心关注点
支付安全是一个很大的范畴,但我们一般只需要重点关注以下几个核心点就够:
- 敏感信息安全存储。
对个人和商户/渠道的敏感信息进行安全存储。
个人敏感信息包括身份证信息、支付卡明文数据和密码等,而商户/渠道的敏感信息则涉及商户登录/操作密码、渠道证书密钥等。
- 交易信息安全传输。
确保客户端与支付系统服务器之间、商户系统与支付系统之间、支付系统内部服务器与服务器之间、支付系统与银行之间的数据传输安全。这包括采用加密技术等措施来保障数据传输过程中的安全性。
- 交易信息的防篡改与防抵赖。
确保交易信息的完整性和真实性,防止交易信息被篡改或者被抵赖。一笔典型的交易,通常涉及到用户、商户、支付机构、银行四方,确保各方发出的信息没有被篡改也无法被抵赖。
- 欺诈交易防范。
识别并防止欺诈交易,包括套现、洗钱等违规操作,以及通过识别用户信息泄露和可疑交易来保护用户资产的安全。这一方面通常由支付风控系统负责。
- 服务可用性。
防范DDoS攻击,确保支付系统的稳定运行和服务可用性。通过部署防火墙、入侵检测系统等技术手段,及时发现并应对可能的DDoS攻击,保障支付服务的正常进行。
极简支付安全大图
支付安全是一个综合性的系统工程,除了技术手段外,还需要建立健全的安全制度和合规制度,而后两者通常被大部分人所忽略。
下图是一个极简版的支付安全大图,包含了支付安全需要考虑的核心要点。
说明:
- 制度是基础。
哪种场景下需要加密存储,加密需要使用什么算法,密钥长度最少需要多少位,哪些场景下需要做签名验签,这些都是制度就明确了的。制度通常分为行业制度和内部安全制度。行业制度通常是国家层面制定的法律法规,比如《网络安全法》、《支付业务管理办法》等。内部安全制度通常是公司根据自身的业务和能力建立的制度,小公司可能就没有。
- 技术手段主要围绕四个目标:
1)敏感数据安全存储。
2)交易安全传输。
3)交易的完整性和真实性。
4)交易的合法性(无欺诈)。
对应的技术手段有:
-
- 敏感信息安全存储:采用加密技术对个人和商户/渠道的敏感信息进行加密存储,限制敏感信息的访问权限,防止未授权的访问和泄露。
- 交易信息安全传输:使用安全套接字层(SSL)或传输层安全性协议(TLS)等加密技术,确保数据在传输过程中的机密性和完整性。
- 交易的完整性和真实性:采用数字签名技术和身份认证技术确保交易信息的完整性和真实性,对交易信息进行记录和审计,建立可追溯的交易日志,以应对可能出现的交易篡改或抵赖情况。
- 防范欺诈交易:通过支付风控系统,及时识别和阻止可疑交易行为。
- 服务可用性:部署流量清洗设备和入侵检测系统,及时发现并阻止恶意流量,确保支付系统的稳定运行和服务可用性,抵御DDoS攻击。
17. 资损防控
所有支付公司都对资损(资金损失)看得很重,轻则钱没了,重则舆论风波,要是引起监管介入,更是吃不了兜着走。
资损本质
资损防控本质
资损防控全生命周期
资损风险分类
资损场景(应对手段限于篇幅无法展开说明)
- 金额放大缩小。
- 幂等击穿。
- 流水号及短号重复。
- 返回码映射错误。
- 数据乱序,但是代码强要求有序列性。
- 越权/测试环境配置外部渠道生产环境。
- 数据库操作没有考虑并发。
- 状态机推进逻辑不严谨。
- 多线程与资源共享导致线程变量污染。
- 系统升级没有考虑到兼容与灰度逻辑。
- 运营操作失误。
18. 监控
监控一般是监控实时交易数据,包括:提交量,成功量,成功率。
对应的有:跌0,同比,环比等。
考虑到支付流量在白天晚上(忙时、闲时),有些渠道流量大,有些渠道流量小,如果想做要到噪音少(告警准确),覆盖全(没有遗漏),建议使用时间窗口算法来做。
19. 核对
监控一般只能监控某个域的数据,但是互联网应用拆成了很多微服务,微服务之间容易出现数据不一致性的问题,需要尽快发现。这个时候就需要用到核对。
有些公司把核对也叫对账,都是一个意思。
实时核对与离线核对是最后的底线
一般的支付平台都会有内部系统之间的两两核对,这种核对主要是信息流层面的对账,主要勾兑状态、金额、笔数等数据的一致性。
再细分,还可以拆成实时对账和离线对账。
实时对账一般就是监听数据库的binlog,当数据有变动时,延时几秒后请求双方系统的查询接口,查到数据后进行对账。
离线对账一般就是把生产数据库的数据定时清洗到离线库(一般还可以分为天表和小时表),然后进行对账。
20. 三层对账
这里主要是指支付平台和外部渠道的对账。存在三种情况:
- 支付平台和外部渠道的流水数据对不上。
- 流水数据对上后,账单对不上。
- 账单对上后,实际打款对不上。
所以对应就有三层对账体系来解决这个问题。
第一层是信息流对账。我方流水和银行清算文件的流水逐一勾兑。可能会存在长短款情况。
第二层是账单对账。就是把我方流水汇总生成我方账单,然后把银行流水汇总生成银行账单,进行对账。可能会存在银行账单和我方账单不一致的情况,比如共支付100万,渠道分2次打款,一笔98万,一笔2万。
第三层是账实对账。就是我方内部记录的银行头寸和银行真实的余额是否一致。可能存在我方记录的头寸是220万,但是银行实际余额只有200万的情况。
21. 结束语
如前面所说,限于篇幅,只从专题文章中摘录了部分关键思路,算是一个引子。意犹未尽的读者可关注公众号后续推出的文章。
深耕境内/跨境支付架构设计十余年,欢迎关注并星标公众号“隐墨星辰”,和我一起深入解码支付系统的方方面面。
这是《图解支付系统设计与实现》专栏系列文章中的第(49)篇。