简介

  • 后端开发,基本设计规则

前言

  • 后端开发工程师,主要的工作就是:如何把一个接口设计好

接口参数校验

  • 入参出参校验是每个程序员必备的基本素养。你设计的接口,必须先校验参数。
  • 比如入参是否允许为空,入参长度是否符合你的预期长度。

修改老接口时,注意接口的兼容性

  • 如果需求是在原来的接口上修改,尤其是这个接口是对外提供服务的话,一定要考虑接口的兼容性 ```java 你加了一个参数C,就可以考虑这样处理:

//老接口 void oldService(A,B){ //兼容新接口,传个null代替C newService(A,B,null); }

//新接口,暂时不能删掉老接口,需要做兼容。 void newService(A,B,C){ … } ```

设计接口时,充分考虑接口的可扩展性

  • 要根据实际业务场景设计接口,充分考虑接口的可扩展性。

接口考虑是否需要防重处理

  • 如果前端重复请求,你的逻辑如何处理?是不是考虑接口去重处理

  • 当然,如果是查询类的请求,其实不用防重。
  • 如果是更新修改类的话,尤其是金融转账的,就要过滤重复请求了。

  • 推荐使用数据库的防重表,以唯一流水号作为主键或者唯一索引

重点接口,考虑线程池隔离

  • 一些登录,转账交易,下单等重要接口,考虑线程池隔离。
  • 如果所有业务都公用一个线程池,有些业务出bug导致线程阻塞打满的话,所有业务都被影响了。

  • 因此进行线程池隔离,重要业务多分配一点核心线程,就更好的保护业务。

调用第三方接口要考虑异常和超时处理

  • 异常处理
    • 比如,调用了别人的接口,如果异常了,怎么处理,是重试还是当作失败还是告警处理
  • 接口超时
    • 没办法预估对方接口一般多久返回,一般设置一个超时断开时间,以保护你的接口。
    • 一个生产问题:就是http调用不设置超时时间,最后响应进程假死,请求一直占着线程不释放,拖垮线程池
  • 重试次数

接口实现考虑熔断和降级

  • 当前互联网系统一般是分布式部署的。而分布式系统中经常会出现某个基础服务不可用,最终导致整个服务不可用的情况,这种现象被称为服务雪崩效应

  • 为了应对服务雪崩,常见的作法就是熔断和降级。

    • 最简单的是加开关控制,当下游系统出问题时,开关降级,不在调用下游系统,还可以选用开源组件 Hystrix

日志打印好,接口的关键代码一定要有日志

  • 入参之前打印一下参数
  • 接口调用之后,打印一下异常

接口的功能定义要具备单一性

  • 单一性是指接口做的事情比较单一,专一
    • 比如一个登陆接口,它做的事情就只是校验账户名密码,然后返回登陆成功以及userId即可。但是如果你为了减少接口交互,把一些注册、一些配置查询等全放到登陆接口,就不太妥
  • 其实这也是微服务一些思想,接口的功能单一

接口有些场景,使用异步更合适

  • 做异步的方式,简单的就是用线程池。还可以使用消息队列,就是用户注册成功后,生产者产生一个注册成功的消息,消费者拉到注册成功的消息,就发送通知

  • 不是所有的接口都适合设计为同步接口。比如你要做一个转账的功能,如果你是单笔的转账,你是可以把接口设计同步。用户发起转账时,客户端在静静等待转账结果就好。如果你是批量转账,一个批次一千笔,甚至一万笔的,你则可以把接口设计为异步。就是用户发起批量转账时,持久化成功就先返回受理成功。然后用户隔十分钟或者十五分钟等再来查转账结果就好。又或者,批量转账成功后,再回调上游系统

优化接口耗时,远程串行考虑改为并行调用

接口合并或者说 考虑批量处理思想

  • 数据库操作或者是 远程过程调用时,能够批量操作就不要for循环调用

  • kafka使用批量消息提升服务端处理能力

接口实现过程中,恰当使用缓存

  • 哪些场景适合使用缓存?
    • 读多,写少且数据时效要求越低的场景
  • 缓存用的好,可以承载更多的请求,提升查询效率,减少数据库的压力

考虑接口热点数据隔离性

  • 瞬时间的高并发,可能会打垮你的系统。可以做一些热点数据隔离。比如业务隔离,系统隔离,用户隔离,数据隔离等等。

可变参数配置化,比如红包皮肤切换等

接口考虑幂等性

  • 接口是需要考虑幂等性的。尤其抢红包、转账这些重要接口。最直观的业务场景,就是用户连着点击两次,你的接口有没有hold住。或者消息队列出现重复消费的情况,你的业务逻辑怎么控制?

  • 什么是幂等?
    • 计算机科学中,幂等表示一次和多次请求某一个资源应该具有同样的副作用,或者说,多次请求所产生的影响与一次请求执行的影响效果相同。
    • 大家别搞混哈,防重和幂等设计其实是有区别的。防重主要为了避免产生重复数据,把重复请求拦截下来即可。而幂等设计除了拦截已经处理的请求,还要求每次相同的请求都返回一样的效果。不过呢,很多时候,它们的处理流程、方案是类似的哈。
  • 接口幂等实现方案主要有8种:
    • select+insert+主键/唯一索引冲突
    • 直接insert + 主键/唯一索引冲突
    • 状态机幂等
    • 抽取防重表
    • token令牌
    • 悲观锁
    • 乐观锁
    • 分布式锁

读写分离,优先考虑读从库,注意主从延迟问题

  • 我们的数据库都是集群部署的,有主库也有从库,当前一般都是读写分离的。比如你写入数据,肯定是写入主库,但是对于读取实时性要求不高的数据,则优先考虑读从库,因为可以分担主库的压力。

  • 如果读取从库的话,需要考虑主从延迟的问题

接口注意返回的数据量,如果数据大需要分页

  • 一个接口返回报文,不应该包含过多的数据量。过多的数据量不仅处理复杂,并且数据量传输的压力也非常大。因此数量实在是比较大,可以分页返回,如果是功能不相关的报文,那应该考虑接口拆分

好的接口实现,离不开SQL优化

  • SQLL优化从这几个维度思考:
    • explain 分析SQL查询计划(重点关注type、extra、filtered字段)
    • show profile分析,了解SQL执行的线程的状态以及消耗的时间
    • 索引优化 (覆盖索引、最左前缀原则、隐式转换、order by以及group by的优化、join优化)
    • 大分页问题优化(延迟关联、记录上一页最大ID)
    • 数据量太大(分库分表、同步到es,用es查询)

代码锁的粒度控制好

  • 什么是加锁粒度呢?
    • 其实就是你要锁住的范围是多大

接口状态和错误需要统一明确

  • 提供必要的接口调用状态信息。比如你的一个转账接口调用是成功、失败、处理中还是受理成功等,需要明确告诉客户端。如果接口失败,那么具体失败的原因是什么。这些必要的信息都必须要告诉给客户端,因此需要定义明确的错误码和对应的描述。同时,尽量对报错信息封装一下,不要把后端的异常信息完全抛出到客户端。

接口要考虑异常处理

  • 实现一个好的接口,离不开优雅的异常处理。对于异常处理,提十个小建议吧:
    • 尽量不要使用e.printStackTrace(),而是使用log打印。因为e.printStackTrace()语句可能会导致内存占满。
    • catch住异常时,建议打印出具体的exception,利于更好定位问题
    • 不要用一个Exception捕捉所有可能的异常
    • 记得使用finally关闭流资源或者直接使用try-with-resource
    • 捕获异常与抛出异常必须是完全匹配,或者捕获异常是抛异常的父类
    • 捕获到的异常,不能忽略它,至少打点日志吧
    • 注意异常对你的代码层次结构的侵染
    • 自定义封装异常,不要丢弃原始异常的信息Throwable cause
    • 运行时异常RuntimeException ,不应该通过catch的方式来处理,而是先预检查,比如:NullPointerException处理
    • 注意异常匹配的顺序,优先捕获具体的异常

优化程序逻辑

  • 优化程序逻辑这块还是挺重要的,也就是说,你实现的业务代码,如果是比较复杂的话,建议把注释写清楚。还有,代码逻辑尽量清晰,代码尽量高效。

  • 你要使用用户信息的属性,你根据session已经获取到userId了,然后就把用户信息从数据库查询出来,使用完后,后面可能又要用到用户信息的属性,有些小伙伴没想太多,反手就把userId再传进去,再查一次数据库。。。我在项目中,见过这种代码。。。直接把用户对象传下来不好嘛。。

接口实现过程中,注意大文件,大事务,大对象

  • 读取大文件时,不要Files.readAllBytes直接读取到内存,这样会OOM的,建议使用BufferedReader一行一行来。
  • 大事务可能导致死锁、回滚时间长、主从延迟等问题,开发中尽量避免大事务。
  • 注意一些大对象的使用,因为大对象是直接进入老年代的,可能会触发fullGC

你的接口,需要考虑限流

  • 如果你的系统每秒扛住的请求是1000,如果一秒钟来了十万请求呢?换个角度就是说,高并发的时候,流量洪峰来了,超过系统的承载能力,怎么办呢?

  • 如果不采取措施,所有的请求打过来,系统CPU、内存、Load负载飚的很高,最后请求处理不过来,所有的请求无法正常响应。
  • 针对这种场景,我们可以采用限流方案。就是为了保护系统,多余的请求,直接丢弃。
  • 限流定义:
    • 在计算机网络中,限流就是控制网络接口发送或接收请求的速率,它可防止DoS攻击和限制Web爬虫。限流,也称流量控制。是+ 指系统在面临高并发,或者大流量请求的情况下,限制新的请求对系统的访问,从而保证系统的稳定性。
    • 可以使用Guava的RateLimiter单机版限流,也可以使用Redis分布式限流,还可以使用阿里开源组件sentinel限流

代码实现时,注意运行时异常

  • 日常开发中,我们需要采取措施规避数组边界溢出,被零整除,空指针等运行时错误。类似代码比较常见:

保证接口安全性

  • 如果你的API接口是对外提供的,需要保证接口的安全性。保证接口的安全性有token机制和接口签名

  • 机制身份验证方案还比较简单的,就是
    • 客户端发起请求,申请获取token。
    • 服务端生成全局唯一的token,保存到redis中(一般会设置一个过期时间),然后返回给客户端。
    • 客户端带着token,发起请求。
    • 服务端去redis确认token是否存在,一般用 redis.del(token)的方式,如果存在会删除成功,即处理业务逻辑,如果删除失败不处理业务逻辑,直接返回结果。
  • 接口签名的方式,就是把接口请求相关信息(请求报文,包括请求时间戳、版本号、appid等),客户端私钥加签,然后服务端用公钥验签,验证通过才认为是合法的、没有被篡改过的请求。

  • 除了加签验签和token机制,接口报文一般是要加密的。当然,用https协议是会对报文加密的。如果是我们服务层的话,如何加解密呢?
  • 可以参考HTTPS的原理,就是服务端把公钥给客户端,然后客户端生成对称密钥,接着客户端用服务端的公钥加密对称密钥,再发到服务端,服务端用自己的私钥解密,得到客户端的对称密钥。这时候就可以愉快传输报文啦,客户端用对称密钥加密请求报文,服务端用对应的对称密钥解密报文。
  • 有时候,接口的安全性,还包括手机号、身份证等信息的脱敏。就是说,用户的隐私数据,不能随便暴露

分布式事务,如何保证

  • 分布式事务:就是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。简单来说,分布式事务指的就是分布式系统中的事务,它的存在就是为了保证不同数据库节点的数据一致性。

  • 分布式事务的几种解决方案:

    • 2PC(二阶段提交)方案、3PC
    • TCC(Try、Confirm、Cancel)
    • 本地消息表
    • 最大努力通知
    • seata

事务失效的一些经典场景

  • 我们的接口开发过程中,经常需要使用到事务。所以需要避开事务失效的一些经典场景。
    • 方法的访问权限必须是public,其他private等权限,事务失效
    • 方法被定义成了final的,这样会导致事务失效。
    • 在同一个类中的方法直接内部调用,会导致事务失效。
    • 一个方法如果没交给spring管理,就不会生成spring事务。
    • 多线程调用,两个方法不在同一个线程中,获取到的数据库连接不一样的。
    • 表的存储引擎不支持事务
    • 如果自己try…catch误吞了异常,事务失效。
    • 错误的传播特性

掌握常用的设计模式

  • 把代码写好,还是需要熟练常用的设计模式,比如策略模式、工厂模式、模板方法模式、观察者模式等等。设计模式,是代码设计经验的总结。使用设计模式可以可重用代码、让代码更容易被他人理解、保证代码可靠性。

写代码时,考虑线性安全问题

  • 在高并发情况下,HashMap可能会出现死循环。因为它是非线性安全的,可以考虑使用ConcurrentHashMap。所以这个也尽量养成习惯,不要上来反手就是一个new HashMap();

接口定义清晰易懂,命名规范

  • 我们写代码,不仅仅是为了实现当前的功能,也要有利于后面的维护。说到维护,代码不仅仅是写给自己看的,也是给别人看的。所以接口定义要清晰易懂,命名规范。

接口的版本控制

  • 接口要做好版本控制。就是说,请求基础报文,应该包含version接口版本号字段,方便未来做接口兼容。其实这个点也算接口扩展性的一个体现点吧。
  • 比如客户端APP某个功能优化了,新老版本会共存,这时候我们的version版本号就派上用场了,对version做升级,做好版本控制。

注意代码规范问题

  • 注意一些常见的代码坏味道:
    • 大量重复代码(抽共用方法,设计模式)
    • 方法参数过多(可封装成一个DTO对象)
    • 方法过长(抽小函数)
    • 判断条件太多(优化if…else)
    • 不处理没用的代码
    • 不注重代码格式
    • 避免过度设计

保证接口正确性,其实就是保证更少的bug

学会沟通,跟前端沟通,跟产品沟通

  • 我把这一点放到最后,学会沟通是非常非常重要的。比如你开发定义接口时,一定不能上来就自己埋头把接口定义完了,需要跟客户端先对齐接口。遇到一些难点时,跟技术leader对齐方案。实现需求的过程中,有什么问题,及时跟产品沟通。
  • 总之就是,开发接口过程中,一定要沟通好~