Redis的自增也能实现滑动窗口限流?
限流是大家开发之路上一定会遇到的需求。比如:限制一定时间内,接口请求请求频率;一定时间内用户发言、评论次数等等,类似于滑动窗口算法。这里分享一份拿来即用的代码,一起看看如何利用常见的 Redis 实现一个实用并且原理简单的限流器。
限流核心原理以及代码
这个限流器的原理是使用 Redis 的incr
命令来累计次数,key 的过期时间作为时间滑动窗口来实现。比如限制每5
秒最多请求10
次,那么就将 key 的过期时间设置为5
秒,每次执行前对这个 key 自增,5
秒内的次数将累计到这一个 key 上,如果自增的结果没有超过10
次,代表没有被限流。5
秒过后 key 将被 Redis 清除,后续次数将重新累计。
这里大家需要了解下incr
使用的一些细节。incr
每次执行都是将 key 的值自增1
,并返回自增后的结果,比如对key=1执行incr
结果为2
;如果 key 不存在,将设置这个 key 值为1
,返回结果自然也是1
,并且这个 key 是没有过期时间的。
Redis 的incr
不能在自增的同时设置过期时间,这就意味着自增和设置过期时间要分两步做,在第一次incr
完成之后,紧接着使用expire
指令来给这个 key 设置过期时间。非原子方式会带来并发问题,如果incr
成功,而expire
失败将导致生成了一个永不过期的 key,次数一直累计到最大值,永远进入限流状态。这个问题我们可以用个兜底逻辑来解决,在incr
前获取这个 key 的过期时间,如果没有那就删掉。
看到这,有了解过 Redis lua 脚本的同学可能会提出,既然这么麻烦,为何不用 lua 脚本自己实现一个自增且同时能够同时设置过期时间的功能?这个思路很棒,代码量不大且 Redis 也是完全可以支持的。但是在大点的公司,运维可能会禁止开发使用 lua 这种扩展方式,Redis 只有一个主线程执行执行命令,如果脚本中的逻辑执行时间过长将导致后续指令排队等待,它们响应时间自然也会变长,这种不可控的风险运维肯定不愿意承担。当然如果公司允许,并且有其他手段可以控制这个风险,lua 实现还是非常可行的。
为何不直接使用JDK实现而要借助中间件?因为实现出来只能在当前进程有有效,集群情况下不能累计到一起。
下面是具体代码,可以直接使用,代码关键处有详细的注释:
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.lang.NonNull;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
/**
* 用Redis实现的限流器,用于限制方法或者接口请求频率。比如:限制接口每秒请求次数;某个用户请求接口的次数,属于滑动窗口算法。
* 核心方法是 {@link #acquire(RedisTemplate, String, long, long)}
*/
public abstract class RedisIncrLimiter {
/**
* 限制每秒次数,参考 {@link #acquire(RedisTemplate, String, long, long)}
*/
public static boolean acquireLimitPerSecond(@NonNull RedisTemplate<String, String> redisTemplate,
@NonNull String limiterKey, long maxTimes) {
return acquire(redisTemplate, limiterKey, 1, maxTimes);
}
/**
* 限制每分钟次数,参考 {@link #acquire(RedisTemplate, String, long, long)}
*/
public static boolean acquireLimitPerMinute(@NonNull RedisTemplate<String, String> redisTemplate,
@NonNull String limiterKey, long maxTimes) {
return acquire(redisTemplate, limiterKey, 60, maxTimes);
}
/**
* 限制每小时次数,参考 {@link #acquire(RedisTemplate, String, long, long)}
*/
public static boolean acquireLimitPerHour(@NonNull RedisTemplate<String, String> redisTemplate,
@NonNull String limiterKey, long maxTimes) {
return acquire(redisTemplate, limiterKey, 3600, maxTimes);
}
/**
* 限制每天次数,参考 {@link #acquire(RedisTemplate, String, long, long)}
*/
public static boolean acquireLimitPerDay(@NonNull RedisTemplate<String, String> redisTemplate,
@NonNull String limiterKey, long maxTimes) {
return acquire(redisTemplate, limiterKey, 86400, maxTimes);
}
/**
* 执行限流逻辑前,调用这个方法获取一个令牌,如果返回 true 代表没被限流,可以执行。比如:
* <pre>{@code
* // 限制每秒最多发10次消息
* if (RedisIncrLimiter.acquire(redisTemplate, "sendMessage", 1, 10)) {
* // 发消息
* } else {
* // 被限流后的操作
* }
* }</pre>
* 如果限流粒度是用户级,可以将用户的ID或者唯一身份标识加到限流Key中。<br>
* 这个也是限流核心方法,利用 Redis incr 命令累计次数,KEY过期时间作为时间窗口实现。<br>
* 相同的限流KEY、时间窗口和最大次数才会累计到一起,三个参数任一不一致会分开累计,
* 参考{@link #buildFinalLimiterKey(String, long, long)}
*
* @param redisTemplate redisTemplate
* @param limiterKey 限流Key(代表限流逻辑的字符串)
* @param timeWindowSecond 时间窗口
* @param maxTimes 时间窗口内最大次数
* @return true-没有被限流
*/
public static boolean acquire(@NonNull RedisTemplate<String, String> redisTemplate,
@NonNull String limiterKey, long timeWindowSecond, long maxTimes) {
limiterKey = buildFinalLimiterKey(limiterKey, timeWindowSecond, maxTimes);
/*
如果异常情况下产生了没有过期时间的KEY,将导致次数不断累积到最大值(被限流)而无法解除。
这个兜底操作就是为了避免这个问题,清除没有过期时间的KEY
*/
Long ttl = redisTemplate.getExpire(limiterKey);
if (ttl == null || ttl == -1L) {
redisTemplate.delete(limiterKey);
return true;
}
Long incr = redisTemplate.opsForValue().increment(limiterKey);
Objects.requireNonNull(incr);
// 在第一次请求的时候设置过期时间(时间窗口)
if (incr == 1L) {
redisTemplate.expire(limiterKey, timeWindowSecond, TimeUnit.SECONDS);
}
return incr <= maxTimes;
}
/**
* @param limiterKey 限流Key
* @param timeWindowSecond 时间窗口
* @param maxTimes 时间窗口内最大次数
* @return 构建最终的限流 Redis Key,格式为:限流Key:时间窗口:最多次数
*/
private static String buildFinalLimiterKey(String limiterKey, long timeWindowSecond, long maxTimes) {
return limiterKey + ":" + timeWindowSecond + ":" + maxTimes;
}
}
基于Spring切面实现的注解版本
注解版使用起来比较方便,只需要在限流的方法上指定时间三个关键的参数就行,底层逻辑还是上面的代码。比如:
// 每5秒最多10次
@RedisIncrLimit(limiterKey = "test", timeWindowSecond = 5L, maxTimes = 10L)
public String test() {
return "ok";
}
RedisIncrLimit
只用来标记限流方法,接收限流参数。
import java.lang.annotation.*;
/**
* {@link RedisIncrLimiter} 注解版
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface RedisIncrLimit {
/**
* @return 限流KEY
*/
String limiterKey();
/**
* @return 时间窗口
*/
long timeWindowSecond();
/**
* @return 时间窗口内最大次数
*/
long maxTimes();
}
下面切面逻辑doBefore()
会在加了RedisIncrLimit
注解的方法前执行,先判断是否被限流。
import javax.annotation.Resource;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class RedisLimiterAspect {
@Resource
private RedisTemplate<String, String> redisTemplate;
@Pointcut("@annotation(redisLimit)")
public void pointcut(RedisIncrLimit redisLimit) {
}
@Before("pointcut(redisLimit)")
public void doBefore(RedisIncrLimit redisLimit) {
if (!RedisIncrLimiter.acquire(
redisTemplate, redisLimit.limiterKey(), redisLimit.timeWindowSecond(), redisLimit.maxTimes())) {
throw new IllegalStateException("rate limit");
}
}
}
以上是Redis限流器的全部内容,微信号搜索【wybqbx】或者扫描二维码关注公众号,里面有更多的分享,欢迎大家交流提问
Redis的自增也能实现滑动窗口限流?的更多相关文章
- ASP.NET Core中使用滑动窗口限流
滑动窗口算法用于应对请求在时间周期中分布不均匀的情况,能够更精确的应对流量变化,比较著名的应用场景就是TCP协议的流量控制,不过今天要说的是服务限流场景中的应用. 算法原理 这里假设业务需要每秒钟限流 ...
- 基于AOP和Redis实现对接口调用情况的监控及IP限流
目录 需求描述 概要设计 代码实现 参考资料 需求描述 项目中有许多接口,现在我们需要实现一个功能对接口调用情况进行统计,主要功能如下: 需求一:实现对每个接口,每天的调用次数做记录: 需求二:如果某 ...
- 基于redis+lua实现高并发场景下的秒杀限流解决方案
转自:https://blog.csdn.net/zzaric/article/details/80641786 应用场景如下: 公司内有多个业务系统,由于业务系统内有向用户发送消息的服务,所以通过统 ...
- Sentinel源码解析三(滑动窗口流量统计)
前言 Sentinel的核心功能之一是流量统计,例如我们常用的指标QPS,当前线程数等.上一篇文章中我们已经大致提到了提供数据统计功能的Slot(StatisticSlot),StatisticSlo ...
- tcp协议头窗口,滑动窗口,流控制,拥塞控制关系
参考文章 TCP 的那些事儿(下) http://coolshell.cn/articles/11609.html tcp/ip详解--拥塞控制 & 慢启动 快恢复 拥塞避免 http://b ...
- TCP 滑动窗口的简介
TCP 滑动窗口的简介 POSTED BY ADMIN ON AUG 1, 2012 IN FLOWS34ARTICLES | 0 COMMENTS TCP的滑动窗口主要有两个作用,一是提供TCP的可 ...
- TCP协议的滑动窗口具体是怎样控制流量的
首先明确: 1)TCP滑动窗口分为接受窗口,发送窗口滑动窗口协议是传输层进行流控的一种措施,接收方通过通告发送方自己的窗口大小,从而控制发送方的发送速度,从而达到防止发送方发送速度过快而导致自己被淹没 ...
- TCP滑动窗口(发送窗口和接受窗口)
TCP窗口机制 TCP header中有一个Window Size字段,它其实是指接收端的窗口,即接收窗口.用来告知发送端自己所能接收的数据量,从而达到一部分流控的目的. 其实TCP在整个发送过程中, ...
- Redis限流
在电商开发过程中,我们很多地方需要做限流,有的是从Nginx上面做限流,有的是从代码层面限流等,这里我们就是从代码层面用Redis计数器做限流,这里我们用C#语言来编写,且用特性(过滤器,拦截器)的形 ...
- SpringBoot使用自定义注解+AOP+Redis实现接口限流
为什么要限流 系统在设计的时候,我们会有一个系统的预估容量,长时间超过系统能承受的TPS/QPS阈值,系统有可能会被压垮,最终导致整个服务不可用.为了避免这种情况,我们就需要对接口请求进行限流. 所以 ...
随机推荐
- Alibaba Cloud Linux 3.2104 64位安装mydumper-0.9.1
1 下载 wget https://launchpad.net/mydumper/0.9/0.9.1/+download/mydumper-0.9.1.tar.gz tar -zxvf mydumpe ...
- javascript数据类型,定义方法,(工厂模式及闭包的应用)
js数据类型分为两大类:一 值类型 二 引用类型 一 值类型 string number boolean null ...
- 浅谈oracle连接远程数据库
若是本地安装了oracle服务器 1 比较直接的方式是直接修改配置文件 去...\product\11.2.0\dbhome_1\NETWORK\ADMIN下面找三个.ora的配置文件:sqlnet. ...
- OSIDP-线程-04
进程和线程 进程具有两个相互独立的特点: 1.资源所有权:进程包括存放进程映像的虚拟地址空间,具有对资源的控制权. 2.调度/执行:进程具有运行状态和优先级,是可被 OS 调度和分派的实体. 通常将分 ...
- PRVF-4007 : User equivalence check failed for user "grid"
PRVF-4007 : User equivalence check failed for user "grid" 问题:Oracle安装Grid Infrastructure之前 ...
- js下载流文件
npm install js-file-download --savenpm install axios --save import axios from "axios"; imp ...
- raft算法,cap理论中的c是数据一致性,是通过raft算法,
raft算法中的两个重要概念,自旋时间,心跳时间, 领导,候选人,随从,通过日志同步.只要大多数随从同步成功,就算成功.
- java的内存模型,jmm理解和(GC)垃圾回收时机。
jmm模型中的gc处理是在堆中回收. 1.新对象出来以后,先尝试在eden中放下,放不下的时候,进行一次ygc,只会在eden中回收,
- vs2013安装完VASSISTX助手之后字体变成斜体如何解决?
VC助手为最新版本. 1. 打开vc助手选项 2. 取消勾选"show stable symbols in italics"
- 使用arcgis按掩膜批量处理MODIS.tif数据提取研究区.shp边界的python代码
在输入代码之前,需要先用arcgis把需要处理的数据导入,再把研究区shp导入,然后定义投影,先掩膜处理一个数据,再进行下一步输入代码批量处理数据的步骤.如果 不先定义投影,可能会出现输入代码不执行的 ...