从0到1构造自定义限流组件 | 京东云技术团队

这篇具有很好参考价值的文章主要介绍了从0到1构造自定义限流组件 | 京东云技术团队。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

一 背景

在系统高可用设计中,接口限流是一个非常重要环节,一方面是出于对自身服务器资源的保护,另一方面也是对依赖资源的一种保护措施。比如对于 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动态化的来配置,这里呢因为和令牌桶数量类似,就不再展开描述了。感兴趣的小伙伴可以自行设置,根据我们的使用,使用默认配置即可。

作者:京东零售 王磊

来源:京东云开发者社区文章来源地址https://www.toymoban.com/news/detail-494839.html

到了这里,关于从0到1构造自定义限流组件 | 京东云技术团队的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 基于AIGC的京东购物助手的技术方案设想 | 京东云技术团队

    随着AIGC的爆火,ChatGPT,GPT-4的发布,我作为一个算法工作者,深感AI发展的迅猛。最近,OpenAI的插件和联网功能陆续向用户公开,我也在第一时间试用了这些最新的功能。在OpenAI的插件市场上,我被一个可以帮助分析食谱,并生成购物清单的功能所吸引。我开始思考,如果我

    2024年02月12日
    浏览(56)
  • 技术赋能-混流编排功能,助力京东618直播重保 | 京东云技术团队

    每每到618、双11这样的大型活动的时候,每天都有几个重要的大v或者品牌直播需要保障。 以往的重点场次监播方式是这么造的: 对每路直播的源流、各档转码流分别起一个ffplay播放窗口,再手动调整尺寸在显示器桌面进行布局,排到一屏里来监播。 这样做的缺点: 操作复杂

    2024年02月08日
    浏览(47)
  • 商品推荐系统浅析 | 京东云技术团队

    本文主要做推荐系统浅析,主要介绍推荐系统的定义,推荐系统的基础框架,简单介绍设计推荐的相关方法以及架构。适用于部分对推荐系统感兴趣的同学以及有相关基础的同学,本人水平有限,欢迎大家指正。 2.1 推荐系统的定义 推荐系统本质上还是解决信息过载的问题,

    2024年02月13日
    浏览(40)
  • 初探webAssembly | 京东物流技术团队

    一种运行在现代网络浏览器中的新型代码,并且提供新的性能特性和效果 W3C WebAssembly Community Group开发的一项网络标准,对于浏览器而言,WebAssembly 提供了一条途径,让各种语言编写的代码以接近原生的速度在 Web 中运行。在这种情况下,以前无法以此方式运行的客户端软件等

    2024年02月15日
    浏览(39)
  • 事务,不只ACID | 京东物流技术团队

    1. 什么是事务? 应用在运行时可能会发生数据库、硬件的故障,应用与数据库的网络连接断开或多个客户端端并发修改数据导致预期之外的数据覆盖问题,为了提高应用的可靠性和数据的一致性, 事务 应运而生。 从概念上讲,事务是 应用程序将多个读写操作组合成一个逻

    2024年02月13日
    浏览(48)
  • Spring源码核心剖析 | 京东云技术团队

    SpringAOP作为Spring最核心的能力之一,其重要性不言而喻。然后需要知道的是AOP并不只是Spring特有的功能,而是一种思想,一种通用的功能。而SpringAOP只是在AOP的基础上将能力集成到SpringIOC中,使其作为bean的一种,从而我们能够很方便的进行使用。 1.1 使用场景 当我们在日常业

    2024年02月10日
    浏览(41)
  • 定时任务原理方案综述 | 京东云技术团队

    本文主要介绍目前存在的定时任务处理解决方案。业务系统中存在众多的任务需要定时或定期执行,并且针对不同的系统架构也需要提供不同的解决方案。京东内部也提供了众多定时任务中间件来支持,总结当前各种定时任务原理,从定时任务基础原理、单机定时任务(单线

    2024年02月09日
    浏览(66)
  • 618技术揭秘:探究竞速榜页面核心前端技术 | 京东云技术团队

    H5页面作为移动端Web应用的重要形式之一,已经成为了现代Web开发的热门话题。在H5页面的开发过程中,前端技术的应用至关重要。本文将探究京东竞速榜H5页面的核心前端技术,包括动画、样式配置化、皮肤切换、海报技术、调试技巧等方面,希望能够为广大前端开发者提供

    2024年02月12日
    浏览(42)
  • 插件化工程R文件瘦身技术方案 | 京东云技术团队

    随着业务的发展及版本迭代,客户端工程中不断增加新的业务逻辑、引入新的资源,随之而来的问题就是安装包体积变大,前期各个业务模块通过无用资源删减、大图压缩或转上云、AB实验业务逻辑下线或其他手段在降低包体积上取得了一定的成果。 在瘦身的过程中我们关注

    2024年02月08日
    浏览(46)
  • 楠姐技术漫话:图计算的那些事 | 京东云技术团队

    不知道大家在平时的工作中 有没有听说过“图计算”这个名词 但大家一定在各工作汇报,技术分享中听说过“智能化”,“人工智能”这样的字眼 而我们今天要唠的这个图计算 就是人工智能领域内近几年炙手可热的前沿宠儿 也是我们风控反欺诈中常用的“大杀器” 在了解

    2024年02月05日
    浏览(38)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包