科普文:软件架构设计之【Resutful Api设计梳理-上】

363 篇文章 1 订阅
345 篇文章 1 订阅

1、逻辑梳理

除非是特别简单的需求,一定要在需求分析的时候去看看代码逻辑,看看数据关系,接口调用关系,对工作量以及实现方式有初步的想法,做到心中有数,遇事不慌。否则,开发的时候,才知道一个点变成了三个点,加班加点都干不完。
外部交互相关的需求,也要及时拉通产品去沟通协调,为开发铺路。如果要提测了,外部接口文档都没给,那还搞啥呀,是吧。
这里也给大家提供一些建议,希望能够提高大家的开发效率。

SequenceDiagram

一款强大的时序图生成器,可以很方便了解接口的调用关系。

准备开发备忘录

主要可以分为三部分,设计文档、开发笔记和上线准备。
设计文档:开发之前可以用来接口设计文档,复杂的流程需要梳理流程图;
开发笔记:开发过程中记录问题,记录重要配置参数,修改的项目,分支,接口依赖关系,生产配置等内容。
上线准备:不要等到上线之前,还要花很多时间找上线代码。要是周期长,改了哪些项目都忘了吧。提前准备好相关的配置、SQL脚本等。

2、参数校验

后端不校验,轻则和前端扯皮,重则直接导致生产异常,甚至可能出现大事故。所以为了省事,还是花点时间做一做参数校验吧。慢就是快,也不需要节省这一点时间。
这方面,基本上引入spring-boot-starter-validation这个包,使用相关注解就可以完成90%的工作。

3、接口的向上、向下兼容

考虑是否影响老接口,是都有其他的调用方?是否适合后续扩展。
对应不熟悉的接口,建议多看看调用方,如果是HTTP接口,可以找前端看看调用方,或者如果有血缘关系工具或者精准测试工具,最好理一理关系。如果不清楚调用方的情况下,最好在不影响现有功能的前提下进行修改。

我记得组内曾经就出现过,同事修改了PC端的接口,影响到了移动端的功能,因为当时没有移动端的需求,便没有回归,最后导致了生产事故,紧急上线修复。

可以多用用下面这个工具:
Find Usages
idea很重要的工具,查找调用方,不熟悉的接口,一定要多看看,如果方向调用方不止一个,一定不要忘了去看看影响范围。

4、接口防重

接口防重,主要是针对页面的表单提交,如果不进行控制,容易因为用户的误操作或者网络延迟导致同一请求被发送多次,进而造成重复的数据记录。一般,前端需要对按钮进行置灰等操作,但是后端也需要处理,不能完全依赖前端。
当然,只有在短暂的时间内,相同内容的提交才算重复,在大的时间范围内来看,可能用户就是需要提交相同的请求。

实现方式
一般基于自定义注解和redis分布式锁实现。
注意
这种一般是针对页面的接口,如果是外部调用的接口,尽量不要做防重校验,而是在逻辑上、数据库层面进行控制,因为外部调用可能很频繁,短时间内很容易出现重复的调用。

‌接口防重是指防止在一定的时间内多次请求同一接口,避免产生重复的数据或操作。这种情况通常发生在客户端没有做限流处理,或者服务端没有对重复请求进行有效拦截时。例如,在日常开发中,‌CRUD操作在业务系统中普遍存在,如果没有做任何处理,同一秒内多次请求同一接口,可能会导致‌数据库中存在大量重复数据,这不仅影响用户体验,还会增加服务器的压力。‌

常见的接口防重处理方法及其优缺点如下:

  1. 前台传递唯一值:在请求接口时传递一个唯一值,然后在接口中判断该唯一值是否已被消费过。这种方法局限性较高,前台必须传递唯一值,且需要配置大量不需要拦截的方法。‌
  2. 采用SpringAop理念:通过AOP(面向切面编程)实现请求的拦截和防重。这种方法可以在不污染源代码的情况下,实现统一防重处理,业务解耦。
  3. 请求锁:使用注解@RequestLock,在接口方法上添加该注解,实现请求防抖锁,防止前端重复提交导致的错误。‌
  4. 检查唯一ID:在业务提交时检查唯一ID是否存在,如果存在则提示用户不要重复提交。‌
  5. ‌RSA加密签名:应用端生成RSA私钥加密签名串,后台获取签名进行RSA公钥解密,比较时间戳防止请求重放。‌
  6. ‌数据库锁机制:利用数据库本身的锁机制保证交易留痕的唯一性,适用于高并发场景。‌

具体实现细节和代码示例:

  • 前台传递唯一值:在请求接口时传递一个唯一值(如UUID),然后在服务端判断该唯一值是否已被使用。
  • 采用Spring AOP理念:通过AOP实现请求的拦截和防重,具体实现可以参考Spring AOP的相关文档和示例代码。
  • 请求锁:使用注解@RequestLock,在接口方法上添加该注解,实现请求防抖锁。
  • 检查唯一ID:在业务提交时检查唯一ID是否存在,如果不存在则进行业务处理,否则提示用户不要重复提交。
  • RSA加密签名:应用端生成RSA私钥加密签名串,后台获取签名进行RSA公钥解密,比较时间戳防止请求重放。
  • 数据库锁机制:利用数据库本身的锁机制保证交易留痕的唯一性,具体实现可以参考数据库事务和锁的相关文档。

5、接口限流、熔断、降级

对于分布式系统来说,为了避免某个应用不可用导致整个系统的不可用,需要降低服务间的强关联,避免服务雪崩,保证尽可能多的服务可用。

主要使用sentinel和Hystrix组件,在控制接口的请求数量、及时关闭对外部接口的调用。

科普文:微服务之Spring Cloud Alibaba组件熔断过载保护器Sentinel_spring-cloud-starter-alibaba-sentinel jdk 版本-CSDN博客

科普文:微服务之Spring Cloud 熔断保护组件Hystrix实践小结:调优和踩坑-CSDN博客

 

实战:微服务之Spring Cloud 熔断保护组件Hystrix熔断、请求合并、请求缓存实操-CSDN博客

科普文:微服务之Spring Cloud 熔断保护组件Hystrix流程和原理的官方介绍-CSDN博客 

6、外部调用 超时、异常、重试

外部调用的时候需要考虑超时、异常和重试几种场景。

同样参考上面的Hystrix和sentainel。

超时

有个接口是和外部团队进行交互的场景,因为外部接口异常,导致有一批数据推送失败。最后只能去找日志,手动推送。像这种场景,可以和对方确认,是否支持重复推送,其次,在自身的系统里,也可以增加简单的重试机制。比如基于redis进行参数的缓存,并指定持久化机制等。

异常

对于重要的报错,是否需要进行短信或者站内信等方式进行通知?

重试

接口请求失败或者出现暂时性错误,重试是提高接口成功率的重要手段。查询外部接口,最好是支持重试,设置超时时间。

场景的重试机制包括循环重试、并发框架异步重试、消息队列重试、redis重试以及使用Spring Retry库等方式。

  • 循环重试:判断接口的返回值,报错时再次调用。

  • 异步重试:比如CompletableFuture框架并提供了失败时的处理方法,可以再次调用

  • 消息队列和redis重试:需要把请求参数封装成消息放到消息队列或者redis的队列中,进行消费,失败后继续重试,成功后删除消息。

  • Spring Retry 库可以很方便地实现接口请求的重试机制,基于注解指定重试接口和处理策略。

@Retry(value = Exception.class, backoff = @Backoff(delay = 1200,multiplier = 1.5),maxAttempts = 3)
public void notifyToiletryCreateOrder(String originalCode) throws ServiceException {
    // 代码逻辑

}

7、日志打印

虽然日志不是越多越好,但是必要的日志对解决问题是很重要的。
常见的需要打印日志的地方:

  • 入参和出参,特别是外部调用的入参、出参;

  • 重要逻辑的数据信息;

  • 异常信息;

同时建议设置traceId,使用skywalking等工具实现接口的链路追踪,便于快速处理问题。

8、异步

首先,肯定得使用线程池,其次,要根据不同的场景,对线程池进行隔离。避免某个线程池打满影响其他业务场景。
工作中曾经遇到过一个发短信的场景,调用方没有采用异步的方式,后来因为短信服务积压,导致短信发送接口超时,直接导致了调用方同时超时,影响后续流程。
这个场景就出现了好几个问题:

  1. 外部调用,不需要返回值的场景,应该采用异步的方式,不能影响自身业务;

  2. 对于外部调用,最好能够统一操作,和自身逻辑拆分开,比如短信调用,可以放在最后。

比如报表接口,需要统一账单金额、收款金额、退款金额,就可以采用不同的线程进行数据统计,最后汇总,减少响应时间。

for (String communityCode : communityCodes) {
    // 分项目计算
    CompletableFuture.supplyAsync(() -> sumOne(), ThreadPool);
    // 汇总结果集
    totalResult.add(communityResult);
}

CompletableFuture<Void> future = CompletableFuture.allOf(totalResult.toArray(completableFutures));
future.join();
// 汇总结果
result.addAll(Stream.of(completableFutures).map(CompletableFuture::join).flatMap(Collection::stream).collect(Collectors.toList()));
// 后续处理
...

9、影响内存或者数据库的场景

当我们处理数据的时候,要对处理的数据量有一定的了解,内存占用,耗时等,不要在生产环境直接导致事故。场景的比如大对象,长事务问题要注意。

大对象

常见的容易内存溢出的场景便是导出、导入、大表查询。
尽量使用分批查询的操作。对于查一个列表的接口,一定要注意查询的量,有可能以前没有出现问题,随着数据量的增长,数据量陡增。

长事务问题

@Transactional注解简化了我们使用事务的工作,但是也让我们不注意事务的一些问题。

比如长事务问题等。长事务,顾名思义就是运行时间比较长,操作的数据比较多的事务。
比如下面这种,在事务中嵌套了很多RPC调用、HTTP接口调用这种非数据库的操作,正常的情况下问题不大,对流程没有影响,但是如果后续的接口超时,事务一直不提交,就会一直占用数据库连接,影响数据库性能,影响从库数据同步。

最好不要用@Transactional声明式事务,而是用编程式事务,事务操作完成,即使提交,避免长事务;实战:spring项目中不推荐用自带的声明式事务@Transactional_spring 不加transactional-CSDN博客

科普文:Java基础系列之【spring的注解式事务和编程式事务】-CSDN博客

@Transactional
public int createOrder(String orderNo){

    // 数据库操作
    Order  order = orderDbStorage.selectByCondition(orderNo)
    orderDbStorage.save(order);
    orderItemDbStorage.save(order.getItems());

    // RPC调用
    sendRpc();

    // 消息推送
    sendMessage();

    return order.getId();

}

这种就要尽可能把事务的代码拆出来,减少事务的范围。

public int createOrder(String orderNo){
    Order  order = orderDbStorage.selectByCondition(orderNo);
    // 拆分数据库操作作为一个独立的事务
    int  id = OrderBService.createOrder(order );
    sendRpc();
    sendMessage();
    return id;
}
@Transactional
public int createOrder( Order  order){
    orderDbStorage.save(order);
    orderItemDbStorage.save(order.getItems());
    return order.getId();
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

-无-为-

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值