0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

Redis实现分布式多规则限流的方式介绍

jf_ro2CN3Fa 来源:稀土掘金 2024-02-26 10:07 次阅读

简介

市面上很多介绍redis如何实现限流的,但是大部分都有一个缺点,就是只能实现单一的限流,比如1分钟访问1次或者60分钟访问10次这种,但是如果想一个接口两种规则都需要满足呢,我们的项目又是分布式项目,应该如何解决,下面就介绍一下redis实现分布式多规则限流的方式。

思考

如何一分钟只能发送一次验证码,一小时只能发送10次验证码等等多种规则的限流

如何防止接口被恶意打击(短时间内大量请求)

如何限制接口规定时间内访问次数

解决方法

记录某IP访问次数

使用 String结构 记录固定时间段内某用户IP访问某接口的次数

RedisKey = prefix : className : methodName

RedisVlue = 访问次数

拦截请求:

初次访问时设置 「[RedisKey] [RedisValue=1] [规定的过期时间]」

获取 RedisValue 是否超过规定次数,超过则拦截,未超过则对 RedisKey 进行加1

分析: 规则是每分钟访问 1000 次

考虑并发问题

假设目前 RedisKey => RedisValue 为 999

目前大量请求进行到第一步( 获取Redis请求次数 ),那么所有线程都获取到了值为999,进行判断都未超过限定次数则不拦截,导致实际次数超过 1000 次

「解决办法:」

保证方法执行原子性(加锁、lua)

考虑在临界值进行访问

思考下图

26e79030-d449-11ee-a297-92fbcf53809c.jpg

代码实现: 比较简单,

Zset解决临界值问题

使用 Zset 进行存储,解决临界值访问问题

26f03032-d449-11ee-a297-92fbcf53809c.jpg

网上几乎都有实现,这里就不过多介绍

实现多规则限流

先确定最终需要的效果

能实现多种限流规则

能实现防重复提交

通过以上要求设计注解(先想象出最终实现效果)

@RateLimiter(
rules={
//60秒内只能访问10次
@RateRule(count=10,time=60,timeUnit=TimeUnit.SECONDS),
//120秒内只能访问20次
@RateRule(count=20,time=120,timeUnit=TimeUnit.SECONDS)

},
//防重复提交(5秒钟只能访问1次)
preventDuplicate=true
)

编写注解(RateLimiter,RateRule)

编写 RateLimiter 注解。

/**
*@Description:请求接口限制
*@Author:yiFei
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public@interfaceRateLimiter{

/**
*限流key
*/
Stringkey()defaultRedisKeyConstants.RATE_LIMIT_CACHE_PREFIX;

/**
*限流类型(默认Ip模式)
*/
LimitTypeEnumlimitType()defaultLimitTypeEnum.IP;

/**
*错误提示
*/
ResultCodemessage()defaultResultCode.REQUEST_MORE_ERROR;

/**
*限流规则(规则不可变,可多规则)
*/
RateRule[]rules()default{};

/**
*防重复提交值
*/
booleanpreventDuplicate()defaultfalse;

/**
*防重复提交默认值
*/
RateRulepreventDuplicateRule()default@RateRule(count=1,time=5);
}

编写RateRule注解

@Target(ElementType.ANNOTATION_TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public@interfaceRateRule{

/**
*限流次数
*/
longcount()default10;

/**
*限流时间
*/
longtime()default60;

/**
*限流时间单位
*/
TimeUnittimeUnit()defaultTimeUnit.SECONDS;

}

拦截注解 RateLimiter

确定redis存储方式

RedisKey = prefix : className : methodName

RedisScore = 时间戳

RedisValue = 任意分布式不重复的值即可

编写生成 RedisKey 的方法

/**
*通过rateLimiter和joinPoint拼接prefix:ip/userId:classSimpleName-methodName
*
*@paramrateLimiter提供prefix
*@paramjoinPoint提供classSimpleName:methodName
*@return
*/
publicStringgetCombineKey(RateLimiterrateLimiter,JoinPointjoinPoint){
StringBufferkey=newStringBuffer(rateLimiter.key());
//不同限流类型使用不同的前缀
switch(rateLimiter.limitType()){
//XXX可以新增通过参数指定参数进行限流
caseIP:
key.append(IpUtil.getIpAddr(((ServletRequestAttributes)Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest())).append(":");
break;
caseUSER_ID:
SysUserDetailsuser=SecurityUtil.getUser();
if(!ObjectUtils.isEmpty(user))key.append(user.getUserId()).append(":");
break;
caseGLOBAL:
break;
}
MethodSignaturesignature=(MethodSignature)joinPoint.getSignature();
Methodmethod=signature.getMethod();
ClasstargetClass=method.getDeclaringClass();
key.append(targetClass.getSimpleName()).append("-").append(method.getName());
returnkey.toString();
}

编写lua脚本

编写lua脚本 (两种将时间添加到Redis的方法)。

Zset的UUID value值

UUID(可用其他有相同的特性的值)为Zset中的value值

参数介绍

KEYS[1] = prefix : ? : className : methodName

KEYS[2] = 唯一ID

KEYS[3] = 当前时间

ARGV = [次数,单位时间,次数,单位时间, 次数, 单位时间 ...]

java传入分布式不重复的 value 值

--1.获取参数
localkey=KEYS[1]
localuuid=KEYS[2]
localcurrentTime=tonumber(KEYS[3])
--2.以数组最大值为ttl最大值
localexpireTime=-1;
--3.遍历数组查看是否超过限流规则
fori=1,#ARGV,2do
localrateRuleCount=tonumber(ARGV[i])
localrateRuleTime=tonumber(ARGV[i+1])
--3.1判断在单位时间内访问次数
localcount=redis.call('ZCOUNT',key,currentTime-rateRuleTime,currentTime)
--3.2判断是否超过规定次数
iftonumber(count)>=rateRuleCountthen
returntrue
end
--3.3判断元素最大值,设置为最终过期时间
ifrateRuleTime>expireTimethen
expireTime=rateRuleTime
end
end
--4.redis中添加当前时间
redis.call('ZADD',key,currentTime,uuid)
--5.更新缓存过期时间
redis.call('PEXPIRE',key,expireTime)
--6.删除最大时间限度之前的数据,防止数据过多
redis.call('ZREMRANGEBYSCORE',key,0,currentTime-expireTime)
returnfalse

根据时间戳作为Zset中的value值

参数介绍

KEYS[1] = prefix : ? : className : methodName

KEYS[2] = 当前时间

ARGV = [次数,单位时间,次数,单位时间, 次数, 单位时间 ...]

根据时间进行生成value值,考虑同一毫秒添加相同时间值问题

以下为第二种实现方式,在并发高的情况下效率低,value是通过时间戳进行添加,但是访问量大的话会使得一直在调用 redis.call('ZADD', key, currentTime, currentTime),但是在不冲突value的情况下,会比生成 UUID 好

--1.获取参数
localkey=KEYS[1]
localcurrentTime=KEYS[2]
--2.以数组最大值为ttl最大值
localexpireTime=-1;
--3.遍历数组查看是否越界
fori=1,#ARGV,2do
localrateRuleCount=tonumber(ARGV[i])
localrateRuleTime=tonumber(ARGV[i+1])
--3.1判断在单位时间内访问次数
localcount=redis.call('ZCOUNT',key,currentTime-rateRuleTime,currentTime)
--3.2判断是否超过规定次数
iftonumber(count)>=rateRuleCountthen
returntrue
end
--3.3判断元素最大值,设置为最终过期时间
ifrateRuleTime>expireTimethen
expireTime=rateRuleTime
end
end
--4.更新缓存过期时间
redis.call('PEXPIRE',key,expireTime)
--5.删除最大时间限度之前的数据,防止数据过多
redis.call('ZREMRANGEBYSCORE',key,0,currentTime-expireTime)
--6.redis中添加当前时间(解决多个线程在同一毫秒添加相同value导致Redis漏记的问题)
--6.1maxRetries最大重试次数retries重试次数
localmaxRetries=5
localretries=0
whiletruedo
localresult=redis.call('ZADD',key,currentTime,currentTime)
ifresult==1then
--6.2添加成功则跳出循环
break
else
--6.3未添加成功则value+1再次进行尝试
retries=retries+1
ifretries>=maxRetriesthen
--6.4超过最大尝试次数采用添加随机数策略
localrandom_value=math.random(1,1000)
currentTime=currentTime+random_value
else
currentTime=currentTime+1
end
end
end

returnfalse

编写 AOP 拦截

@Autowired
privateRedisTemplateredisTemplate;

@Autowired
privateRedisScriptlimitScript;

/**
*限流
*XXX对限流要求比较高,可以使用在Redis中对规则进行存储校验或者使用中间件
*
*@paramjoinPointjoinPoint
*@paramrateLimiter限流注解
*/
@Before(value="@annotation(rateLimiter)")
publicvoidboBefore(JoinPointjoinPoint,RateLimiterrateLimiter){
//1.生成key
Stringkey=getCombineKey(rateLimiter,joinPoint);
try{
//2.执行脚本返回是否限流
Booleanflag=redisTemplate.execute(limitScript,
ListUtil.of(key,String.valueOf(System.currentTimeMillis())),
(Object[])getRules(rateLimiter));
//3.判断是否限流
if(Boolean.TRUE.equals(flag)){
log.error("ip:'{}'拦截到一个请求RedisKey:'{}'",
IpUtil.getIpAddr(((ServletRequestAttributes)Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest()),
key);
thrownewServiceException(rateLimiter.message());
}
}catch(ServiceExceptione){
throwe;
}catch(Exceptione){
e.printStackTrace();
}
}

/**
*获取规则
*
*@paramrateLimiter获取其中规则信息
*@return
*/
privateLong[]getRules(RateLimiterrateLimiter){
intcapacity=rateLimiter.rules().length<< 1;
    // 1. 构建 args
    Long[] args = new Long[rateLimiter.preventDuplicate() ? capacity + 2 : capacity];
    // 3. 记录数组元素
    int index = 0;
    // 2. 判断是否需要添加防重复提交到redis进行校验
    if (rateLimiter.preventDuplicate()) {
        RateRule preventRateRule = rateLimiter.preventDuplicateRule();
        args[index++] = preventRateRule.count();
        args[index++] = preventRateRule.timeUnit().toMillis(preventRateRule.time());
    }
    RateRule[] rules = rateLimiter.rules();
    for (RateRule rule : rules) {
        args[index++] = rule.count();
        args[index++] = rule.timeUnit().toMillis(rule.time());
    }
    return args;
}



审核编辑:刘清

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • lua脚本
    +关注

    关注

    0

    文章

    21

    浏览量

    7577
  • Redis
    +关注

    关注

    0

    文章

    371

    浏览量

    10844

原文标题:Redis 多规则限流和防重复提交方案实现

文章出处:【微信号:芋道源码,微信公众号:芋道源码】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    redis分布式锁场景实现

    今天带大家深入剖析一下Redis分布式锁,彻底搞懂它。 场景 既然要搞懂Redis分布式锁,那肯定要有一个需要它的场景。 高并发售票问题就是一个经典案例。 搭建环境 准备
    的头像 发表于 09-25 17:09 694次阅读

    在 Java 中利用 redis 实现一个分布式锁服务

    在 Java 中利用 redis 实现一个分布式锁服务
    发表于 07-05 13:14

    分布式Redis的五种数据类型

    分布式_Redis》_概述汇总
    发表于 10-15 10:55

    Redis 分布式锁的正确实现方式

    分布式锁一般有三种实现方式:1. 数据库乐观锁;2. 基于Redis分布式锁;3. 基于ZooKeeper的
    的头像 发表于 05-31 14:19 3568次阅读

    Redis分布式锁真的安全吗?

    今天我们来聊一聊Redis分布式锁。
    的头像 发表于 11-02 14:07 977次阅读

    手撸了个Redis分布式

    实现分布式锁的方式有很多,其中 Redis 是最常见的一种。而相较于 Java + Redis 的方案,我个人更倾向于 Go+
    的头像 发表于 11-03 14:44 668次阅读

    Redis实现限流的三种方式分享

    当然,限流有许多种实现方式Redis具有很强大的功能,我用Redis实践了三种的实现
    的头像 发表于 02-22 09:52 1033次阅读

    如何使用注解实现redis分布式锁!

    使用 Redis 作为分布式锁,将锁的状态放到 Redis 统一维护,解决集群中单机 JVM 信息不互通的问题,规定操作顺序,保护用户的数据正确。
    发表于 04-25 12:42 640次阅读
    如何使用注解<b class='flag-5'>实现</b><b class='flag-5'>redis</b><b class='flag-5'>分布式</b>锁!

    分布式限流简介

    限流是生产中经常遇到的一个场景, 目前现有的一个工具大部分是提供单机限流的能力, 例如 google 的 guava 中提供的 RateLimiter. 但是生产环境大部分是分布式环境, 在多台机器的环境下, 需要的是能对多台机
    的头像 发表于 05-16 16:40 1043次阅读
    <b class='flag-5'>分布式</b><b class='flag-5'>限流</b>简介

    深入理解redis分布式

    深入理解redis分布式锁 哈喽,大家好,我是指北君。 本篇文件我们来介绍如何Redis实现分布式
    的头像 发表于 10-08 14:13 902次阅读
    深入理解<b class='flag-5'>redis</b><b class='flag-5'>分布式</b>锁

    redis分布式锁如何实现

    的情况,分布式锁的作用就是确保在同一时间只有一个客户端可以访问共享资源,从而保证数据的一致性和正确性。 下面将详细介绍Redis分布式锁的实现
    的头像 发表于 11-16 11:29 497次阅读

    redis分布式锁死锁处理方案

    引言: 随着分布式系统的广泛应用,尤其是在大规模并发操作下,对并发控制的需求越来越高。Redis分布式锁作为一种常见的分布式实现方案,由于
    的头像 发表于 11-16 11:44 1686次阅读

    redis分布式锁的应用场景有哪些

    Redis分布式锁是一种基于Redis实现分布式锁机制,可以在分布式环境下确保资源的独占性,避
    的头像 发表于 12-04 11:21 1391次阅读

    如何实现Redis分布式

    机制,下面将详细介绍如何实现Redis分布式锁。 一、引言 在分布式系统中,多个节点可能同时读写同一共享资源。如果没有
    的头像 发表于 12-04 11:24 659次阅读

    redis分布式锁的缺点

    Redis分布式锁是一种常见的用于解决分布式系统中资源争用问题的解决方案。尽管Redis分布式锁具有很多优点,但它也存在一些缺点。本文将从几
    的头像 发表于 12-04 14:05 1201次阅读