一、背景
API接口设计在微服务开发的过程中太重要了,一旦发布出去了,客户端也好,调用方也好,再想让他们升级或者改动的难度非常大。
这要求我们在定义接口的时候,需要慎之又慎,不可马虎。
二、目标
- 1、可扩展性好
- 2、单一职责
- 3、隔离性
- 4、幂等性
- 5、接口的并发
- 6、一致性
二、设计思路
1、可扩展性
后期迭代的过程中,难免会增加一些参数,当然得是非必填的,否则之前的调用就会报错。
- 新增的非必填参数,加在query中,不要写在path里。(尽可能保证rest api的uri path不要太长,因为rest风格的接口是基于资源的角度)
// 反例
// 删除题目
@DeleteMapping("/api/v1/answerRoom/{answerRoomId}/stu/{stuId}/task/{taskId}/topic/{topicId}")
在path中带入了太多的PathVariable参数,意味着他们是必填的,不仅导致路径太长,而且破坏了可扩展性。
// 正例
// 删除题目,它的目标对象是题目ID,不跟其他无关的领域做强耦合。
@DeleteMapping("/api/v1/topic/{topicId}")
你可能会反问说,题目一定和它的上级领域相关的啊,离不开任务或答疑室,也离不可题目所属的学生ID。所以上面的接口设计,也是正确的啊。
接口设计没有对错之分,只有原则的把握度的问题。熟好熟劣,自己推敲再三。
2、单一职责
有时候接口需要拆分为多个接口,有时候又可能需要合并抽象为一个接口。
2.1、商品的相关接口
image.png上下架、修改库存等从商品的编辑接口抽离出来。
2.2、积分的发放
用户获得积分的途径有多种,拆分为用户购买积分、奖励发放积分和管理后台手动发放给用户等。
2.3、聚合支付
支付平台需要将多种支付渠道(微信支付、支付宝支付和不同的银行渠道)的支付请求,合并为一个抽象接口。而不是微信支付一个接口、支付宝支付又是另一个接口,以支付渠道为维度的话,虽然隔离性是好,但是业务对接方要吐槽不停。
支付平台还需要将多个对接的业务方,也合并为一个接口。
总之,对外的支付请求接口,就一个聚合接口。
3、隔离性
image.png保证接口的粒度尽可能小,比如和支付方对接,我们都需要提供回调接口给到第三方支付。这时候,和上面的支付请求接口相反,往往是按支付渠道拆分为多个回调接口。
@PostMapping("/api/v1/notify/abc")
@PostMapping("/api/v1/notify/alipay")
@PostMapping("/api/v1/notify/hzbank")
@PostMapping("/api/v1/notify/icbc")
@PostMapping("/api/v1/notify/wxpay")
当然,入口拆分开来,是因为他们回调的参数体差异很大,所以我们需要把共性的处理逻辑,放在一个抽象类里,实现代码的复用。
4、幂等性
我们说一个接口做到了幂等,必须满足可以重试(不建议给调用方直接报错提示)
实现幂等的方式有很多,一般在post请求体中增加一个token字段 。接口在处理业务逻辑前锁定这个token对象即可。可以把token入库,处理完业务,再删除token。 当然也可以不删除token,这将视你怎么使用了。
- token机制,因为网络的不可靠性,可能发生重试,所以需要我们的接口能够对重试进行兼容:要么报错提示说不要重复提交,要么直接返回已处理。
5、接口的并发
在对接口的性能摸底的时候,期望的响应时间内,当时的qps并发量为多少。
常见的实现方式有:
- 无锁
- 乐观锁
- 分布式锁
- 数据库的悲观锁
一般建议你使用基于版本号的乐观锁,因为它的粒度最小,易于控制。
这里举一个分布式锁的例子,支付系统的回调接口是对外暴露出去的,防止被人攻击,或者第三方支付的频繁回调,我们往往是会对支付流水号进行一个锁定。好处是可以利用支付订单的状态机机制,避免重复处理支付回调。(如果是已支付,则返回)而因为在分布式的场景下,支付网关是有多个节点,想要锁定支付流水号,必须是使用分布式锁了。
6、一致性
这里的一致性,是说一个接口方法的所有操作而言。如果全部是操作事务性的数据库,则可以利用数据库的事务来保证。如果还涉及调用外部服务的接口,则需要通过补偿机制和持久化的重试来做到尽可能的一致性。
不太建议你引入分布式事务框架来做,更多地倾向于本地日志表。
- 数据库的事务机制
- 本地日志表的重试
- 补偿机制
这里不会详细讲解上述,只是给你一个提醒,设计和实现接口的时候,有没有满足一致性的要求。
上文不满足事务数据库的事务机制的情况,除了典型的微服务之间的接口调用外,还有redis、rabbitmq等中间件的操作,也是无法支持事务。
当然,后者想要满足接口的一致性,就显得更加困难了。这里推荐你对重要业务,做到必要的补偿。
使用xxl-job分布式的定时任务,对可能出错的业务做补偿,也可用来和业务对接方做定期的对账。
四、和接口设计的其他思路
将查询类的接口和操作类的接口,区分开来。操作类的接口,一般复杂在处理的表及字段比较多,但是它的qps比较低。查询类的接口,它是复杂在拼接数据,所以我们建议是做数据异构,查询异构后的数据。
1、分库分表
提高写入的qps,结合接口的qps来具体确定是否引入分库分表方案。
2、读写分离
- 数据库的主从模式
操作在主库 ,查询在从库。和下面的数据异构方案一样,都存在一定的同步延迟。 - 数据异构
将操作和查询分离,利用数据异构的思路,既保证操作业务的简单,又适应复杂查询的需求。比如我们的商品服务,查询使用ES,操作使用关系型数据库mysql。
网友评论