对接第三方系统的难点与注意事项

Overview

需要对接第三方的场景

第一类:自己需要做一件事情,但是纯靠自己要动用很多资源(产品、设计、开发、测试等),还要花较长的时间。如果恰好市面上有成熟的解决方案,我们完全可以把专业的活交给专业的人,利用他们的能力来解决自己的问题,同时让对方获得他们想要的,达到双赢。

举个例子,新启动一个电商平台,为了方便用户支付。一般会对接微信和支付宝,还要支持各家的银行卡做支付。这多家银行,一家家的对接、一家家的谈费率、一家家的联调、一家家的结算,想想都可怕。

如果此时正好有一家这样的公司,他们已经和这些银行最好了对接,并且提供标准化的接口,也支持后台去查流水记录。那完全就可以直接和他们合作,对接这家的系统。

第二类:自己不具备第三方的能力或者需要为第三方提供服务时。

举个例子,信贷类风控需要一些第三方的数据扩展风控的维度;或者为第三方提供营销获客时,需要对接第三方的API接口。

业务关键问题

考虑需要需要哪些接口

  1. 我需要提供什么: 我们需要和对方约定好我们提供哪些内容,对方才可以有效的处理并返回给我们需要的结果。比如我想查询物流轨迹,那只需要我们传给对方运单号即可(有的公司还需要快递公司)。
  2. 需要别人返回给我什么内容: 比如,我希望反馈给我物流轨迹,每一个时间点,从哪里发到了哪里;还需要当前运单的状态,是已揽件还是拒签。
  3. 异常情况如何处理: 比如支付成功还是失败,失败有哪些原因等,拿到不同的结果我们应该怎么办?
  4. 我方提供哪些接口,对方提供哪些接口,接口大致的数量
  5. 业务流程定义

重复做同样的对接工作

大部分人都不愿意做重复劳动,当发现业务朝着要重复对接的方向走的时候,是时候可以考虑做标准化接口了,将自己的能力标准化出去,类似于开放平台,让别人按照你的标准来做。

备注:如果自己公司还没牛逼到这个地步,那还是乖乖对接对方的接口吧

如何计费

1.是按单次收费还是按用户收费

单次收费,用一次给一次钱;按用户收费,一个用户给一次钱,下次同一个用户就不收费。类似于广告计费的CPC和CPA,按次的单价和按用户的单价大部分情况不一样,就根据自己的业务情况去推算是按次还是按用户划算。

2.是定期统计结算还是对方开个账户实时扣款?

定期统计结算,双方要存在一个对账过程,对账OK财务给对方结款。开账户实时扣款,系统化一些,但要注意余额不足的时候要及时充钱。

3.对方是开套餐包还是无限量供应?

套餐包,给你多少次服务,收你多少钱,你每用一次,套餐就减少一点,跟移动流量包的意思差不多。

技术关键问题

通讯标准

  • 确定协议 确认接口对接的网络协议:https/http 端口号 或 tcp 端口号 Webservice
  • 确定请求格式 数据传参+响应格式为:application/json,数据访问方式 POST请求
  • 考虑是否需要安全考虑,比如内网,外网一定要有认证机制
  • 幂等校验方面,确保本公司接口和三方公司接口都有唯一校验功能,防止重复提交
  • 重试机制方面,一定要确认是否需要接口调用失败后的重试机制,保证数据传输的最终一致性。

异常处理

一般情况下,异常结构应该区分为三大类:

  • RpcException:调用异常
  • BusinessException:业务异常
  • Exception:其他程序异常

对于不同的第三方服务,什么情况下对应什么异常,可能会有不同的划分标准,一般情况下有以下规则:

  • Http StatusCode == 500 时为RpcException
  • 业务响应码 != 统一成功响应码 时为BusinessException
  • 其他异常不作归类

一致性

对于涉及到支付、退款等有下单概念的接口或涉及到状态问题时,则需要考虑到一致性的问题。一般情况下有以下要求(第三方服务也叫上游)由于第三方服务一般不受控,这里说的一致性往往只能是最终一致性

  • 游有的数据,本地一定要有
  • 上游状态与本地每条数据的状态相同 (或者状态可以相对来说一一映射)

事务发起

  • 确定接口当中的唯一标识是哪个字段,通常是requestNo,这个字段的值将上游数据和本地数据进行一一对应,上游存在的requestNo,本地必须存在

  • 划分状态

    1PENDING:本地数据已创建,未发起接口请求
    2UNCONFIRMED:本地数据已创建,不知道接口请求发起了没有,等待回查
    3PROCESSING:接口请求已发起,并且上游已响应,等待回查确认最终状态
    4SUCCESS:终态,业务已成功
    5FAIL:终态,业务已失败
    6DEAD:终态,本地数据已创建,接口死活请求不了,上游也查不到对应数据,不要了,根据实际情况也可以归类为FAIL
    

    这里的流程可以概述为:

    11)发起上游接口前,生成一个全局唯一的requestNo,该条数据的状态为PENDING,并且入库提交。
    22)请求上游接口没有异常的情况下:
    3  如果允许的话,同步处理状态,更新状态入库。
    4  否则,直接更新为PROCESSING,表示请求上游已成功,等待进一步确认状态。
    53)请求上游接口遇到RpcException,更新为PROCESSING,表示请求上游已成功,等待进一步确认状态。
    64)请求上游接口遇到BusinessException,更新为FAIL,表示请求上游已成功,等待进一步确认状态。(这里可能根据不同的业务返回码,处理为PROCESSING,等待进一步确认)
    75)处理过程中遇到其他Exception,更新为UNCONFIRMED,不能确定是否已请求上游,等待进一步确认状态,这里可以概述为本地事务处理失败,即保存到本地数据库时失败。
    

    如果你对上游的信任度较低,可以直接将PROCESSING状态也合并为UNCONFIRMED通一由事务回查处理

    下面用一段伪代码来描述接口调用的流程:

     1// 开启本地事务
     2startTrans();
     3Order order = new Order();
     4// 唯一请求号
     5String requestNo = UUID();
     6order.setRequestNo(requestNo);
     7order.setState(OrderState.PENDING);
     8order.save();
     9 // 提交本地事务
    10commit();
    11
    12try {
    13    startTrans();
    14    RpcResponse rpcRes = rpcService.requestToRpcCreateOrder(...);
    15    order.setState(OrderState.PROCESSING); 
    16
    17    // 如果你的接口可以同步返回业务状态
    18    if(rpcRes.getState() == 'SUCCESS') {
    19        order.setState(OrderState.SUCCESS); 
    20    }
    21    if(rpcRes.getState() == 'FAIL') {
    22        order.setState(OrderState.FAIL); 
    23    }
    24
    25    order.save();
    26    commit();
    27} catch(RpcException e) {
    28    startTrans();
    29    // 认为是处理中,等待后续回查
    30    order.setState(OrderState.PROCESSING);
    31    order.save();
    32    commit();
    33} catch(BusinessException e) {
    34    startTrans();
    35    // 认为是失败
    36    order.setState(OrderState.FAIL);
    37    // 失败时,建议记录rpc响应参数
    38    order.setRpcResponseCode(e.getCode());
    39    order.setRpcResponseMsg(e.getMsg());
    40    order.save();
    41    commit();
    42} catch(Exception e) {
    43    startTrans();
    44    // 认为是待确认,等待后续回查
    45    order.setState(OrderState.UNCONFIRMED);
    46    order.save();
    47    commit();
    48}
    

事务回查(重试)

经过上面的流程,数据会剩下UNCONFIRMED 和 PROCESSING 两种状态,因此对这两种状态进行进一步确认,保证数据到达终态。

事务回查有几种实现方式:

  1. 利用定时器扫描数据库状态为UNCONFIRMED或PROCESSING的数据
    • 保证数据库有索引
    • 如果requestNo字段也有索引,则可利用覆盖索引机制缩短查询时间,查询上游数据状态一般只需要requestNo
  2. 把UNCONFIRMED或PROCESSING数据的requestNo存入Redis,再利用定时器处理
  3. 利用队列,将UNCONFIRMED 和 PROCESSING塞在回查队列中

实际上,假如你的rpc请求不需同步返回出去,推荐使用具有事务机制的消息队列,否则利用队列方案需要考虑复杂度的上升程度

那么UNCONFIRMED 和 PROCESSING分别怎么处理呢

  • 对应PROCESSING,处理思路很简单,因为这种状态上游肯定能够返回对应的状态(实际上有的上游并不一定),只要查询到对应状态更新为SUCCESS或FAIL即可

  • 对应UNCONFIRMED需要区分上游数据不存在的情况,也就是说上面的事务发起流程当中,上游没有收到我们的请求,那么我们需要根据业务情况进行处理:

    • 重新发起这个请求(要确保上游接口是否幂等,否则要自己处理)
    • 更新为DEAD,抛弃这个请求

    如果上游存在该记录,则视为PROCESSING情况处理即可

如果你的上游提供异步处理通知,则可按照同样的思路完成事务回查这个阶段

总结

本文简单总结了一下对接第三方服务接口时需要考虑的几个问题:通讯标准、异常处理、一致性,实际处理时通常会分为RPC层Service层来处理,RPC层封装通讯标准、异常处理的问题,Service层处理一致性问题。

关于分布式应用下的幂等与重试

业务幂等需要从几个方面去控制:

  • 接口层,服务提供方需要引入订单号与订单状态机机制,保证重复提交的请求服务端不会重复交易。
  • 数据库层,引入联合索引机制,防止数据重复。
  • 高并发场景下,还需要引入分布式锁机制(或者数据库本身的机制)来防止重复交易。

参考

[幂等实例](