从0到1构造自定义限流组件

构造,自定义,限流,组件 · 浏览次数 : 237

小编点评

**关于限流组件的简单封装和使用** **4、动态设置令牌数量通过DUCC配置令牌数量** 通过DUCC配置,我们可以获取指定limitKey的令牌数量,并获取失败则采用默认数量。 **5、后续其他配置** 其他属性也可以通过DUCC动态的配置,例如设置默认值、获取指定值等。 **示例代码:** ```java @Data @Slf4j public class RateLimitConfig { private Map<String, Integer> limitConfig; // ...其他配置 ... } public double getPermitsPerSecond(SysRateLimit sysRateLimit) { // ...获取令牌桶数量 ... } ``` **其他说明:** * 具体设置令牌数量的代码需要根据具体的需求进行修改。 * 我们可以使用不同的策略来实现限流,例如设置默认值、使用降级逻辑等。 * DUCC配置可以动态配置其他属性,例如设置默认值、获取指定值等。

正文

一 背景

在系统高可用设计中,接口限流是一个非常重要环节,一方面是出于对自身服务器资源的保护,另一方面也是对依赖资源的一种保护措施。比如对于 Web 应用,我限制单机只能处理每秒 1000 次的请求,超过的部分直接返回错误给客户端。虽然这种做法损害了用户的使用体验,但是它是在极端并发下的无奈之举,是短暂的行为,因此是可以接受的。

二 设计思路

常见的限流有2种思路

  • 第一种是限制总量,也就是限制某个指标的累积上限,常见的是限制当前系统服务的用户总量,例如:某个抢购活动商品数量只有 100 个,限制参与抢购的用户上限为 1 万个,1 万以后的用户直接拒绝。

  • 第二种是限制时间量,也就是限制一段时间内某个指标的上限,例如 1 分钟内只允许 10000 个用户访问;每秒请求峰值最高为 10 万。

三 限流算法

目前实现限流算法主要分为3类,这里不详细展开介绍:

1)时间窗口

固定时间窗口算法是最简单的限流算法,它的实现原理就是控制单位时间内请求的数量,但是这个算法有个缺点就是临界值问题。
为了解决临界值的问题,又推出滑动时间窗口算法,其实现原理大致上是将时间分为一个一个小格子,在统计请求数量的时候,是通过统计滑动时间周期内的请求数量。

2)漏斗算法

漏斗算法的核心是控制总量,请求流入的速率不确定,超过流量部分益出,该算法比较适用于针对突发流量,想要尽可能的接收全部请求的场景。其缺点也比较明显,这个总量怎么评估,大小怎么配置,而且一旦初始化也没法动态调整。

3)令牌桶算法

令牌桶算法的核心是控制速率,令牌产生的速度是关键,不断的请求获取令牌,获取不到就丢弃。该算法比较适用于针对突发流量,以保护自身服务资源以及依赖资源为主,支持动态调整速率。缺点的话实现比较复杂,而且会丢弃很多请求。

四 实现步骤

我们自定义的这套限流组件有是基于guava RateLimiter封装的,采用令牌桶算法以控制速率为主,支持DUCC动态配置,同时支持限流后的降级措施。接下来看一下整体实现方案

1、自定义RateLimiter Annotation标签

这里主要对限流相关属性的一个定义,包括每秒产生的令牌数、获取令牌超时时间、降级逻辑实现以及限流开关等内容

@Documented
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface SysRateLimit {

    /**
     * 每秒产生的令牌数 默认500
     *
     * @return
     */
    double permitsPerSecond() default 500D;

    /**
     * 获取令牌超时时间 默认100
     *
     * @return
     */
    long timeout() default 100;

    /**
     * 获取令牌超时时间单位 默认毫秒
     *
     * @return
     */
    TimeUnit timeUnit() default TimeUnit.MILLISECONDS;

    /**
     * 服务降级方法名称 Spring bean id
     *
     * @return
     */
    String fallbackBeanId() default "";

    /**
     * 限流key 唯一
     *
     * @return
     */
    String limitKey() default "";
}

2、基于Spring Aspect 构造切面

首先就是我们需要构造一个Aspect切面用于扫描我们自定义的SysRateLimit标签

@Slf4j
@EnableAspectJAutoProxy
@Aspect
public class SysRateLimitAspect {
    
    /**
     * 自定义切入点
     */
    @Pointcut("@annotation(com.jd.smb.service.ratelimiter.annotation.SysRateLimit)")
    public void pointCut() {

    }

    /**
     * 方法前执行限流方案
     *
     * @param joinPoint
     * @return
     * @throws Throwable
     */
    @Around("pointCut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        // 如果未获取到对象,直接执行方法
        if (signature == null) {
            return joinPoint.proceed();
        }

        try {
            Method method = joinPoint.getTarget().getClass().getDeclaredMethod(signature.getName(), signature.getMethod().getParameterTypes());
            // 获取注解对象
            SysRateLimit sysRateLimit = method.getAnnotation(SysRateLimit.class);
            if (sysRateLimit == null) {
                return joinPoint.proceed();
            }
            
        } catch (Exception e) {
            // todo log
        }
        return joinPoint.proceed();
    }
}

获取自定义SysRateLimit标签的各种属性

 // 限流key
String limitKey = sysRateLimit.limitKey();
if (StringUtils.isBlank(limitKey)) {
    return joinPoint.proceed();
}
// 令牌桶数量
double permitsPerSecond = sysRateLimit.permitsPerSecond();
// 获取令牌超时时间
long timeout = sysRateLimit.timeout();
// 获取令牌超时时间单位
TimeUnit timeUnit = sysRateLimit.timeUnit();

将我们自定义的SysRateLimiter 和 Guava RateLimiter 进行整合

  1. 首先我们需要构造一个全局Map,用于存储我们开启限流的方法,key就是我们定义的limitKey, value就是我们转换后的Guava RateLimiter
 /**
 * 存储RateLimiter(key: limitKey value:RateLimiter )
 */
private static final Map<String, RateLimiter> LIMITER_MAP = new ConcurrentHashMap<>();
  1. 接着就是核心逻辑:这里首先从我们创建的Map中获取Guava RateLimiter,获取不到就创建RateLimiter.create(permitsPerSecond) ;然后调用RateLimiter.tryAcquire()尝试获取令牌桶,获取成功则执行后续的逻辑,这里重点获取失败后,我们需要执行我们的降级方法。(注意:Guava RateLimiter 有很多API,这里我们不展开讨论,后续会针对Guava限流的源码进行详细的解析)
RateLimiter rateLimiter;
// Map中是否存在 存在直接获取
if (LIMITER_MAP.containsKey(limitKey)) {
    rateLimiter = LIMITER_MAP.get(limitKey);
} else {
    // 不存在创建后放到Map中
    rateLimiter = RateLimiter.create(permitsPerSecond);
    LIMITER_MAP.put(limitKey, rateLimiter);
}
// 尝试获取令牌
if (!rateLimiter.tryAcquire(timeout, timeUnit)) {
    // todo 限流后降级措施
    return this.fallBack(sysRateLimit, joinPoint, signature);
}

降级方案执行

上面我们在获取令牌桶超时后,需要执行我们的降级逻辑,怎么做呢?也很简单,我们在定义SysRateLimiter的时候有个fallBackBeanId,这个就是我们执行降级逻辑的bean对象Id,需要我们提前进行创建。接着我们看一下是怎么实现的。

    /**
     * 执行降级逻辑
     *
     * @param sysRateLimit
     * @param joinPoint
     * @param signature
     * @return
     */
    private Object fallBack(SysRateLimit sysRateLimit, ProceedingJoinPoint joinPoint, MethodSignature signature) {
        String fallbackBeanId = sysRateLimit.fallbackBeanId();
        // 当没有配置具体的降级实现方案的时候 可以结合业务世纪情况设置限流错误码
        if (StringUtils.isBlank(fallbackBeanId)) {
            // 自定义的 可以结合自己系统里的进行设置
            return ApiResult.error(ResultCode.REACH_RATE_LIMIT);
        }

        try {
            // SpringContext中通过BeanId获取对象 SpringUtils只是获取bean对象的工具类 有多种实现方式 可自行百度
            Object bean = SpringUtils.getBean(fallbackBeanId);
            Method method = bean.getClass().getMethod(signature.getName(), signature.getParameterTypes());
            // 执行对应的方法
            return method.invoke(bean, joinPoint.getArgs());
        } catch (Exception e) {
            // todo error log
        }
        return ApiResult.error(ResultCode.REACH_RATE_LIMIT);
    }

这样我们大概的一个架子就弄好了。 接下来我们看看实际该如何使用

3、具体应用

在方法入口引入SysRateLimiter标签

@Slf4j
@RestController
@RequestMapping("/api/user")
@RequiredArgsConstructor
public class UserQueryController extends AbstractController {

    /**
     * 查询用户信息
     *
     * @param request
     * @return
     */
    @GetMapping("/info/{id}")
    @SysRateLimit(permitsPerSecond = 500, limitKey = "UserQueryController.info", fallbackBeanId = "userQueryControllerFallBack",
            timeout = 100, timeUnit = TimeUnit.MILLISECONDS)
    public ApiResult<UserInfo> info(@PathVariable Long id, HttpServletRequest request) {
        // todo 业务逻辑查询 这里不展开
        return ApiResult.success();
    }
}

设置降级方法

@Service
public class UserQueryControllerFallBack {

    /**
     * 降级后执行的逻辑
     *
     * @param request
     * @return
     */
    public ApiResult<UserInfo> info(Long id, HttpServletRequest request) {
        // todo 编写限流降级后的逻辑 可以是降级码 也可以是默认对象
        return ApiResult.success(null);
    }
}

当请求进来的时候,会结合我们设置的阈值进行令牌桶的获取,获取失败后会执行限流,这里我们进行了限流后的降级处理。其实到这里我们完成限流组件的简单封装和使用,但是仍有一些点需要我们进行处理,例如如何动态设置令牌的数量,接下来我们就看一下如何实现令牌的动态设置。

4、动态设置令牌数量

通过DUCC配置令牌数量 我们需要定义一个DUCC配置,这里面内容很简单,配置我们设置limitKey的令牌数量

@Data
@Slf4j
@Component
public class RateLimitConfig {

    /**
     * 配置config key: limitKey value: 数量
     */
    private Map<String, Integer> limitConfig;

    /**
     * 监听ducc配置
     *
     * @param json
     */
    @LafValue(key = "rate.limit.conf")
    public void setConfig(String json) {
        if (StringUtils.isBlank(json)) {
            return;
        }
        Map<String, Integer> map = JsonModelUtils.getModel(json, Map.class, null);
        if (map != null) {
            Wrapper.wrapperBean(map, this, true);
        }
    }
}

通过DUCC配置获取指定limitKey的令牌数量,获取失败则采用方法设置默认数量,这样我们后面设置令牌数量就可以通过DUCC动态的配置了

 /**
     * 获取令牌桶数量
     *
     * @param sysRateLimit
     * @return
     */
    private double getPermitsPerSecond(SysRateLimit sysRateLimit) {
        // 方法默认令牌数量
        double defaultValue = sysRateLimit.permitsPerSecond();
        if (rateLimitConfig == null || rateLimitConfig.getLimitConfig() == null) {
            return defaultValue;
        }
        // 配置的令牌数量
        Integer value = rateLimitConfig.getLimitConfig().get(sysRateLimit.limitKey());
        if (value == null) {
            return defaultValue;
        }
        return value;
    }

5、后续其他配置

其实后续我们的其他属性都可以通过DUCC动态化的来配置,这里呢因为和令牌桶数量类似,就不再展开描述了。感兴趣的小伙伴可以自行设置,根据我们的使用,使用默认配置即可。

作者:京东零售 王磊

来源:京东云开发者社区

与从0到1构造自定义限流组件相似的内容:

从0到1构造自定义限流组件

在系统高可用设计中,接口限流是一个非常重要环节,一方面是出于对自身服务器资源的保护,另一方面也是对依赖资源的一种保护措施。比如对于 Web 应用,我限制单机只能处理每秒 1000 次的请求,超过的部分直接返回错误给客户端。虽然这种做法损害了用户的使用体验,但是它是在极端并发下的无奈之举,是短暂的行为,因此是可以接受的。

从0到1构建基于自身业务的前端工具库

在实际项目开发中无论 M 端、PC 端,或多或少都有一个 utils 文件目录去管理项目中用到的一些常用的工具方法,比如:时间处理、价格处理、解析url参数、加载脚本等,其中很多是重复、基础、或基于某种业务场景的工具,存在项目间冗余的痛点以及工具方法规范不统一的问题

Go-Zero从0到1实现微服务项目开发(二)

继续更新GoZero微服务实战系列文章:上一篇被GoZero作者万总点赞了,本文将继续使用 Go-zero 提供的工具和组件,从零开始逐步构建一个基本的微服务项目。手把手带你完成:项目初始化+需求分析+表结构设计+api+rpc+goctl+apifox调试+细节处理。带你实现一个完整微服务的开发。

乌卡时代的云成本管理:从0到1了解FinOps

在上一篇文章中,我们介绍了企业云业务的成本构成以及目前面临的成本困境,以及当前企业逐步转向 FinOps 的行业趋势,这篇文章我们将详细聊聊 FinOps,包括概念、重要性以及成熟度评价指标。 随着对云服务和供应商的使用越来越多,可能会出现复杂性和管理上的挑战,导致成本超支和其他问题。曾在2018年

[转帖]构建安全可靠的系统:从SRE到SRS

https://www.jianshu.com/p/ba61020aeb1e 在看到《Google系统架构解密:构建安全可靠的系统》这本书之前,个人就有安全和可靠性不分家的观念。看到有同样想法的书籍,甚是欢喜。读完上一本书,终于可以读这一本了,接下来很长一段时间,看下Google是如何构建安全可靠系

AlertManager解析:构建高效告警系统

本文深入探讨了AlertManager的技术细节和实际应用,从基本概念、核心组件、工作流程,到与Prometheus的集成和实战案例,旨在为专业人士提供一个全面的AlertManager技术和应用指南。 关注作者,分享互联网架构、云服务技术的全维度知识。作者拥有10+年互联网服务架构、AI产品研发经

什么是IPD项目管理模式?聊聊IPD下的产品研发流程

IPD(集成产品开发)涵盖了产品从创意提出到研发、生产、运营等,包含了产品开发到营销运营的整个过程。围绕产品(或项目)生命周期的过程的管理模式,是一套生产流程,更是时下国际先进的管理体系。IPD(集成产品开发)以产品成功、用户满意为最终目标,通过构建一套科学、持续、稳定、可重复、高质量的管理模式。

从0到1学Python丨图像平滑方法的两种非线性滤波:中值滤波、双边滤波

摘要:常用于消除噪声的图像平滑方法包括三种线性滤波(均值滤波、方框滤波、高斯滤波)和两种非线性滤波(中值滤波、双边滤波),本文将详细讲解两种非线性滤波方法。 本文分享自华为云社区《[Python从零到壹] 五十六.图像增强及运算篇之图像平滑(中值滤波、双边滤波)》,作者:eastmount。 常用于

从 0 到 1 搭建自己的脚手架(java 后端)

脚手架是一种基础设施工具,用于快速生成项目的框架代码和文件结构。它是一种标准化的开发工具,使开发人员能够在项目的早期阶段快速搭建出一个具备基本功能和结构的系统。

Python从0到1丨细说图像增强及运算

摘要:本文主要讲解常见的图像锐化和边缘检测方法,即Roberts算子和Prewitt算子。 本文分享自华为云社区《[Python从零到壹] 五十七.图像增强及运算篇之图像锐化Roberts、Prewitt算子实现边缘检测》,作者: eastmount。 一.图像锐化 由于收集图像数据的器件或传输图像