限流是大家开发之路上一定会遇到的需求。比如:限制一定时间内,接口请求请求频率;一定时间内用户发言、评论次数等等,类似于滑动窗口算法。这里分享一份拿来即用的代码,一起看看如何利用常见的 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的自增也能实现滑动窗口限流?的更多相关文章

  1. ASP.NET Core中使用滑动窗口限流

    滑动窗口算法用于应对请求在时间周期中分布不均匀的情况,能够更精确的应对流量变化,比较著名的应用场景就是TCP协议的流量控制,不过今天要说的是服务限流场景中的应用. 算法原理 这里假设业务需要每秒钟限流 ...

  2. 基于AOP和Redis实现对接口调用情况的监控及IP限流

    目录 需求描述 概要设计 代码实现 参考资料 需求描述 项目中有许多接口,现在我们需要实现一个功能对接口调用情况进行统计,主要功能如下: 需求一:实现对每个接口,每天的调用次数做记录: 需求二:如果某 ...

  3. 基于redis+lua实现高并发场景下的秒杀限流解决方案

    转自:https://blog.csdn.net/zzaric/article/details/80641786 应用场景如下: 公司内有多个业务系统,由于业务系统内有向用户发送消息的服务,所以通过统 ...

  4. Sentinel源码解析三(滑动窗口流量统计)

    前言 Sentinel的核心功能之一是流量统计,例如我们常用的指标QPS,当前线程数等.上一篇文章中我们已经大致提到了提供数据统计功能的Slot(StatisticSlot),StatisticSlo ...

  5. tcp协议头窗口,滑动窗口,流控制,拥塞控制关系

    参考文章 TCP 的那些事儿(下) http://coolshell.cn/articles/11609.html tcp/ip详解--拥塞控制 & 慢启动 快恢复 拥塞避免 http://b ...

  6. TCP 滑动窗口的简介

    TCP 滑动窗口的简介 POSTED BY ADMIN ON AUG 1, 2012 IN FLOWS34ARTICLES | 0 COMMENTS TCP的滑动窗口主要有两个作用,一是提供TCP的可 ...

  7. TCP协议的滑动窗口具体是怎样控制流量的

    首先明确: 1)TCP滑动窗口分为接受窗口,发送窗口滑动窗口协议是传输层进行流控的一种措施,接收方通过通告发送方自己的窗口大小,从而控制发送方的发送速度,从而达到防止发送方发送速度过快而导致自己被淹没 ...

  8. TCP滑动窗口(发送窗口和接受窗口)

    TCP窗口机制 TCP header中有一个Window Size字段,它其实是指接收端的窗口,即接收窗口.用来告知发送端自己所能接收的数据量,从而达到一部分流控的目的. 其实TCP在整个发送过程中, ...

  9. Redis限流

    在电商开发过程中,我们很多地方需要做限流,有的是从Nginx上面做限流,有的是从代码层面限流等,这里我们就是从代码层面用Redis计数器做限流,这里我们用C#语言来编写,且用特性(过滤器,拦截器)的形 ...

  10. SpringBoot使用自定义注解+AOP+Redis实现接口限流

    为什么要限流 系统在设计的时候,我们会有一个系统的预估容量,长时间超过系统能承受的TPS/QPS阈值,系统有可能会被压垮,最终导致整个服务不可用.为了避免这种情况,我们就需要对接口请求进行限流. 所以 ...

随机推荐

  1. Sharp7与S7NetPlus 性能测试

    介绍 ​ Sharp7和都S7NetPlus是纯C#实现的基于以太网与S7系列的西门子PLC通讯的开源库.都支持.net core 跨平台可以部署在linxu, docker,windwos 中. 测 ...

  2. Java知识体系深度理解

    1 post请求和get请求异同点 ①无论是POST还是GET请求,都是基于超文本传输协议(HTTP)的,而HTTP协议是TCP/IP协议族的应用层协议. HTTP的底层是TCP/IP.所以GET和P ...

  3. nginx二进制安装脚本

    #!/bin/bash NGINX_FILE=nginx-1.22.0#NGINX_FILE=nginx-1.20.2#NGINX_FILE=nginx-1.18.0NGINX_URL=http:// ...

  4. SpingBoot面试大汇总

    1.什么是SpringBoot? 1)用来优化Spring应用的初始搭建以及开发过程,使用特定的方式来配置(properties和yml文件) 2)嵌入式的内置服务器tomcat无需部署war文件简化 ...

  5. C语言初级阶段8——预处理

    C语言初级阶段8--预处理 预定义符号 1.概念:预处理是编译之前做的一些事. 2.常用的预定义符号: 注意:: (1)-(4)的格式占位符都用%是,如:printf("%s",D ...

  6. JAVA 学习打卡 day1

    2022-04-21 21:03:26 1.java程序的注释 (1)单行注释,就是在注释内容前面加双斜线(//) (2)多行注释,就是在注释内容前面以单斜线加一个星形标记(/*)开头,并在注释内容末 ...

  7. SQLyog 13.1.1.0注册码证书秘钥

    注册信息: Name:(用户名随意) License Key: Professional: 8e053a86-cdd3-48ed-b5fe-94c51b3d343c Enterprise: a4668 ...

  8. UIPath踩坑记一 对 COM 组件的调用返回了错误 HRESULT E_FAIL。UiPath.UiNodeClass.InjectAndRunJS

    [ERROR] [UiPath.Studio] [1] 错误: System.Exception: 对 COM 组件的调用返回了错误 HRESULT E_FAIL. ---> System.Ex ...

  9. 使用Navicat查询后 , 在结果处更改数据

    参考资料: https://blog.csdn.net/weixin_43786801/article/details/125364995 问题: 在使用Navicat查询是,往往想直接对查询结果进行 ...

  10. kafka-集群搭建及启动脚本

    集群搭建: [root@localhost kafka_2.11-0.10.2.1]# cat config/server.properties | grep -v ^$ | grep -v ^# b ...