@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AccessLimit {/*** 限制次數*/int count() default 15;/*** 時間窗口,單位為秒*/int seconds() default 60;
}
@Aspect
@Component
public class AccessLimitAspect {private static final String LUA_SCRIPT ="local key = KEYS[1] " +"local limit = tonumber(ARGV[1]) " +"local current = tonumber(redis.call('get', key) or '0') " +"if current + 1 > limit then " +" return 0 " +"else " +" redis.call('INCR', key) " +" redis.call('EXPIRE', key, ARGV[2]) " +" return 1 " +"end";private static final RedisScript<Long> SCRIPT_INSTANCE = new DefaultRedisScript<>(LUA_SCRIPT, Long.class);@Autowiredprivate RedisTemplate<String, Object> redisTemplate;@Before("@annotation(accessLimit)")public void checkAccessLimit(JoinPoint joinPoint, AccessLimit accessLimit) throws Throwable {validateAccessLimitParams(accessLimit);HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();String ipAddr = IpUtils.getIpAddr(request);String cacheKey = generateCacheKey(joinPoint, ipAddr);Long result = redisTemplate.execute(SCRIPT_INSTANCE,Collections.singletonList(cacheKey),accessLimit.count(),accessLimit.seconds());if (result != null && result == 0) {throw new RateLimitExceededException("操作過于頻繁,請稍后再試");}}private void validateAccessLimitParams(AccessLimit accessLimit) {if (accessLimit.count() <= 0 || accessLimit.seconds() <= 0) {throw new IllegalArgumentException("Invalid Access Limit parameters");}}private String generateCacheKey(JoinPoint joinPoint, String ipAddr) {MethodSignature signature = (MethodSignature) joinPoint.getSignature();String methodName = signature.getMethod().getName();return TradeCachePrefix.ACCESS_LIMIT_PREFIX + methodName + "_" + ipAddr;}}
代碼邏輯
- 定義和初始化變量:
key = KEYS[1]
:從傳入的鍵列表中獲取第一個鍵,作為 Redis 中存儲計數器的鍵。limit = tonumber(ARGV[1])
:將傳入參數中的第一個值轉換為數字,這個值代表允許的最大請求次數(限制)。current = tonumber(redis.call('get', key) or '0')
:嘗試從 Redis 中獲取當前計數器的值。如果鍵不存在,使用默認值'0'
。
- 判斷是否超出限制:
if current + 1 > limit then
: 檢查當前計數加一是否超過限制。- 如果超過限制,則返回
0
,表示請求被拒絕。
- 如果超過限制,則返回
- 更新計數器和設置過期時間:
else
: 如果沒有超過限制:redis.call('INCR', key)
: 將計數器加一。redis.call('EXPIRE', key, ARGV[2])
: 設置該鍵的過期時間為傳入參數中的第二個值(秒)。- 返回
1
,表示請求被接受。
應用場景
- 限流機制:這個腳本通常用于實現基于 Redis 的限流功能。例如,在一定時間窗口內,只允許某個操作執行一定次數,以防止濫用。
- API 請求限制:可以用于限制 API 的調用頻率,每個用戶或每個 IP 地址在特定時間內只能調用 API 一定次數。
使用方法
- 執行腳本時需要傳遞兩個參數:
- 第一個參數是允許的最大請求次數(
limit
)。 - 第二個參數是鍵的過期時間(單位為秒),即在多長時間內重置計數器。
- 第一個參數是允許的最大請求次數(
通過這種方式,可以有效地控制訪問頻率,保護系統資源不被濫用。