后台接口设计思路

前言

之前面试被问到是如何进行接口设计的,我回答的是按前端需求设计,保证灵活可扩展。
后来发现很不全面,因此做个总结。

设计思路

1. 参数校验

  • 接口的入参需要进行校验,如sql注入、xss攻击,一般通过拦截器做统一处理

2. 接口规范

  • 字段命名统一规范
  • 返回统一的结构
  • 响应码:提前定义好响应码的含义
  • 提示信息
  • 返回值:是否为空,如果为空的时候是否返回默认值。

3. 接口拓展性

  • 灵活性:模块化和可重用。对接口逻辑进行抽象,设计成可重用的类或方法。
    抽象接口用统一接口处理一类事情,如:获取各种枚举值列表、发送各类消息通知接口、对象存储接口等。
  • 兼容性:新接口兼容旧接口,实现向后兼容。设计URL时引入版本号,方便版本控制。

4. 接口幂等性

  • 幂等性是什么?
    在编程中一个幂等操作的特点是其多次执行所产生的影响均与一次执行的影响相同。
    这个概念源于抽象代数,之后被应用于计算机领域。如:取绝对值,两次取反,取整运算等都是幂等操作。

  • 幂等场景?

    • 网络波动:因网络波动,可能会引起重复请求
    • 分布式消息消费:任务发布后,使用分布式消息服务来进行消费
    • 用户重复操作:用户在使用产品时,可能会无意的触发多笔交易,甚至没有响应而有意触发多笔交易
    • 未关闭的重试机制:因开发人员、测试人员或运维人员没有检查出来,而开启的重试机制(如Nginx重试、RPC通信重试或业务层重试等)
  • 幂等重要性

针对一个微服务架构,如果不支持幂等操作,那将会出现以下情况:

  • 电商超卖现象
  • 重复转账、扣款或付款
  • 重复增加金币、积分或优惠券

超卖现象

比如某商品的库存为1,此时用户1和用户2并发购买该商品,用户1提交订单后该商品的库存被修改为0,而此时用户2并不知道的情况下提交订单,该商品的库存再次被修改为-1这就是超卖现象。

究其深层原因,是因为数据库底层的写操作和读操作可以同时进行,虽然写操作默认带有隐式锁(即对同一数据不能同时进行写操作)但是读操作默认是不带锁的,所以当用户1去修改库存的时候,用户2依然可以都到库存为1,所以出现了超卖现象。

解决方案A:可以对读操作加上显式锁(即在select …语句最后加上for update)这样一来用户1在进行读操作时用户2就需要排队等待了。但问题来了,如果该商品很热门并发量很高那么效率就会大大的下降,如何解决呢?(解决方案B)

解决方案B:我们可以有条件有选择的在读操作上加锁,比如可以对库存做一个判断,当库存小于一个量时开始加锁,让购买者排队,这样一来就解决了超卖现象。

  • 如何实现幂等性?
1. select+insert+主键/唯一索引冲突

交易请求过来,我会先根据请求的唯一流水号 bizSeq字段,先select一下数据库的流水表
如果数据已经存在,就拦截是重复请求,直接返回成功;
如果数据不存在,就执行insert插入,如果insert成功,则直接返回成功,如果insert产生主键冲突异常,则捕获异常,接着直接返回成功。
流程图:

伪代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* 幂等处理
*/
Rsp idempotent(Request req){
Object requestRecord =selectByBizSeq(bizSeq);

if(requestRecord !=null){
//拦截是重复请求
log.info("重复请求,直接返回成功,流水号:{}",bizSeq);
return rsp;
}

try{
insert(req);
}catch(DuplicateKeyException e){
//拦截是重复请求,直接返回成功
log.info("主键冲突,是重复请求,直接返回成功,流水号:{}",bizSeq);
return rsp;
}

//正常处理请求
dealRequest(req);

return rsp;
}
2. 状态机幂等

很多业务表,都是有状态的,比如转账流水表,就会有0-待处理,1-处理中、2-成功、3-失败状态。转账流水更新的时候,都会涉及流水状态更新,即涉及状态机 (即状态变更图)。我们可以利用状态机实现幂等,一起来看下它是怎么实现的。

比如转账成功后,把处理中的转账流水更新为成功状态,SQL这么写:

1
update transfr_flow set status=2 where biz_seq=‘666’ and status=1;

流程图:

状态机是怎么实现幂等的呢?

  • 第1次请求来时,bizSeq流水号是 666 ,该流水的状态是处理中,值是 1 ,要更新为2-成功的状态 ,所以该update语句可以正常更新数据,sql执行结果的影响行 数是1,流水状态最后变成了2。
  • 第2请求也过来了,如果它的流水号还是 666 ,因为该流水状态已经2-成功的状态 了,所以更新结果是0,不会再处理业务逻辑,接口直接返回成功。
3. 抽取防重表

1的方案,是建立在业务流水表上bizSeq的唯一性上。很多时候,我们业务表唯一流水号希望后端系统生成,又或者我们希望防重功能与业务表分隔开来,这时候我们可以单独搞个防重表。当然防重表也是利用主键/索引的唯一性,如果插入防重表冲突即直接返回成功,如果插入成功,即去处理请求。

4. token令牌

token 令牌方案一般包括两个请求阶段:

  • 客户端请求申请获取token,服务端生成token返回
  • 客户端带着token请求,服务端校验token
    流程图如下:
  1. 客户端发起请求,申请获取token。
  2. 服务端生成全局唯一的token,保存到redis中(一般会设置一个过期时间),然后返回给客户端。
  3. 客户端带着token,发起请求。
  4. 服务端去redis确认token是否存在,一般用 redis.del(token) 的方式,如果存在会删除成功,即处理业务逻辑,如果删除失败不处理业务逻辑,直接返回结果。
5. 悲观锁(如select for update)

什么是悲观锁?

通俗点讲就是很悲观,每次去操作数据时,都觉得别人中途会修改,所以每次在拿数据的时候都会上锁。官方点讲就是,共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程。

悲观锁如何控制幂等的呢?就是加锁呀,一般配合事务来实现。

举个更新订单的业务场景:

假设先查出订单,如果查到的是处理中状态,就处理完业务,再然后更新订单状态为完成。如果查到订单,并且是不是处理中的状态,则直接返回

6. 乐观锁

悲观锁有性能问题,可以试下乐观锁。

什么是乐观锁?

乐观锁在操作数据时,则非常乐观,认为别人不会同时在修改数据,因此乐观锁不会上锁。只是在执行更新的时候判断一下,在此期间别人是否修改了数据。

怎样实现乐观锁呢?

就是给表的加多一列version版本号,每次更新记录version都升级一下(version=version+1)。具体流程就是先查出当前的版本号version,然后去更新修改数据时,确认下是不是刚刚查出的版本号,如果是才执行更新

比如,我们更新前,先查下数据,查出的版本号是version =1

1
select order_id,version from order where order_id='666'

然后使用version =1 和订单Id一起作为条件,再去更新

1
update order set version = version +1,status='P' where  order_id='666' and version =1

最后更新成功,才可以处理业务逻辑,如果更新失败,默认为重复请求,直接返回。
流程图如下:

7. 分布式锁

分布式锁实现幂等性的逻辑就是,请求过来时,先去尝试获得分布式锁,如果获得成功,就执行业务逻辑,反之获取失败的话,就舍弃请求直接返回成功。执行流程如下图所示:

  • 分布式锁可以使用Redis,也可以使用ZooKeeper,不过还是Redis相对好点,因为较轻量级。
  • Redis分布式锁,可以使用命令SET EX PX NX + 唯一流水号实现,分布式锁的key必须为业务的唯一标识哈
  • Redis执行设置key的动作时,要设置过期时间哈,这个过期时间不能太短,太短拦截不了重复请求,也不能设置太长,会占存储空间。

5.接口日志

接入统一日志,便于做错误排查和数据分析。

6.接口优化

接口优化可以提升系统的性能、可靠性和用户体验,从而更好地满足用户的需求。

  • 常见方法
    • 缓存: 使用缓存技术来存储接口的计算结果或数据,以避免重复计算或查询数据库。常见的缓存方案包括内存缓存、分布式缓存和CDN(内容分发网络)等。
    • 异步处理: 将耗时的操作(如IO操作、网络请求、计算密集型任务)转移到后台线程或者异步任务队列中处理,以避免阻塞主线程,提高接口的响应速度。
    • 使用索引: 在数据库中使用索引来加速查询操作,特别是对于频繁使用的查询字段和排序字段。合理设计索引可以大幅提高数据库查询的效率。
    • 并行处理: 将接口的处理任务分解为多个并行的子任务,并行处理这些子任务,以提高接口的处理效率和吞吐量。
      控制锁的颗粒度: 避免长事务锁,只锁住最少且必要的共享资源。

7.接口安全

接口安全的是保护系统和用户的数据安全的基础

  • 常见的措施
    • 对高频接口设置限流(滑动窗口、漏桶、令牌桶)
    • 对接口进行身份认证和鉴权

后台接口设计思路
https://liuxx1106.github.io/2024/04/07/java-interface-design/
作者
巨鹿
发布于
2024年4月7日
许可协议