注册

什么?你设计接口什么都不考虑?

后端接口设计


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


image.png


1.接口参数校验


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



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

2.接口扩展性


举个例子,比如用户在进行某些操作之后,后端需要进行消息推送,那么是直接针对这个业务流程来开发一个专门为这个业务流程服务的消息推送功能呢?还是说将消息推送整合为一个通用的接口,其他流程都可以进行调用,并非针对特定业务。


这个场景可能光靠说不是很能理解,大家想想策略工厂设计模式,是不是可以根据不同的策略,来选择不同的实现方式呢?再结合上面的这个例子,是否对扩展性有了进一步的理解呢?


3.接口幂等设计


什么是幂等呢?幂等是指多次调用接口不会改变业务状态,可以保证重复调用的结果和单次调用的结果一致


举个例子,在购物商场里面你用手机下单,要买某个商品,你需要去支付,然后你点击了支付,但是因为网速问题,始终没有跳转到


支付界面,于是你又连点了几次支付,那在没有做接口幂等的时候,是不是你点击了多少次支付,我们就需要执行多少次支付操作?


所以接口幂等到的是什么?防止用户多次调用同一个接口



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

image.png


4.关键接口日志打印


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



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

5.核心接口要进行线程池隔离


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


image.png


6.第三方接口异常重试


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



  • 异常处理

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


  • 请求超时

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


  • 重试机制

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



7.接口是否需要采用异步处理


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


image.png


8.接口查询优化,串行优化为并行


假设我们要开发一个网站的首页,我们设计了一个首页数据查询的接口,这个接口需要查用户信息,需要查头部信息,需要查新闻信息


等等之类的,最简单的就是一个一个接口串行调用,那要是想要提高性能,那就采取并行调用的方式,同时查询,而不是阻塞


可以使用CompletableFuture(推荐)或者FutureTask(不推荐)


        Map<Long, List<SubjectLabelBO>> map = new HashMap<>();
      List<CompletableFuture<Map<Long, List<SubjectLabelBO>>>> completableFutureList =
      categoryBOList.stream().map(category ->
              CompletableFuture.supplyAsync(() -> getLabelBOList(category), labelThreadPool)
      ).collect(Collectors.toList());

      completableFutureList.forEach(future -> {
          try {
              Map<Long, List<SubjectLabelBO>> resultMap = future.get(); //这里会阻塞
              map.putAll(resultMap);
          } catch (Exception e) {
              e.printStackTrace();
          }
      });
       
public Map<Long, List<SubjectLabelBO>> getLabelBOList(SubjectCategoryBO category) {...}

9.高频接口注意限流


自定义注解 + AOP


@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<String, RateLimiter> 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加锁,这就是锁粒度过大



void test(){
   synchronized (this) {
      B();
      A();
  }
}


  • 缩小锁粒度

void test(){
      B();
   synchronized (this) {
      A();
  }
}

12.避免长事务问题


长事务期间可能伴随cpu、内存升高、严重时会导致服务端整体响应缓慢,导致在线应用无法使用


产生长事务的原因除了sql本身可能存在问题外,和应用层的事务控制逻辑也有很大的关系。



  • 如何尽可能的避免长事务问题呢?

    1.RPC远程调用不要放到事务里面


    2.一些查询相关的操作如果可用,尽量放到事务外面


    3.并发场景下,尽量避免使用@Transactional注解来操作事务,使用TransactionTemplate的编排式事务来灵活控制事务的范围



在原先使用@Transactional来管理事务的时候是这样的


@Transactional
public int createUser(User user){
   //保存用户信息
   userDao.save(user);
   passCertDao.updateFlag(user.getPassId());
   // 该方法为远程RPC接口
   sendEmailRpc(user.getEmail());
   return user.getUserId();
}

使用TransactionTemplat进行编排式事务


@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();
}

作者:radient
来源:juejin.cn/post/7343548913034133523

0 个评论

要回复文章请先登录注册