redis zset實現滑動窗口


限流

需求背景:同一用戶1分鍾內登錄失敗次數超過3次,頁面添加驗證碼登錄驗證,也即是限流的思想。

常見的限流算法:固定窗口計數器;滑動窗口計數器;漏桶;令牌桶。本篇選擇的滑動窗口計數器

redis zset特性

Redis 有序集合(sorted set)和集合(set)一樣也是 string 類型元素的集合,且不允許重復的成員。不同的是每個元素都會關聯一個 double 類型的分數(score)。redis 正是通過分數來為集合中的成員進行從小到大的排序。

可參考java的LinkedHashMap和HashMap,都是通過多維護變量使無序的集合變成有序的。區別是LinkedHashMap內部是多維護了2個成員變量Entry<K,V> before, after用於雙向鏈表的連接,redis zset是多維護了一個score變量完成順序的排列。

有序集合的成員是唯一的,但分數(score)可以重復。

滑動窗口算法

滑動窗口算法思想就是記錄一個滑動的時間窗口內的操作次數,操作次數超過閾值則進行限流。

網上找的圖:

 

 

 

 

 

java代碼實現

key使用用戶的登錄名,value數據類型使用zset,zset的score使用當前登錄時間戳,value也使用當前登錄時間戳。

key雖然我用的登錄名(已滿足我的需求),但建議實際應用時使用uid等具有唯一標識的字段。zset要求value唯一不可重復,所以當前時間戳需不需要再添加一隨機數來做唯一標識待驗證。

import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.stereotype.Component;

/**
* redis使用zset實現滑動窗口計數
* key:sliding_window_用戶登錄名
* value(zset):value=當前時間戳,score=當前時間戳
*
* @author yangzihe
* @date 2021/5/27
*/
@Component
@Slf4j
public class SlidingWindowCounter {

/**
* redis key前綴
*/
private static final String SLIDING_WINDOW = "sliding_window_";

@Autowired
private RedisTemplate redisTemplate;

/**
* 判斷key的value中的有效訪問次數是否超過最大限定值maxCount
* 判斷與數量增長分開處理
*
* @param key redis key
* @param windowInSecond 窗口間隔,秒
* @param maxCount 最大計數
*
* @return 是 or 否
*/
public boolean overMaxCount(String key, int windowInSecond, long maxCount) {
key = SLIDING_WINDOW + key;
log.info("redis key = {}", key);
// 當前時間
long currentMs = System.currentTimeMillis();
// 窗口開始時間
long windowStartMs = currentMs - windowInSecond * 1000L;
// 按score統計key的value中的有效數量
Long count = redisTemplate.opsForZSet().count(key, windowStartMs, currentMs);
// 已訪問次數 >= 最大可訪問值
return count >= maxCount;
}

/**
* 判斷key的value中的有效訪問次數是否超過最大限定值maxCount,若沒超過,調用increment方法,將窗口內的訪問數加一
* 判斷與數量增長同步處理
*
* @param key redis key
* @param windowInSecond 窗口間隔,秒
* @param maxCount 最大計數
*
* @return 可訪問 or 不可訪問
*/
public boolean canAccess(String key, int windowInSecond, long maxCount) {
key = SLIDING_WINDOW + key;
log.info("redis key = {}", key);
//按key統計集合中的有效數量
Long count = redisTemplate.opsForZSet().zCard(key);
if (count < maxCount) {
increment(key, windowInSecond);
return true;
} else {
return false;
}
}

/**
* 滑動窗口計數增長
*
* @param key redis key
* @param windowInSecond 窗口間隔,秒
*/
public void increment(String key, Integer windowInSecond) {
// 當前時間
long currentMs = System.currentTimeMillis();
// 窗口開始時間
long windowStartMs = currentMs - windowInSecond * 1000;
// 單例模式(提升性能)
ZSetOperations zSetOperations = redisTemplate.opsForZSet();
// 清除窗口過期成員
zSetOperations.removeRangeByScore(key, 0, windowStartMs);
// 添加當前時間 value=當前時間戳 score=當前時間戳
zSetOperations.add(key, String.valueOf(currentMs), currentMs);
// 設置key過期時間
redisTemplate.expire(key, windowInSecond, TimeUnit.SECONDS);
}
}

————————————————
版權聲明:本文為CSDN博主「yzh_1346983557」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/yzh_1346983557/article/details/117397502

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM