常见的限流方案

从实现方式上来讲,限流可分为简单计数器限流、滑动窗口限流,基于漏桶和令牌桶算法的限流。

从是否支持多机拓展上来讲,又分为单机限流和分布式限流。单机限流大多通过线程锁的方式实现,而分布式限流多借助于Redis等中间件。

简单计数器限流

通过维护单位时间内的请求次数来实现限流,当请求次数超过最大限制时拒绝访问。这种实现方式的好处是实现起来较为简单,缺点是可能会产生“毛刺”。如下图。

滑动窗口限流

滑动窗口也是维护单位时间内的请求次数,其与简单计数器的区别是,滑动窗口的粒度更细,将一个大的时间窗口划分为若干个小的时间窗口,通过滑动时间删除小的时间窗口,以此来避免简单计数器的“毛刺”问题。如下图。

基于漏桶算法限流

漏桶算法将流量放入一个固定容量的“漏斗”中,以恒定速度将流量进行输出。当“漏斗”装满时,拒绝掉涌入的流量。

基于令牌桶算法限流

令牌桶算法每隔一段时间就将一定量的令牌放入桶中,获取到令牌的请求直接访问后段的服务,没有获取到令牌的请求会被拒绝。同时令牌桶有一定的容量,当桶中的令牌数达到最大值后,不再放入令牌。

几种方案各有优劣,需要结合实际场景进行选型。

方案 优势 劣势
计数器 实现最为简单方便 "毛刺"现象
滑动窗口 应对突发流量能力强,可配置性强 取决于窗口粒度,非严格均匀,流量整形效果弱
漏桶 流量整形效果最好,输出流量最平滑(均匀输出) 应对突发流量效果差
令牌桶 相较漏桶,有一定的应对突发流量的能力 各方面都比较平庸,实现起来最为复杂
这里有一个流量整形的概念。所谓流量整形,是指流量经过我们的限流器后,其形状发生了变化,将短时间的大流量整形为长时间的平缓流量。而显然,通过计数器及滑动窗口的方式实现的限流,通过暴力拒绝掉部分流量,仅仅是对流量进行了“裁剪”,并没有对流量进行时间维度上的重新分配。而漏桶算法与令牌桶算法,通过一定的阻塞机制,真正改变了流量的时间分布,实现了一定的削峰填谷的效果。

限流器整体结构

整体设计思路上是通过注解实现对controller无侵入的限流,通过拦截请求,获取请求中相应的参数进行定制化的限流逻辑处理,并调用redis脚本进行是否限流的判断。

因此整体分为三部分代码

  • 限流注解,主要是一些限流参数的指定
  • redis限流脚本,限流方法的具体实现
  • 切面层,拦截请求,定制化限流逻辑,调用限流脚本实现限流。

限流注解如下:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimiter {/*** 限流key* @return*/String key() default "rate:limiter";/*** 窗口允许最大请求数* @return*/long maxCount() default 10;/*** 窗口宽度,单位为ms* @return*/long winWidth() default 1000;/*** 限流提示语* @return*/String message() default "false";
}

这里的限流key只是一个基本key,对于特定的业务逻辑,可以有一些定制化的限流,如对于我的使用场景下,需要对不同的租户进行分开的限流,那么就可以在限流逻辑中对key进行一个定制化,以实现拓展的效果。下面是切面层。

@Component
@Aspect
@Slf4j
public class RateLimitAspect {@ResourceStringRedisTemplate stringRedisTemplate;private DefaultRedisScript<Long> getRedisScript;@PostConstructpublic void init() {getRedisScript = new DefaultRedisScript<>();getRedisScript.setResultType(Long.class);getRedisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("rateLimiterSlidingWindow.lua")));log.info("RateLimiter[分布式限流处理器]脚本加载完成");}@Pointcut("@annotation(com.tencent.cloud.iov.ivm.annotations.RateLimiter)")public void rateLimiter() {}@Around("@annotation(rateLimiter)")public Object around(ProceedingJoinPoint proceedingJoinPoint, RateLimiter rateLimiter) throws Throwable {if (log.isDebugEnabled()) {log.debug("RateLimiter[分布式限流处理器]开始执行限流操作");}Signature signature = proceedingJoinPoint.getSignature();if (!(signature instanceof MethodSignature)) {throw new IllegalArgumentException("the Annotation @RateLimiter must used on method!");}/*** 获取注解参数*//** 限流模块key*  按业务需求定制化处理*  这里用tenantId作为key的一部分,实现分租户限流的目的*/String limitKey = rateLimiter.key();RequestVo arg = (RequestVo)proceedingJoinPoint.getArgs()[0];limitKey += "-" + arg.getTenantId();Preconditions.checkNotNull(limitKey);/**时间窗口内可接受的最大请求次数*/Long maxCount = rateLimiter.maxCount();/**时间窗口宽度*/Long winWidth = rateLimiter.winWidth();if (log.isDebugEnabled()) {log.debug("RateLimiterHandler[分布式限流处理器]参数值为-maxCount={},winWidth={}", maxCount, winWidth);}// 限流提示语String message = rateLimiter.message();if (StringUtils.isBlank(message)) {message = "false";}/*** 执行Lua脚本*/List<String> keyList = new ArrayList();// 设置key值为注解中的值keyList.add(limitKey);/*** 调用脚本并执行*/log.info("keyList={}, maxCount={}, winWidth={}", keyList, maxCount, winWidth);Long result = stringRedisTemplate.execute(getRedisScript, keyList, maxCount.toString(), winWidth.toString());if (result == 0) {String msg = "由于超过窗口宽度=" + winWidth + "-允许" + limitKey + "的请求次数=" + maxCount + "[触发限流]";log.debug(msg);throw new BusinessException(BusinessCode.EXCEEDING_LIMIT_ERROR);}if (log.isDebugEnabled()) {log.debug("RateLimiterHandler[分布式限流处理器]限流执行结果-result={},请求[正常]响应", result);}return proceedingJoinPoint.proceed();}
}

限流脚本

这里将redis的限流方法解耦开,通过使用不同的脚本,以实现不同方案的限流。

简单计数器

首先是简单计数器的限流。
redis限流脚本如下:

--获取KEY
local key1 = KEYS[1]local val = redis.call('incr', key1)
local ttl = redis.call('ttl', key1)--获取ARGV内的参数并打印
local expire = ARGV[1]
local times = ARGV[2]redis.log(redis.LOG_DEBUG,tostring(times))
redis.log(redis.LOG_DEBUG,tostring(expire))redis.log(redis.LOG_NOTICE, "incr "..key1.." "..val);
if val == 1 thenredis.call('expire', key1, tonumber(expire))
elseif ttl == -1 thenredis.call('expire', key1, tonumber(expire))end
endif val > tonumber(times) thenreturn 0
endreturn 1

比较简单,主要是利用了Redis脚本的原子性,这里不再过多介绍了。

滑动窗口的实现方式

限流脚本如下:

redis.replicate_commands();--获取KEY
local key = KEYS[1]--获取ARGV内的参数并打印
local max_quantity = ARGV[1]
local window_width = ARGV[2]--获取当前时间及时间边界
local time = redis.call('TIME') --返回值为当前所过去的秒数,当前秒所过去的微秒数
local timestamp = time[1] * 1000 + math.floor(time[2] / 1000)local left_border = timestamp - window_width--移除窗口外的值
redis.call('zremrangebyscore', key, 0, left_border)--统计窗口内元素个数
local count = redis.call('zcard', key)if count < tonumber(max_quantity) thenredis.call('zadd', key, timestamp, timestamp)return 1
elsereturn 0
end

每次获取当前的时间戳,并移除时间窗口外的元素,随后判断当前是否出发限流,由于这里时间的粒度是毫秒,因此限流的效果还是比较平滑的。

这里要注意通过redis.replicate_commands()开启命令复制模式。这是因为redis在集群模式下,对于获取时间这种命令,由于到达各台机器的时间不一致,因此会出现数据不一致的问题。而采用命令复制模式,会直接复制时间值而非获取时间的命令。

漏桶实现方式

主要是利用了redis 4.0的cell命令。

--获取KEY
local key1 = KEYS[1]--获取ARGV内的参数并打印
local max_quantity = ARGV[1]
local window_width = ARGV[2]--这里漏桶的容量直接写死了,后续应该作为参数传入
local res = redis.call('CL.THROTTLE', key, 1000, max_quantity, window_width)if res[1] == 0 thenreturn 1
elsereturn 0
end

这里要注意redis-cell模块需要额外安装。

基于Redis的分布式限流器Java实现相关推荐

  1. 手写基于redis实现分布式限流器-pdlr

    一.限流器介绍 简单来讲,限流器就是控制流量访问速率的一种机制,在短时间内大量流量访问时,不至于使服务器崩溃.比如,一分钟之内,如果限制1000个请求可以访问,当第1001个请求到达时,请求被拒绝(这 ...

  2. Java基于redis实现分布式锁(SpringBoot)

    前言 分布式锁,其实原理是就是多台机器,去争抢一个资源,谁争抢成功,那么谁就持有了这把锁,然后去执行后续的业务逻辑,执行完毕后,把锁释放掉. 可以通过多种途径实现分布式锁,例如利用数据库(mysql等 ...

  3. java redis id生成器_基于redis的分布式ID生成器

    项目地址 基于redis的分布式ID生成器. 准备 首先,要知道redis的EVAL,EVALSHA命令: 原理 利用redis的lua脚本执行功能,在每个节点上通过lua脚本生成唯一ID. 生成的I ...

  4. 基于 Redis 实现分布式锁思考

    以下文章来源方志朋的博客,回复"666"获面试宝典 来源:blog.csdn.net/xuan_lu/article/details/111600302 分布式锁 基于redis实 ...

  5. nx set 怎么实现的原子性_基于Redis的分布式锁实现

    前言 本篇文章主要介绍基于Redis的分布式锁实现到底是怎么一回事,其中参考了许多大佬写的文章,算是对分布式锁做一个总结 分布式锁概览 在多线程的环境下,为了保证一个代码块在同一时间只能由一个线程访问 ...

  6. redis系列:基于redis的分布式锁

    一.介绍 这篇博文讲介绍如何一步步构建一个基于Redis的分布式锁.会从最原始的版本开始,然后根据问题进行调整,最后完成一个较为合理的分布式锁. 本篇文章会将分布式锁的实现分为两部分,一个是单机环境, ...

  7. 基于redis的分布式任务管理

    业务背景 系统中有各种定时任务,需要满足以下要求: 定时任务需要能够动态增删改查 需要能够设置任务的有效时间范围(只在此范围内执行) 任务执行周期需要能够灵活配置 需要能够轻松接入各种任务实现 需要能 ...

  8. redis延迟队列 实现_灵感来袭,基于Redis的分布式延迟队列(续)

    背景 上一篇(灵感来袭,基于Redis的分布式延迟队列)讲述了基于Java DelayQueue和Redis实现了分布式延迟队列,这种方案实现比较简单,应用于延迟小,消息量不大的场景是没问题的,毕竟J ...

  9. js 拉勾网效果_Node.js 中实践基于 Redis 的分布式锁实现

    在一些分布式环境下.多线程并发编程中,如果对同一资源进行读写操作,避免不了的一个就是资源竞争问题,通过引入分布式锁这一概念,可以解决数据一致性问题. 作者简介:五月君,Nodejs Developer ...

最新文章

  1. linux shell 实现循环输出
  2. UVA - 11882Biggest Number dfs+期望剪枝
  3. 浅谈 js 数字格式类型
  4. php pdo-insert,php mysql pdo insert multiple rows 批量插入
  5. Nagios安装配置教程(二)环境搭建
  6. mysql使用mybatis删除不生效_解决myBatis中删除条件的拼接问题
  7. 从云效1.0到2.0的升级,看技术如何驱动企业提效
  8. talib如何安装方法
  9. HTML 自动跳转代码
  10. 优化vmware mac神器beamoff
  11. Xshell 6安装和使用教程
  12. 深度剖析Java集合之BitSet
  13. python爬虫用AI技术-破解企业工商数据抓取+网络爬虫+网站数据采集+数据抓取遇到的三大问题
  14. 工厂模式,从第三方登录说起
  15. jvm精通之柳暗花明
  16. 解决WH1000XM3连接电脑无法使用麦克风问题
  17. python setup.py install与python setup.py develop的区别
  18. data单复数一样吗_data的用法
  19. DBoW、VLAD、NetVLAD、NeXtVLAD资料整理
  20. 记第一次Ubuntu系统的安装、搜狗输入法的安装与VIM插件的安装(入门篇)

热门文章

  1. Activiti7.0实战学习(九):查询历史表中的数据
  2. 验证码爬取并识别-云大urp教务系统大作战(1)
  3. Pytorch为什么总要设置随机种子
  4. Ivor Horton's Beginning Visual C++ 2008
  5. matlab 蜗杆轮廓,solidworks环面蜗杆画法
  6. vue预览excel
  7. 微盟电子商城网络交易系统——Day01【项目介绍、项目环境搭建、快速搭建后台管理系统】
  8. matlab生成s加减速曲线,一种基于移动平均算法的S曲线加减速控制方法与流程
  9. springboot整合H+,基于summernote的富文本编辑器图片上传
  10. 我支持平板能代替笔记本电脑