詳解Redis如何多規(guī)則限流和防重復(fù)提交
一、簡(jiǎn)介
市面上很多介紹redis如何實(shí)現(xiàn)限流的,但是大部分都有一個(gè)缺點(diǎn),就是只能實(shí)現(xiàn)單一的限流,比如1分鐘訪問1次或者60分鐘訪問10次這種,但是如果想一個(gè)接口兩種規(guī)則都需要滿足呢,我們的項(xiàng)目又是分布式項(xiàng)目,應(yīng)該如何解決,下面就介紹一下redis實(shí)現(xiàn)分布式多規(guī)則限流的方式。
二、思考
- 如何一分鐘只能發(fā)送一次驗(yàn)證碼,一小時(shí)只能發(fā)送10次驗(yàn)證碼等等多種規(guī)則的限流
- 如何防止接口被惡意打擊(短時(shí)間內(nèi)大量請(qǐng)求)
- 如何限制接口規(guī)定時(shí)間內(nèi)訪問次數(shù)
三、解決方法
1. 使用 String結(jié)構(gòu) 記錄固定時(shí)間段內(nèi)某用戶IP訪問某接口的次數(shù)
- RedisKey = prefix : className : methodName
- RedisVlue = 訪問次數(shù)
攔截請(qǐng)求:
- 初次訪問時(shí)設(shè)置 [RedisKey] [RedisValue=1] [規(guī)定的過(guò)期時(shí)間]
- 獲取 RedisValue 是否超過(guò)規(guī)定次數(shù),超過(guò)則攔截,未超過(guò)則對(duì) RedisKey 進(jìn)行加1
分析: 規(guī)則是每分鐘訪問 1000 次
1.考慮并發(fā)問題
- 假設(shè)目前 RedisKey => RedisValue 為 999
- 目前大量請(qǐng)求進(jìn)行到第一步( 獲取Redis請(qǐng)求次數(shù) ),那么所有線程都獲取到了值為999,進(jìn)行判斷都未超過(guò)限定次數(shù)則不攔截,導(dǎo)致實(shí)際次數(shù)超過(guò) 1000 次
- 解決辦法: 保證方法執(zhí)行原子性(加鎖、lua)
2.考慮在臨界值進(jìn)行訪問
思考下圖

代碼實(shí)現(xiàn): 比較簡(jiǎn)單(可參考若依代碼 gitee.com/y_project/RuoYi-Vue/blob/master/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/RateLimiterAspect.java )
2. 使用 Zset 進(jìn)行存儲(chǔ),解決臨界值訪問問題

網(wǎng)上幾乎都有實(shí)現(xiàn),這里就不過(guò)多介紹
3.實(shí)現(xiàn)多規(guī)則限流
3.1 先確定最終需要的效果
- 能實(shí)現(xiàn)多種限流規(guī)則
- 能實(shí)現(xiàn)防重復(fù)提交
通過(guò)以上要求設(shè)計(jì)注解(先想象出最終實(shí)現(xiàn)效果)
@RateLimiter(
rules = {
// 60秒內(nèi)只能訪問10次
@RateRule(count = 10, time = 60, timeUnit = TimeUnit.SECONDS),
// 120秒內(nèi)只能訪問20次
@RateRule(count = 20, time = 120, timeUnit = TimeUnit.SECONDS)
},
// 防重復(fù)提交 (5秒鐘只能訪問1次)
preventDuplicate = true
)
3.2 編寫注解(RateLimiter,RateRule)
編寫 RateLimiter 注解
/**
* @Description: 請(qǐng)求接口限制
* @Author: yiFei
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface RateLimiter {
/**
* 限流key
*/
String key() default RedisKeyConstants.RATE_LIMIT_CACHE_PREFIX;
/**
* 限流類型 ( 默認(rèn) Ip 模式 )
*/
LimitTypeEnum limitType() default LimitTypeEnum.IP;
/**
* 錯(cuò)誤提示
*/
ResultCode message() default ResultCode.REQUEST_MORE_ERROR;
/**
* 限流規(guī)則 (規(guī)則不可變,可多規(guī)則)
*/
RateRule[] rules() default {};
/**
* 防重復(fù)提交值
*/
boolean preventDuplicate() default false;
/**
* 防重復(fù)提交默認(rèn)值
*/
RateRule preventDuplicateRule() default @RateRule(count = 1, time = 5);
}
編寫RateRule注解
@Target(ElementType.ANNOTATION_TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface RateRule {
/**
* 限流次數(shù)
*/
long count() default 10;
/**
* 限流時(shí)間
*/
long time() default 60;
/**
* 限流時(shí)間單位
*/
TimeUnit timeUnit() default TimeUnit.SECONDS;
}
3.3 攔截注解 RateLimiter
確定redis存儲(chǔ)方式
- RedisKey = prefix : className : methodName
- RedisScore = 時(shí)間戳
- RedisValue = 任意分布式不重復(fù)的值即可
編寫生成 RedisKey 的方法
/**
* 通過(guò) rateLimiter 和 joinPoint 拼接 prefix : ip / userId : classSimpleName - methodName
*
* @param rateLimiter 提供 prefix
* @param joinPoint 提供 classSimpleName : methodName
* @return
*/
public String getCombineKey(RateLimiter rateLimiter, JoinPoint joinPoint) {
StringBuffer key = new StringBuffer(rateLimiter.key());
// 不同限流類型使用不同的前綴
switch (rateLimiter.limitType()) {
// XXX 可以新增通過(guò)參數(shù)指定參數(shù)進(jìn)行限流
case IP:
key.append(IpUtil.getIpAddr(((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest())).append(":");
break;
case USER_ID:
SysUserDetails user = SecurityUtil.getUser();
if (!ObjectUtils.isEmpty(user)) key.append(user.getUserId()).append(":");
break;
case GLOBAL:
break;
}
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
Class<?> targetClass = method.getDeclaringClass();
key.append(targetClass.getSimpleName()).append("-").append(method.getName());
return key.toString();
}
3.4 編寫lua腳本 (兩種將時(shí)間添加到Redis的方法)
3.4.1 UUID(可用其他有相同的特性的值)為Zset中的value值
參數(shù)介紹
- KEYS[1] = prefix : ? : className : methodName
- KEYS[2] = 唯一ID
- KEYS[3] = 當(dāng)前時(shí)間
- ARGV = [次數(shù),單位時(shí)間,次數(shù),單位時(shí)間, 次數(shù), 單位時(shí)間 ...]
由java傳入分布式不重復(fù)的 value 值
-- 1. 獲取參數(shù)
local key = KEYS[1]
local uuid = KEYS[2]
local currentTime = tonumber(KEYS[3])
-- 2. 以數(shù)組最大值為 ttl 最大值
local expireTime = -1;
-- 3. 遍歷數(shù)組查看是否超過(guò)限流規(guī)則
for i = 1, #ARGV, 2 do
local rateRuleCount = tonumber(ARGV[i])
local rateRuleTime = tonumber(ARGV[i + 1])
-- 3.1 判斷在單位時(shí)間內(nèi)訪問次數(shù)
local count = redis.call('ZCOUNT', key, currentTime - rateRuleTime, currentTime)
-- 3.2 判斷是否超過(guò)規(guī)定次數(shù)
if tonumber(count) >= rateRuleCount then
return true
end
-- 3.3 判斷元素最大值,設(shè)置為最終過(guò)期時(shí)間
if rateRuleTime > expireTime then
expireTime = rateRuleTime
end
end
-- 4. redis 中添加當(dāng)前時(shí)間
redis.call('ZADD', key, currentTime, uuid)
-- 5. 更新緩存過(guò)期時(shí)間
redis.call('PEXPIRE', key, expireTime)
-- 6. 刪除最大時(shí)間限度之前的數(shù)據(jù),防止數(shù)據(jù)過(guò)多
redis.call('ZREMRANGEBYSCORE', key, 0, currentTime - expireTime)
return false
3.4.2 根據(jù)時(shí)間戳作為Zset中的value值
參數(shù)介紹
- KEYS[1] = prefix : ? : className : methodName
- KEYS[2] = 當(dāng)前時(shí)間
- ARGV = [次數(shù),單位時(shí)間,次數(shù),單位時(shí)間, 次數(shù), 單位時(shí)間 ...]
根據(jù)時(shí)間進(jìn)行生成value值,考慮同一毫秒添加相同時(shí)間值問題
以下為第二種實(shí)現(xiàn)方式,在并發(fā)高的情況下效率低,value是通過(guò)時(shí)間戳進(jìn)行添加,但是訪問量大的話會(huì)使得一直在調(diào)用 redis.call('ZADD', key, currentTime, currentTime),但是在不沖突value的情況下,會(huì)比生成 UUID 好
-- 1. 獲取參數(shù)
local key = KEYS[1]
local currentTime = KEYS[2]
-- 2. 以數(shù)組最大值為 ttl 最大值
local expireTime = -1;
-- 3. 遍歷數(shù)組查看是否越界
for i = 1, #ARGV, 2 do
local rateRuleCount = tonumber(ARGV[i])
local rateRuleTime = tonumber(ARGV[i + 1])
-- 3.1 判斷在單位時(shí)間內(nèi)訪問次數(shù)
local count = redis.call('ZCOUNT', key, currentTime - rateRuleTime, currentTime)
-- 3.2 判斷是否超過(guò)規(guī)定次數(shù)
if tonumber(count) >= rateRuleCount then
return true
end
-- 3.3 判斷元素最大值,設(shè)置為最終過(guò)期時(shí)間
if rateRuleTime > expireTime then
expireTime = rateRuleTime
end
end
-- 4. 更新緩存過(guò)期時(shí)間
redis.call('PEXPIRE', key, expireTime)
-- 5. 刪除最大時(shí)間限度之前的數(shù)據(jù),防止數(shù)據(jù)過(guò)多
redis.call('ZREMRANGEBYSCORE', key, 0, currentTime - expireTime)
-- 6. redis 中添加當(dāng)前時(shí)間 ( 解決多個(gè)線程在同一毫秒添加相同 value 導(dǎo)致 Redis 漏記的問題 )
-- 6.1 maxRetries 最大重試次數(shù) retries 重試次數(shù)
local maxRetries = 5
local retries = 0
while true do
local result = redis.call('ZADD', key, currentTime, currentTime)
if result == 1 then
-- 6.2 添加成功則跳出循環(huán)
break
else
-- 6.3 未添加成功則 value + 1 再次進(jìn)行嘗試
retries = retries + 1
if retries >= maxRetries then
-- 6.4 超過(guò)最大嘗試次數(shù) 采用添加隨機(jī)數(shù)策略
local random_value = math.random(1, 1000)
currentTime = currentTime + random_value
else
currentTime = currentTime + 1
end
end
end
return false
3.5 編寫 AOP 攔截
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private RedisScript<Boolean> limitScript;
/**
* 限流
* XXX 對(duì)限流要求比較高,可以使用在 Redis中對(duì)規(guī)則進(jìn)行存儲(chǔ)校驗(yàn) 或者使用中間件
*
* @param joinPoint joinPoint
* @param rateLimiter 限流注解
*/
@Before(value = "@annotation(rateLimiter)")
public void boBefore(JoinPoint joinPoint, RateLimiter rateLimiter) {
// 1. 生成 key
String key = getCombineKey(rateLimiter, joinPoint);
try {
// 2. 執(zhí)行腳本返回是否限流
Boolean flag = redisTemplate.execute(limitScript,
ListUtil.of(key, String.valueOf(System.currentTimeMillis())),
(Object[]) getRules(rateLimiter));
// 3. 判斷是否限流
if (Boolean.TRUE.equals(flag)) {
log.error("ip: '{}' 攔截到一個(gè)請(qǐng)求 RedisKey: '{}'",
IpUtil.getIpAddr(((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest()),
key);
throw new ServiceException(rateLimiter.message());
}
} catch (ServiceException e) {
throw e;
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 獲取規(guī)則
*
* @param rateLimiter 獲取其中規(guī)則信息
* @return
*/
private Long[] getRules(RateLimiter rateLimiter) {
int capacity = rateLimiter.rules().length << 1;
// 1. 構(gòu)建 args
Long[] args = new Long[rateLimiter.preventDuplicate() ? capacity + 2 : capacity];
// 3. 記錄數(shù)組元素
int index = 0;
// 2. 判斷是否需要添加防重復(fù)提交到redis進(jìn)行校驗(yàn)
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;
}
java代碼 RateLimiterAspect.java
以上就是詳解Redis如何多規(guī)則限流和防重復(fù)提交的詳細(xì)內(nèi)容,更多關(guān)于Redis多規(guī)則限流的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Redis核心原理與實(shí)踐之字符串實(shí)現(xiàn)原理
這本書深入地分析了Redis常用特性的內(nèi)部機(jī)制與實(shí)現(xiàn)方式,內(nèi)容源自對(duì)Redis源碼的分析,并從中總結(jié)出設(shè)計(jì)思路、實(shí)現(xiàn)原理。對(duì)Redis字符串實(shí)現(xiàn)原理相關(guān)知識(shí)感興趣的朋友一起看看吧2021-09-09
Windows操作系統(tǒng)下Redis服務(wù)安裝圖文教程
這篇文章主要介紹了Windows操作系統(tǒng)下Redis服務(wù)安裝圖文教程,文中給大家提供了redis的下載地址,安裝程序步驟,需要的朋友可以參考下2018-03-03
Redis常用數(shù)據(jù)類型命令實(shí)例匯總
這篇文章主要介紹了Redis常用數(shù)據(jù)類型命令實(shí)例匯總,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-10-10
Redis中的動(dòng)態(tài)字符串學(xué)習(xí)教程
這篇文章主要介紹了Redis中的動(dòng)態(tài)字符串學(xué)習(xí)教程,以sds模塊的使用為主進(jìn)行講解,需要的朋友可以參考下2015-08-08
Redis集群節(jié)點(diǎn)通信過(guò)程/原理流程分析
這篇文章主要介紹了Redis集群節(jié)點(diǎn)通信過(guò)程/原理,詳細(xì)介紹了Cluster(集群)的節(jié)點(diǎn)通信的流程,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-03-03
Redis分布式鎖python-redis-lock使用方法
這篇文章主要介紹了Redis分布式鎖python-redis-lock使用方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-11-11
使用Redis存儲(chǔ)SpringBoot項(xiàng)目中Session的詳細(xì)步驟
在開發(fā)Spring Boot項(xiàng)目時(shí),我們通常會(huì)遇到如何高效管理Session的問題,默認(rèn)情況下,Spring Boot會(huì)將Session存儲(chǔ)在內(nèi)存中,今天,我們將學(xué)習(xí)如何將Session存儲(chǔ)從內(nèi)存切換到Redis,并驗(yàn)證配置是否成功,需要的朋友可以參考下2024-06-06
使用Redis實(shí)現(xiàn)記錄訪問次數(shù)的三種方案
這篇文章主要介紹了使用Redis實(shí)現(xiàn)記錄訪問次數(shù)的三種方案,文中通過(guò)代碼示例和圖文講解的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作有一定的幫助,需要的朋友可以參考下2024-09-09
Windows環(huán)境下Redis Cluster環(huán)境搭建(圖文)
這篇文章主要介紹了Windows環(huán)境下Redis Cluster環(huán)境搭建(圖文),小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-07-07

