前言
作为后端研发人员,平时需要经常做服务接口设计及开发,需要与前端进行接口联调,排查生产环境线上问题。因此,后端工程师,核心基本工作就是如何把一个接口设计好,以下梳理一些接口设计开发规范及注意事项,希望对大家有所帮助。
1、接口参数校验(入参和出参)
接口入参和出参都需要进行校验,
① 例如入参是否不能为空,入参数据长度,入参是否符合预期规则,很多bug由于未做参数校验导致,对于可能改变的参数建议设计为对象类型;
② 对于返回值,当返回值为空时是否返回为空串、空对象、空数组,需要与前端约定好。
2、接口老版本兼容性
C端服务接口,可能移动端发版不会强升级或者存在前后端上线时间差异,就会导致线上环境存在使用老版本的用户,如果新添加了参数,需要考虑前端未传入时给默认值情况;例如
//老接口
void oldMethod(A,B){
//兼容新接口,传个null或其他默认值代替参数C
newService(A,B,null);
}
//新接口,暂时不能删掉老接口,需要做兼容。
void newMethod(A,B,C){
...
}
3、接口扩展性考虑
① 例如业务中,在用户调用拨打电话接口之后会进行消息推送,是直接就开发一个消息推送功能,还是将消息推送梳理为一个通用流程,在所有需要使用的地方进行调用即可,保留扩展性;
② 消息推送流程,设计为通用流程,同时采用接口定义,可以扩展实现多种消息推送方式。
graph LR
传入参数 --> 构建目标用户群 --> 调用消息中心接口发送 --> 记录发送结果
4、接口防重处理
① 对于查询类型、删除类型接口,不论调用多少次,都是不会产生错误的业务数据,因此不用做防重处理;
② 对于新增和修改,例如转账或者提现类接口,重复提交就会多次转账和提现,影响业务需要做防重处理,让前端传入请求序列号,可以采用redis、LRUMap、数据库防重表、分布式锁等处理。
graph LR
id获取全局请求token --> 写入redis缓存 --> 请求时带上token --> 后端删除 --> 再次请求提示重复
5、核心接口,线程池隔离
登录接口、首页数据接口、转账提现接口等,都可能使用到线程池,某些普通接口也会使用线程池,如果不做线程池隔离,普通接口出bug线程池打满,会导致登录等主要业务受到影响。
graph TB
线程池隔离
subgraph 核心接口
登录接口 --- 首页加载
end
subgraph 普通接口
写入日志 --- 消息推送
end
6、关键接口,日志打印
关键业务代码,需要打印日志进行保驾护航,在入参和出参位置或者其他关键位置,良好的日志打印具有如下好处:
① 方便排查定位线上问题,划清问题责任;
② 生产环境不能直接debug,必须依靠日志查问题和具体异常。
7、三方接口异常、重试、超时
如果调用第三方接口,或者分布式远程服务的的话,需要考虑:
① 异常处理
比如,你调别人的接口,如果异常了,怎么处理,是重试还是当做失败还是告警处理。
② 接口超时
没法预估对方接口一般多久返回,一般设置个超时断开时间,以保护你的接口。之前见过一个生产问题,就是http调用不设置超时时间,最后响应方进程假死,请求一直占着线程不释放,拖垮线程池。
③ 重试次数
你的接口调失败,需不需要重试?重试几次?需要站在业务上角度思考这个问题
8、接口功能单一性原则
单一性是指接口做的事情比较单一、专一。比如一个登陆接口,它做的事情就只是校验账户名密码,然后返回登陆成功以及userId即可。但是如果你为了减少接口交互,把一些注册、一些配置查询等全放到登陆接口,就不太妥。
其实这也是微服务一些思想,接口的功能单一、明确。比如订单服务、积分、商品信息相关的接口都是划分开的。将来拆分微服务的话,是不是就比较简便啦。
9、接口部分场景采用异步处理
举个简单的例子,比如你实现一个用户注册的接口。用户注册成功时,发个邮件或者短信去通知用户。这个邮件或者发短信,就更适合异步处理。因为总不能一个通知类的失败,导致注册失败吧。
至于做异步的方式,简单的就是用线程池。还可以使用消息队列,就是用户注册成功后,生产者产生一个注册成功的消息,消费者拉到注册成功的消息,就发送通知。
graph LR
id2生产者生产消息 -- 注册成功消息 --> 存储端 --发送通知-->消费者消费
10、接口查询优化,串行改为并行
假设我们设计一个APP首页的接口,它需要查用户信息、需要查banner信息、需要查弹窗信息等等。那你是一个一个接口串行调,还是并行调用呢?
可以使用CompletableFuture 并行调用提高性能。
// 查询获奖经历
LambdaQueryWrapper<RewardExp> rewardExpQuery = new LambdaQueryWrapper<RewardExp>()
.eq(RewardExp::getResumeId, resume.getId())
.eq(RewardExp::getDelFlag, NORMAL)
.orderByDesc(RewardExp::getDate);
CompletableFuture<List<RewardExp>> rewardExpFuture = CompletableFuture.supplyAsync(() ->
rewardExpMapper.selectList(rewardExpQuery)
);
// 查询资格证书
LambdaQueryWrapper<Credential> credentialQuery = new LambdaQueryWrapper<Credential>()
.eq(Credential::getResumeId, resume.getId())
.eq(Credential::getDelFlag, NORMAL)
.orderByDesc(Credential::getDate);
CompletableFuture<List<Credential>> credentialFuture = CompletableFuture.supplyAsync(() ->
credentialMapper.selectList(credentialQuery)
);
// 查询工种
LambdaQueryWrapper<ResumeJobs> jobsQuery = new LambdaQueryWrapper<ResumeJobs>()
.eq(ResumeJobs::getResumeId, resume.getId()).eq(ResumeJobs::getDelFlag, NORMAL);
CompletableFuture<List<ResumeJobs>> jobsFuture = CompletableFuture.supplyAsync(() ->
resumeJobsMapper.selectList(jobsQuery)
);
CompletableFuture.allOf(rewardExpFuture, credentialFuture, jobsFuture).join();
11、接口合并与批处理
数据库操作或或者是远程调用时,能批量操作就不要for循环调用。一个简单例子,我们平时一个列表明细数据插入数据库时,不要在for循环一条一条插入,建议一个批次几百条,进行批量插入。同理远程调用也类似想法,比如你查询营销标签是否命中,可以一个标签一个标签去查,也可以批量标签去查,那批量进行,效率就更高。
//反例
for(int i=0;i<n;i++){
remoteSingleQuery(param)
}
//正例
remoteBatchQuery(param);
12、接口性能、Sql优化
我们做后端的,写好一个接口,离不开SQL优化。
SQL优化从这几个维度思考:
① explain 分析SQL查询计划(重点关注type、extra、filtered字段)
② 索引优化 (覆盖索引、最左前缀原则、隐式转换、order by以及group by的优化、join优化)
③ 大分页问题优化(延迟关联、记录上一页最大ID)
④ 数据量太大(分库分表、同步到es,用es查询)