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

二:使用 Zset 進行存儲,解決臨界值訪問問題

三:實現多規(guī)則限流
①、先確定最終需要的效果(能實現多種限流規(guī)則+能實現防重復提交)
@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 注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface RateLimiter {
/**
* 限流key
*/
String key() default RedisKeyConstants.RATE_LIMIT_CACHE_PREFIX;
/**
* 限流類型 ( 默認 Ip 模式 )
*/
LimitTypeEnum limitType() default LimitTypeEnum.IP;
/**
* 錯誤提示
*/
ResultCode message() default ResultCode.REQUEST_MORE_ERROR;
/**
* 限流規(guī)則 (規(guī)則不可變,可多規(guī)則)
*/
RateRule[] rules() default {};
/**
* 防重復提交值
*/
boolean preventDuplicate() default false;
/**
* 防重復提交默認值
*/
RateRule preventDuplicateRule() default @RateRule(count = 1, time = 5);
}
RateRule 注解:
@Target(ElementType.ANNOTATION_TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface RateRule {
/**
* 限流次數
*/
long count() default 10;
/**
* 限流時間
*/
long time() default 60;
/**
* 限流時間單位
*/
TimeUnit timeUnit() default TimeUnit.SECONDS;
}
③、攔截注解 RateLimiter
- 確定 Redis 存儲方式
RedisKey = prefix : className : methodName
RedisScore = 時間戳
RedisValue = 任意分布式不重復的值即可 - 編寫生成 RedisKey 的方法
public String getCombineKey(RateLimiter rateLimiter, JoinPoint joinPoint) {
StringBuffer key = new StringBuffer(rateLimiter.key());
// 不同限流類型使用不同的前綴
switch (rateLimiter.limitType()) {
// XXX 可以新增通過參數指定參數進行限流
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();
}
④、編寫Lua腳本(兩種將事件添加到Redis的方法)
Ⅰ:UUID(可用其他有相同的特性的值)為 Zset 中的 value 值
- 參數介紹:
KEYS[1] = prefix : ? : className : methodName
KEYS[2] = 唯一ID
KEYS[3] = 當前時間
ARGV = [次數,單位時間,次數,單位時間, 次數, 單位時間 …] - 由 Java傳入分布式不重復的 value 值
-- 1. 獲取參數
local key = KEYS[1]
local uuid = KEYS[2]
local currentTime = tonumber(KEYS[3])
-- 2. 以數組最大值為 ttl 最大值
local expireTime = -1;
-- 3. 遍歷數組查看是否超過限流規(guī)則
for i = 1, #ARGV, 2 do
local rateRuleCount = tonumber(ARGV[i])
local rateRuleTime = tonumber(ARGV[i + 1])
-- 3.1 判斷在單位時間內訪問次數
local count = redis.call('ZCOUNT', key, currentTime - rateRuleTime, currentTime)
-- 3.2 判斷是否超過規(guī)定次數
if tonumber(count) >= rateRuleCount then
return true
end
-- 3.3 判斷元素最大值,設置為最終過期時間
if rateRuleTime > expireTime then
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)
return false
Ⅱ、根據時間戳作為 Zset 中的 value 值
- 參數介紹
KEYS[1] = prefix : ? : className : methodName
KEYS[2] = 當前時間
ARGV = [次數,單位時間,次數,單位時間, 次數, 單位時間 …] - 根據時間進行生成 value 值,考慮同一毫秒添加相同時間值問題
以下為第二種實現方式,在并發(fā)高的情況下效率低,value 是通過時間戳進行添加,但是訪問量大的話會使得一直在調用 redis.call(‘ZADD’, key, currentTime, currentTime),但是在不沖突 value 的情況下,會比生成 UUID 好。
-- 1. 獲取參數
local key = KEYS[1]
local currentTime = KEYS[2]
-- 2. 以數組最大值為 ttl 最大值
local expireTime = -1;
-- 3. 遍歷數組查看是否越界
for i = 1, #ARGV, 2 do
local rateRuleCount = tonumber(ARGV[i])
local rateRuleTime = tonumber(ARGV[i + 1])
-- 3.1 判斷在單位時間內訪問次數
local count = redis.call('ZCOUNT', key, currentTime - rateRuleTime, currentTime)
-- 3.2 判斷是否超過規(guī)定次數
if tonumber(count) >= rateRuleCount then
return true
end
-- 3.3 判斷元素最大值,設置為最終過期時間
if rateRuleTime > expireTime then
expireTime = rateRuleTime
end
end
-- 4. 更新緩存過期時間
redis.call('PEXPIRE', key, expireTime)
-- 5. 刪除最大時間限度之前的數據,防止數據過多
redis.call('ZREMRANGEBYSCORE', key, 0, currentTime - expireTime)
-- 6. redis 中添加當前時間 ( 解決多個線程在同一毫秒添加相同 value 導致 Redis 漏記的問題 )
-- 6.1 maxRetries 最大重試次數 retries 重試次數
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 再次進行嘗試
retries = retries + 1
if retries >= maxRetries then
-- 6.4 超過最大嘗試次數 采用添加隨機數策略
local random_value = math.random(1, 1000)
currentTime = currentTime + random_value
else
currentTime = currentTime + 1
end
end
end
return false
⑤、編寫AOP攔截
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private RedisScript<Boolean> limitScript;
/**
* 限流
* XXX 對限流要求比較高,可以使用在 Redis中對規(guī)則進行存儲校驗 或者使用中間件
*
* @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: '{}' 攔截到一個請求 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. 構建 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;
}到此這篇關于Redis 多規(guī)則限流和防重復提交方案實現小結的文章就介紹到這了,更多相關Redis 多規(guī)則限流和防重復提交內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
如何監(jiān)聽Redis中Key值的變化(SpringBoot整合)
測試過程中我們有一部分常量值放入redis,共大部分應用調用,但在測試過程中經常有人會清空redis,回歸測試,下面這篇文章主要給大家介紹了關于如何監(jiān)聽Redis中Key值變化的相關資料,需要的朋友可以參考下2024-03-03
在Ubuntu?14.04系統上備份和恢復Redis數據詳細步驟
這篇文章主要給大家介紹了關于在Ubuntu?14.04系統上備份和恢復Redis數據的詳細步驟,文中通過代碼介紹的非常詳細,對大家學習或者使用Redis具有一定的參考借鑒價值,需要的朋友可以參考下2024-04-04
Redis遍歷所有key的兩個命令(KEYS 和 SCAN)
這篇文章主要介紹了Redis遍歷所有key的兩個命令(KEYS 和 SCAN),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2021-04-04

