如果让你设计一个接口,你会考虑哪些问题?

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 Tair(兼容Redis),内存型 2GB
简介: 接口设计需关注参数校验、扩展性、幂等性、日志、线程池隔离、异常重试、异步处理、查询优化、限流、安全性、锁粒度和避免长事务。入参与返回值校验确保数据正确性;考虑接口扩展性以适应不同业务需求;幂等设计防止重复操作;关键接口打印日志辅助问题排查;核心接口使用线程池隔离确保稳定性;异常处理中可采用重试机制,注意超时控制;适合异步的场景如用户注册后的通知;并行查询提升性能;限流保护接口,防止过载;配置黑白名单保障安全;适当控制锁粒度提高并发性能;避免长事务影响系统响应。

1.接口参数校验
接口的入参和返回值都需要进行校验。

入参是否不能为空,入参的长度限制是多少,入参的格式限制,如邮箱格式限制
返回值是否为空,如果为空的时候是否返回默认值,这个默认值需要和前端协商

2.接口扩展性
举个例子,比如用户在进行某些操作之后,后端需要进行消息推送,那么是直接针对这个业务流程来开发一个专门为这个业务流程服务的消息推送功能呢?还是说将消息推送整合为一个通用的接口,其他流程都可以进行调用,并非针对特定业务。
这个场景可能光靠说不是很能理解,大家想想策略工厂设计模式,是不是可以根据不同的策略,来选择不同的实现方式呢?再结合上面的这个例子,是否对扩展性有了进一步的理解呢?
3.接口幂等设计
什么是幂等呢?幂等是指多次调用接口不会改变业务状态,可以保证重复调用的结果和单次调用的结果一致
举个例子,在购物商场里面你用手机下单,要买某个商品,你需要去支付,然后你点击了支付,但是因为网速问题,始终没有跳转到
支付界面,于是你又连点了几次支付,那在没有做接口幂等的时候,是不是你点击了多少次支付,我们就需要执行多少次支付操作?
所以接口幂等到的是什么?防止用户多次调用同一个接口

对于查询和删除类型的接口,不论调用多少次,都是不会产生错误的业务逻辑和数据的,因此无需幂等处理
对于新增和修改,例如转账等操作,重复提交就会导致多次转账,这是很严重的,影响业务的接口需要做接口幂等的处理,跟前端约定好一个固定的token接口,先通过用户的id获取全局的token,写入到Redis缓存,请求时带上Token,后端做处理

4.关键接口日志打印
关键的业务代码,是需要打印日志进行监测的,在入参和返回值或者如catch代码块中的位置进行日志打印

方便排查和定位线上问题,划清责任
生产环境是没有办法进行debug的,必须依靠日志查问题,看看到底是出现了什么异常情况

5.核心接口要进行线程池隔离
分类查询啊,首页数据等接口,都有可能使用到线程池,某些普通接口也可能会使用到线程池,如果不做线程池隔离,万一普通接口出现bug把线程池打满了,会导致你的主业务受到影响

6.第三方接口异常重试
如果有场景出现调用第三方接口,或者分布式远程服务的话,需要考虑的问题

异常处理
比如你在调用别人提供的接口的时候,如果出现异常了,是要进行重试还是直接就是当做失败

请求超时
有时候如果对方请求迟迟无响应,难道就一直等着吗?肯定不是这样的,需要设法预估对方接口响应时间,设置一个超时断开的机制,以保护接口,提高接口的可用性,举个例子,你去调用别人对外提供的一个接口,然后你去发http请求,始终响应不回来,此时你又没设置超时机制,最后响应方进程假死,请求一直占着线程不释放,拖垮线程池。

重试机制
如果调用对外的接口失败了或者超时了,是否需要重新尝试调用呢?还是失败了就直接返回失败的数据?

7.接口是否需要采用异步处理
举个例子,比如你实现一个用户注册的接口。用户注册成功时,发个邮件或者短信去通知用户。这个邮件或者发短信,就更适合异步处理。总不能一个通知类的失败,导致注册失败吧。 那我们如何进行异步操作呢?可以使用消息队列,就是用户注册成功后,生产者产生一个注册成功的消息,消费者拉到注册成功的消息,就发送通知。

8.接口查询优化,串行优化为并行
假设我们要开发一个网站的首页,我们设计了一个首页数据查询的接口,这个接口需要查用户信息,需要查头部信息,需要查新闻信息
等等之类的,最简单的就是一个一个接口串行调用,那要是想要提高性能,那就采取并行调用的方式,同时查询,而不是阻塞
可以使用CompletableFuture(推荐)或者FutureTask(不推荐)
swift复制代码 Map> map = new HashMap<>();
List>>> completableFutureList =
categoryBOList.stream().map(category ->
CompletableFuture.supplyAsync(() -> getLabelBOList(category), labelThreadPool)
).collect(Collectors.toList());

completableFutureList.forEach(future -> {
try {
Map> resultMap = future.get(); //这里会阻塞
map.putAll(resultMap);
} catch (Exception e) {
e.printStackTrace();
}
});

    public Map<Long, List<SubjectLabelBO>> getLabelBOList(SubjectCategoryBO category) {...}

9.高频接口注意限流
自定义注解 + AOP
java复制代码@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimiter {
int value() default 1;
int durationInSeconds() default 1;
}

@Aspect
@Component
public class RateLimiterAspect {

private final ConcurrentHashMap rateLimiters = new ConcurrentHashMap<>();

@Pointcut("@annotation(RateLimiter)")
public void rateLimiterPointcut(RateLimiter rateLimiterAnnotation) {
}

@Around("rateLimiterPointcut(rateLimiterAnnotation)")
public Object around(ProceedingJoinPoint joinPoint, RateLimiter rateLimiterAnnotation) throws Throwable {
int permits = rateLimiterAnnotation.value();
int durationInSeconds = rateLimiterAnnotation.durationInSeconds();

// 使用方法签名作为 RateLimiter 的 key
String key = joinPoint.getSignature().toLongString();
com.google.common.util.concurrent.RateLimiter rateLimiter = rateLimiters.computeIfAbsent(key, k -> com.google.common.util.concurrent.RateLimiter.create((double) permits / durationInSeconds));

// 尝试获取令牌,如果获取到则执行方法,否则抛出异常
if (rateLimiter.tryAcquire()) {
return joinPoint.proceed();
} else {
throw new RuntimeException("Rate limit exceeded.");
}
}
}

@RestController
public class ApiController {

@GetMapping("/api/limited")
@RateLimiter(value = 10, durationInSeconds = 60) //限制为每分钟 10 次请求
public String limitedEndpoint() {
return "This API has a rate limit of 10 requests per minute.";
}

@GetMapping("/api/unlimited")
public String unlimitedEndpoint() {
return "This API has no rate limit.";
}
}

10.保障接口安全
配置黑白名单,用Bloom过滤器实现黑白名单的配置
具体代码不贴出来了,大家可以去看看布隆过滤器的具体使用
11.接口控制锁粒度
在高并发场景下,为了防止超卖等情况,我们会对共享资源进行加锁的操作来保证线程安全的问题,但是如果加锁的粒度过大,是会影响
到接口性能的。那什么是加锁粒度呢?举一个例子,你带了一封情书回家,但是不想被爸妈发现,然后你偷偷回到房间里放到一个可以锁
住的抽屉里面,而不用把房间的门锁给锁上。 无论是使用synchronized加锁还是redis分布式锁,只需要在共享临界资源加锁即可,不涉
及共享资源的,就不必要加锁。

锁粒度过大:
把方法A和方法B全部进行加锁,但是实际上我只是想要对A加锁,这就是锁粒度过大

scss复制代码void test(){
synchronized (this) {
B();
A();
}
}

缩小锁粒度

scss复制代码void test(){
B();
synchronized (this) {
A();
}
}

12.避免长事务问题
长事务期间可能伴随cpu、内存升高、严重时会导致服务端整体响应缓慢,导致在线应用无法使用
产生长事务的原因除了sql本身可能存在问题外,和应用层的事务控制逻辑也有很大的关系。

如何尽可能的避免长事务问题呢?
1.RPC远程调用不要放到事务里面
2.一些查询相关的操作如果可用,尽量放到事务外面
3.并发场景下,尽量避免使用@Transactional注解来操作事务,使用TransactionTemplate的编排式事务来灵活控制事务的范围

在原先使用@Transactional来管理事务的时候是这样的
scss复制代码@Transactional
public int createUser(User user){
//保存用户信息
userDao.save(user);
passCertDao.updateFlag(user.getPassId());
// 该方法为远程RPC接口
sendEmailRpc(user.getEmail());
return user.getUserId();
}

使用TransactionTemplat进行编排式事务
scss复制代码@Resource
private TransactionTemplate transactionTemplate;

public int createUser(User user){
transactionTemplate.execute(transactionStatus -> {
try {
userDao.save(user);
passCertDao.updateFlag(user.getPassId());
} catch (Exception e) {
// 异常手动设置回滚
transactionStatus.setRollbackOnly();
}
return true;
});
// 该方法为远程RPC接口
sendEmailRpc(user.getEmail());
return user.getUserId();
}

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
JavaScript API
接口封装如何实现?
接口封装如何实现?
|
4月前
|
Java
设计接口的几种方法
设计接口的几种方法
|
6月前
|
JSON 数据格式
如何创建接口,设计过接口
项目遵循Restful规范设计接口,请求路径基于资源命名,如查询用GET,新增用POST,修改用PUT,删除用DELETE。GET参数通过问号或路径传递,POST/PUT用JSON。统一的接口规范规定:返回数据多时,用VO过滤或整合数据。
45 0
|
程序员 C++
论接口的封装能力
论接口的封装能力
49 0
|
Java
接口特性
接口特性
87 1
|
SQL 负载均衡 Java
怎么设计一个高质量的接口API设计
什么是幂等性?对于同一笔业务交易,不管调用多少次,只会成功处理一次。二、幂等性设计我们转账业务为例,来说明一下这个问题,转账接口一定要做到幂等性,否则会出现重复转账的问题。调用转账接口从A中转100元资金给B,参数中会携带业务流水号biz_no和源账户A,目的账户B,和转账金额100,业务流水号biz_no是唯一的。转账接口实现有以下实现方式。
|
数据库 开发者
业务层设计与开发(业务层标准实现类) | 学习笔记
简介:快速学习业务层设计与开发(业务层标准实现类)
134 0
|
安全 前端开发 物联网
接口设计篇《怎么设计好的接口?》
这样设计接口【升职加薪】?
417 0
|
架构师 Java Go
第五章 接口3 -- 接口的设计原则
接口的设计原则有很多. 今天我们来研究两种. 后面在陆续研究 1. 开闭原则 2. 依赖倒置原则
357 0
第五章 接口3 -- 接口的设计原则