API项目6:接口调用次数的统计 && 网关

本项目是一个面向开发者的 API 平台,提供 API 接口供开发者调用。用户通过注册登录,可以开通接口调用权限,并可以浏览和调用接口。每次调用都会进行统计,用户可以根据统计数据进行分析和优化。管理员可以发布接口、下线接口、接入接口,并可视化接口的调用情况和数据。本项目侧重于后端,涉及多种编程技巧和架构设计层面的知识。

思考

后端项目现在有 backend、interface,为什么不在一个工程呢?

因为平台可以接入任何人开发的接口,不一定是同一个团队或者公司内部的项目。

接口调用次数的业务流程

需求:

  1. 用户每次调用接口成功,次数 + 1
  2. 给用户分配或者用户自主申请接口调用次数

业务流程:

  1. 用户调用接口(之前已完成)
  2. 修改数据库,调用次数 +1

实际上,整个接口调用次数加 1 的业务流程是嵌入在我们调用接口的业务流程中的。

首先用户在前端看到接口,然后他要开通接口获取调用次数,获取调用次数后发起调用。

在原有的调用接口成功基础上,再添加一个步骤,即在统计次数的位置进行记录。

设计库表

既然每次调用接口成功都要加 1 次数,那我们需要区分是哪个用户调用了哪个接口

根据需求分析,这是一个多对多的关系,一个用户可以调用多个接口,而一个接口也可以被多个用户调用。

设计一个新的表来存储用户和接口之间的关系,可称之为"用户调用接口关系表"。

-- 用户调用接口关系表
create table if not exists yuapi.`user_interface_info`
(
    `id` bigint not null auto_increment comment '主键' primary key,
  	`userId` bigint not null comment '调用用户 id',
    `interfaceInfoId` bigint not null comment '接口 id',
   	`totalNum` int default 0 not null comment '总调用次数',
  	`leftNum` int default 0 not null comment '剩余调用次数',
  	`status` int default 0 not null comment '0-正常,1-禁用',
  	`createTime` datetime default CURRENT_TIMESTAMP not null comment '创建时间',
    `updateTime` datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',
    `isDelete` tinyint default 0 not null comment '是否删除(0-未删, 1-已删)'
) comment '用户调用接口关系';

总调用次数:指用户从第一次开通接口开始至今累计的调用次数。

剩余调用次数:指用户每次购买接口后剩余的可调用次数。

状态字段(status):决定是否允许其调用特定接口。

  • 为了增加安全性,我们可以考虑为每个用户设置一个状态字段(status),来决定是否允许其调用特定接口。这样,如果用户触发了某些规则或违反了规定,我们可以将其状态设置为不允许调用该接口。通过添加一个状态字段,我们可以灵活地管理用户对接口的访问权限。例如,当用户违反规则时,我们可以限制其对某些接口的调用,而对其他接口仍然保持开放。这个状态字段可以帮助我们实现精确的接口访问控制。

💡 不建议将调用时间直接写入数据库

如果每个用户每次调用接口都要在数据库中新增一条数据,那么数据库表可能会变得非常庞大。建议使用日志来存储这些调用信息,可以将其记录在文件中,使用类似 ELK 等工具进行日志存储和分析。

OK创建表~~~

mybatisX 插件生成代码

表创建完成之后,现在去生成增删改查代码;

鼠标右键 user_interface_info 表 → MybatisX-Generator。

模块路径选择当前路径即可,它会生成到 src/main/java 下

点击 Next 点击 Finish

多了一个 generator包,代码都生成在这里。

给实体对象中的 isDelete 加上 @TableLogic,表示是逻辑删除的字段。

迁移生成的代码

把 UserInterfaceInfo.java 拖到entity包下。

把 UserInterfaceInfoMapper.java 拖到mapper包下。

把 UserInterfaceInfoServiceImpl.java 拖到impl包下。

把 UserInterfaceInfoService.java 拖到service包下。

迁移完了,把generator包删除,鼠标右键generator包 → Delete。

然后我们再写一下 controller,复制 InterfaceInfoController.java;

粘贴到controller包下,并重命名为 UserInterfaceInfoController。

按[Ctrl+R]替换,把 interfaceInfo 替换成userInterfaceInfo

把 InterfaceInfo 替换成UserInterfaceInfo

修改代码逻辑

为了确保用户每次调用接口成功时次数加一,而不是随意增加某个接口的调用次数,我们应该将增加调用次数的逻辑放在接口调用成功的业务逻辑之后。这样可以保证接口调用的准确性和安全性,防止滥用和不当操作

💡有按用户限制 QPS 的思路吗?

既然我们已经能够统计用户调用接口的次数,我们再统计用户调用接口的调用时间,然后根据用户在某一个时间段内的调用次数来限制。

例如,我们可以每隔一秒统计一次用户的接口调用次数,如果用户在这一秒内的调用次数超过了某个设定的频率,那么我们就给他禁用接口调用。

UserInterfaceInfoAddRequest

管理员要去给用户开通内的接口调用关系,接口调用次数的话,需要填写什么呢?

  • 用户 id,知道哪个用户开通
  • 接口 id
  • 总调用次数
  • 剩余调用次数
  • 状态,状态默认是正常(所以可以不用填)

UserInterfaceInfoQueryRequest

管理员会根据哪些字段查询用户和接口的调用关系呢?

  • id,根据 id 查询,但次数可能比较少,保留一下
  • 用户 id,查某个用户开通哪些接口的调用权限
  • 接口 id,这个接口有哪些用户调用
  • 总调用次数,可以留着,一般是用范围查询
  • 剩余调用次数
  • 状态,常用的查询的状态

UserInterfaceInfoUpdateRequest 

管理员会去修改用户接口调用关系的哪些内容呢?

  • id,更新必须指定一条数据
  • 用户 id(不会改这个,都给用户开通,再改用户就不合理了)
  • 总调用次数
  • 剩余调用次数
  • 状态

实现用户调用成功次数加一

我们之前校验用户是否有权限调用,都直接写在了模拟接口项目里。

调用成功之后我们要做的事情是什么?

就是去调用我们的 userInterfaceInfoService.save,把这条调用记录添加到数据库中

或者调用 updateUserInterfaceInfo 在原有的调用次数的统计之上再加一。

两种情况:

  • 第一种情况是用户没有这个调用次数记录,那么我们需要创建一条新的记录。
  • 第二种情况是用户已经有了调用次数记录,我们需要在现有的次数基础上加 1。

现在我们要开发调用次数加一的功能。之前的updateUserInterfaceInfo方法是以管理员的视角去更新调用次数记录,但并没有包含调用次数加一的逻辑,所以我们需要在这里进行开发。4

在 UserInterfaceInfoService 中补充次数加一的功能

并实现

@Override
public boolean invokeCount(long interfaceInfoId, long userId) {
    // 判断(其实这里还应该校验存不存在,这里就不用校验了,因为它不存在,也更新不到那条记录)
    if (interfaceInfoId <= 0 || userId <= 0) {
        throw new BusinessException(ErrorCode.PARAMS_ERROR);
    }
    // 使用 UpdateWrapper 对象来构建更新条件
    UpdateWrapper<UserInterfaceInfo> updateWrapper = new UpdateWrapper<>();
    // 在 updateWrapper 中设置了两个条件:interfaceInfoId 等于给定的 interfaceInfoId 和 userId 等于给定的 userId。
    updateWrapper.eq("interfaceInfoId", interfaceInfoId);
    updateWrapper.eq("userId", userId);
    // setSql 方法用于设置要更新的 SQL 语句。这里通过 SQL 表达式实现了两个字段的更新操作:
    // leftNum=leftNum-1和totalNum=totalNum+1。意思是将leftNum字段减一,totalNum字段加一。
    updateWrapper.setSql("leftNum = leftNum - 1, totalNum = totalNum + 1");
    // 最后,调用update方法执行更新操作,并返回更新是否成功的结果
    return this.update(updateWrapper);
}

在这里需要注意的是,由于用户可能会瞬间调用大量接口次数,为了避免统计出错,需要涉及到事务和锁的知识。在这种情况下,如果我们是在分布式环境中运行的,那么可能需要使用分布式锁来保证数据的一致性。

存在问题

现在的需求是在用户调用每个接口成功后,需要调用invokeCount方法,来给当前接口的调用次数加 1。

但是这样做可能会带来一个问题:如果每个方法调用成功后,返回结果之前都要调用一次invokeCount方法,会显得非常繁琐。

解决方案1:AOP

我们可以使用 AOP(面向切面编程)来实现这个功能。AOP 允许我们在原有业务逻辑的基础上,增加额外的操作,而不需要改动原有代码。

具体来说,我们可以通过 AOP 切面、拦截器或者过滤器来实现这个统计次数的逻辑。在接口调用成功后,AOP 切面或拦截器可以自动触发调用次数加 1 的方法,从而实现统一的统计功能。

AOP 切面是我们推荐的方案,它可以将统计次数的逻辑从业务逻辑中解耦出来,并实现统一的处理。在学习 AOP 切面的过程中,也要重点学习 Spring 的核心特性之一。 

AOP 切面的缺点只存在于单个项目中,如果每个团队都要开发自己的模拟接口,那么都要写一个切面。

考虑到我们的项目架构,我们希望实现一种通用的统计方案,可以统一处理所有项目的接口调用情况。因此,我们决定采用网关来实现这个功能。

架构图:

将统计次数的功能再抽出来一层。我们可以将统计次数的逻辑放在一个公共的位置,无论哪个模拟接口被调用,都会经过这个统一的统计次数逻辑。 

通过这种设计,我们实现了一个统一的网关来处理不同项目的请求,用户和开发者都不需要关心具体的细节,简化了操作,提高了系统的可用性和可维护性

网关

网关的应用场景

路由:路由实际上就像一个中转站,类似于我们的路由器。

  • 假设用户要访问某个接口 A,但用户不需要直接调用接口 A,而是通过我们的网关统一接收用户的请求。网关记录了用户调用的接口,并将其转发到对应的项目和接口进行处理,有点类似于前台接待。
  • 路由在这里起到了转发的作用。举个例子,假设我们有接口 A 和接口 B,网关会记录这些信息,并根据用户访问的地址和参数,将请求转发到对应的接口(服务器/集群)。这种转发过程就叫做路由。

参考文档:The After Route Predicate Factory

统一鉴权:判断用户是否有权限进行操作,无论访问什么接口,都统一判断权限,不用重复写

  • 之前的鉴权逻辑写在 interface 这个项目中的方法里,用于判断用户是否有权限进行操作。但是如果每个方法都要单独写鉴权逻辑,显然是不可行的。所以将鉴权逻辑和统计次数一样,抽取出来放到网关里面。
  • 在网关中,鉴权的重点是实现统一鉴权。无论用户要访问哪个接口,网关都会统一判断权限,不需要重复编写鉴权逻辑。网关的作用在很多方面都是强调统一性,将重复的逻辑进行抽象和集中。

统一处理跨域:网关统一处理跨域,不用在每个项目里单独处理

参考文档:Global CORS Configuration

统一业务处理:把一些每个项目中都要做的通用逻辑放到上层(网关),统一处理,比如本项目的次数统计。

  • 项目中可能存在一些通用的逻辑,比如统计调用次数和鉴权等。如果把这些逻辑写在每个项目的方法里,就会导致重复代码和维护困难
  • 为了避免重复的代码,将这些统一的业务逻辑放到网关层面进行处理。让项目中的方法更加清晰、简洁,同时也避免了重复劳动。

访问控制:黑白名单,比如限制 DDOS IP

  • 访问控制,实际上也是一种权限控制机制。它与鉴权有一些区别。鉴权通常指授权,即判断用户是否有访问某种资源的权限。而黑白名单则主要用于判断每个用户是否可以访问特定资源,它是一种与业务逻辑独立的控制方式。
  • 举个例子,如果有人恶意刷我们的流量,进行 DDOS 攻击,我们可以将这些恶意 IP 加入黑名单,限制它们的访问。这样,这些 IP 就无法访问我们的服务,从而保护了我们的接口和服务不受恶意攻击。

发布控制:灰度发布,比如上线新接口,先给新接口分配 20% 的流量,老接口 80%,再慢慢调整比重。

  • 举个例子,假设团队开发了一个名为项目 A 的接口 A,现在我们要对接口 A 进行升级,推出一个新版本的接口 A-V2。但我们并不确定新版本是否稳定可靠,所以我们想先让一部分用户试用这个新接口。我们可以将流量按照比例划分,比如 80% 的流量继续访问旧版本的接口 A,而 20% 的流量则引导到新版本的接口 A-V2。这样就实现了灰度测试的效果。然后我们会观察 V2 的表现,如果测试没有问题,我们可以逐步增加流量比例,比如 50%、70%、80%,直到 100%。最后,当我们确认新版本的接口稳定可靠时,就可以完全替换掉旧版本,下线接口 A。
  • 这个流量分配的过程就是发布控制,而它通常是在网关层进行。因为网关是整个流量的入口,所以它可以担当请求流量分配的角色。

参考文档:The Weight Route Predicate Factory

流量染色:给请求(流量)添加一些标识。通常通过设置请求头来实现。

举个例子。假设现在有一个用户要访问我的接口。但是有一个问题,我希望用户不能绕过网关直接调用我的接口,我想要防止这种情况发生。那么我应该如何防止绕过网关呢?

  • 一个方法是要确定请求的来源。我们可以为用户通过网关来的请求打上一个标识,比如添加一个请求头 source=gateway。只要经过网关的请求,网关就会给它打上 source=gateway 的标识。接口 A 就可以根据这个请求头来判断,如果请求没有 source=gateway 这个标识,就直接拒绝掉它。这就是流量染色的一种应用。流量染色还有其他应用,比如区分用户的来源,这和鉴权是不同的概念,属于不同的应用场景。
  • 另外一个常见的应用是用于排查用户调用接口时出现的问题。我们为每个用户的每次调用都打上一个唯一的 traceid,这是分布式链路追踪的概念。通过这个 traceid,当出现问题时,下游服务可以根据 traceid 追踪到具体的请求,从而逐层排查问题。这也是流量染色的作用之一。

参考文档:TheAddRequestHeaderGatewayFilterFactory

💡 用户怎么绕过网关?

用户只要知道服务器的 IP 地址,尤其你的服务又在外网上公开时,用户就可以直接绕过网关进行访问。这种情况下,我们不能让这些接口直接对外暴露,而需要网关来隐藏这些接口信息。

统一接口保护:涉及多种方式,例如限制请求信息、数据脱敏、降级、限流、超时时间等措施,强调了统一的管理。

  • 对于接口保护,我们可以通过网关统一限制请求的大小,确保接收到的请求在合理的范围内,避免恶意请求或者大量请求对后端服务造成不必要的负担。

限制请求参考文档:requestheadersize-gatewayfilter-factory

  • 有些接口原本会在响应头中返回服务器的 IP 地址等敏感信息,但通过网关的操作,我们可以将这些敏感信息抹掉或删除,保护服务器的隐私和安全。

信息脱敏参考文档:the-removerequestheader-gatewayfilter-factory

  • 另一个重要的保护机制是降级。当接口调用失败或接口下线时,我们可以采取降级逻辑,比如向用户提示接口已下线,或引导用户访问其他功能,从而确保用户始终能够得到有意义的响应。

降级(熔断)参考文档:fallback-headers

  • 限流也是接口保护的重要手段。通过限制用户每分钟或每秒钟访问接口的次数,可以避免过多的请求对服务器造成压力。

限流参考文档:the-requestratelimiter-gatewayfilter-factory

  • 设置超时时间也是保护服务器的一种方式,当接口调用时长超过设定时间,强制中断请求,保证服务器的稳定性。

超时时间参考文档:http-timeouts-configuration

网关的分类

  1. 全局网关(接入层网关):主要功能是负载均衡,将大量的请求平均分摊到系统中的多台机器上。它通常不涉及过多的业务逻辑,而更注重处理请求日志等任务。
  2. 业务网关(微服务网关):更多地关注业务逻辑,例如统计次数、鉴权等,同时也会负责转发请求到具体的业务处理单元

技术选型

Kong 是专门为 API 服务提供的网关。但是不推荐使用 Kong 的原因是,它有商业版本和免费版本,而免费版本可能会有一些限制。

Nginx 是比较推荐的全局网关,也称为接入层网关。Nginx 可以部署前端和后端,还能提供文件访问服务等多种功能,非常灵活。我们甚至可以在 Nginx 中编写业务逻辑,但是并不推荐这样做,因为它并不像 Spring Cloud Gateway 那样方便。

Spring Cloud Gateway ,取代了 Zuul。Zuul 的架构设计有一些问题,例如并发量有限。而 Spring Cloud Gateway 则使用了 NIO 和多路复用等技术,底层采用了 native 和 react 模型,因此性能更高。最大优点是它允许我们使用 Java 代码来编写逻辑。

Spring Cloud Gateway

访问:

  1. 官网
  2. 官方文档

核心概念

路由:用于根据请求的网址进行转发

断言:一组规则、条件,用来确定如何转发路由

过滤器:对请求进行一系列的处理,比如添加请求头、添加请求参数

在这里我们定义的是一个匹配器,或者更明确地说,在 Spring Cloud Gateway 中它被称为"断言"。 

两种配置方式:

  1. 配置式(方便、规范,推荐

  2. 编程式(灵活、相对麻烦)

如何配置这个路由、过滤器和断言呢?Spring Cloud Gateway 提供了两种方式。第一种方式是配置式或者叫声明式配置,就是在 application.yml文件中写配置(推荐)。

它的配置式分为两种,一种是简单的参数,像下图这样(推荐):

spring:
  cloud:
    gateway:
      routes:
      - id: after_route
        uri: https://example.org
        predicates:
        - After=2017-01-20T17:42:47.789-07:00[America/Denver]
  • spring.cloud.gateway.routes: 这是配置路由的属性。
  • - id: after_route: 这是路由的唯一标识符,用于区分不同的路由。
  • uri: https://example.org: 这是路由将请求转发到的目标 URI,即请求经过此路由后将被转发到 https://example.org 这个地址。
  • predicates: 这是断言的配置属性,用于定义请求是否满足路由条件。
  • - Cookie=mycookie,mycookievalue: 这是一个断言条件,它指定了请求必须具有名为 mycookie 的 Cookie,且其值必须为 mycookievalue,才能匹配这个路由。

通过这个配置,当满足请求带有特定mycookie 的 Cookie 并且其值为mycookievalue时,请求将被路由到https://example.org这个目标 URI。

请求流程

  1. 客户端发起请求
  2. Handler Mapping:根据断言,去将请求转发到对应的路由
  3. Web Handler:处理请求(一层层经过过滤器)
  4. 实际调用服务

断言:

  1. After 在 xx 时间之后
  2. Before 在 xx 时间之前
  3. Between 在 xx 时间之间
  4. 请求类别
  5. 请求头(包含 Cookie)
  6. 查询参数
  7. 客户端地址
  8. 权重

过滤器:

基本功能:对请求头、请求参数、响应头的增删改查。

  1. 添加请求头
  2. 添加请求参数
  3. 添加响应头
  4. 降级
  5. 限流
  6. 重试
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值