Redis Multi-Rule Rate Limiting & Duplicate Submission Prevention in Distributed Systems
This article details a distributed Redis-based solution for enforcing multiple rate-limiting rules simultaneously (e.g., 10 requests per minute and 20 per two minutes) while preventing duplicate submissions, using Zset data structures, Lua scripts for atomicity, and Spring AOP annotations.
Introduction
Most Redis rate-limiting tutorials cover only a single rule (e.g., 1 request per minute or 10 per hour). In a distributed system, an endpoint often needs to satisfy several rules at once. This article presents a complete implementation that supports multiple configurable rules and duplicate-submission prevention.
Key Challenges
Enforce multiple rate-limiting rules on the same endpoint (e.g., 1 verification code per minute, 10 per hour).
Protect interfaces from malicious bursts.
Limit total accesses within a specified time window.
Solution Overview
3.1 String-Based Counter (Fixed Window)
Initial approach uses a Redis String key prefix:className:methodName with value = access count and TTL = window length.
Steps on each request:
First access: set key with value 1 and expiry.
Subsequent accesses: read value; if > limit, reject; else increment.
Concurrency issue at boundary: When count = 999 (limit 1000), many threads read 999 simultaneously, all pass the check, then increment — actual requests exceed 1000.
Fix: Ensure atomicity via locking or Lua scripts.
3.2 Zset-Based Sliding Window (Solves Boundary Problem)
Store each request as a Zset member with score = timestamp. ZCOUNT counts members within the sliding window. Two Lua implementations are provided.
Approach A: UUID as Member Value
-- 1. Get parameters
local key = KEYS[1]
local uuid = KEYS[2]
local currentTime = tonumber(KEYS[3])
-- 2. Determine max TTL from rules
local expireTime = -1
-- 3. Iterate rules (ARGV = count, windowMs, count, windowMs, ...)
for i = 1, #ARGV, 2 do
local rateRuleCount = tonumber(ARGV[i])
local rateRuleTime = tonumber(ARGV[i + 1])
-- 3.1 Count requests in window
local count = redis.call('ZCOUNT', key, currentTime - rateRuleTime, currentTime)
-- 3.2 If any rule exceeded, reject
if tonumber(count) >= rateRuleCount then
return true
end
-- 3.3 Track max window for TTL
if rateRuleTime > expireTime then
expireTime = rateRuleTime
end
end
-- 4. Add current request
redis.call('ZADD', key, currentTime, uuid)
-- 5. Set expiry to max window (ms)
redis.call('PEXPIRE', key, expireTime)
-- 6. Remove expired entries
redis.call('ZREMRANGEBYSCORE', key, 0, currentTime - expireTime)
return falseApproach B: Timestamp as Member Value (Handles Same-Millisecond Collisions)
-- 1. Get parameters
local key = KEYS[1]
local currentTime = KEYS[2]
-- 2. Determine max TTL
local expireTime = -1
-- 3. Check each rule
for i = 1, #ARGV, 2 do
local rateRuleCount = tonumber(ARGV[i])
local rateRuleTime = tonumber(ARGV[i + 1])
local count = redis.call('ZCOUNT', key, currentTime - rateRuleTime, currentTime)
if tonumber(count) >= rateRuleCount then
return true
end
if rateRuleTime > expireTime then
expireTime = rateRuleTime
end
end
-- 4. Set expiry
redis.call('PEXPIRE', key, expireTime)
-- 5. Clean old entries
redis.call('ZREMRANGEBYSCORE', key, 0, currentTime - expireTime)
-- 6. Add with retry on duplicate score
local maxRetries = 5
local retries = 0
while true do
local result = redis.call('ZADD', key, currentTime, currentTime)
if result == 1 then
break
else
retries = retries + 1
if retries >= maxRetries then
local random_value = math.random(1, 1000)
currentTime = currentTime + random_value
else
currentTime = currentTime + 1
end
end
end
return falseApproach B retries up to 5 times with +1 ms increments, then adds a random 1–1000 ms offset to avoid infinite loops under extreme concurrency.
Annotation Design
Two annotations enable declarative configuration:
@RateLimiter(
rules = {
@RateRule(count = 10, time = 60, timeUnit = TimeUnit.SECONDS),
@RateRule(count = 20, time = 120, timeUnit = TimeUnit.SECONDS)
},
preventDuplicate = true
) @RateLimiterattributes: key – Redis key prefix (default RATE_LIMIT_CACHE_PREFIX) limitType – IP, USER_ID, or GLOBAL (default IP) message – error code (default REQUEST_MORE_ERROR) rules – array of
@RateRule preventDuplicate– enable duplicate submission check (default false) preventDuplicateRule – default 1 request per 5 seconds @RateRule attributes: count (default 10), time (default 60), timeUnit (default SECONDS).
AOP Interception
RateLimiterAspectuses @Before("@annotation(rateLimiter)") to intercept annotated methods.
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private RedisScript<Boolean> limitScript;
@Before(value = "@annotation(rateLimiter)")
public void boBefore(JoinPoint joinPoint, RateLimiter rateLimiter) {
String key = getCombineKey(rateLimiter, joinPoint);
try {
Boolean flag = redisTemplate.execute(limitScript,
ListUtil.of(key, String.valueOf(System.currentTimeMillis())),
(Object[]) getRules(rateLimiter));
if (Boolean.TRUE.equals(flag)) {
log.error("ip: '{}' 拦截到一个请求 RedisKey: '{}'",
IpUtil.getIpAddr(...), key);
throw new ServiceException(rateLimiter.message());
}
} catch (ServiceException e) {
throw e;
} catch (Exception e) {
e.printStackTrace();
}
}
private Long[] getRules(RateLimiter rateLimiter) {
int capacity = rateLimiter.rules().length << 1;
Long[] args = new Long[rateLimiter.preventDuplicate() ? capacity + 2 : capacity];
int index = 0;
if (rateLimiter.preventDuplicate()) {
RateRule r = rateLimiter.preventDuplicateRule();
args[index++] = r.count();
args[index++] = r.timeUnit().toMillis(r.time());
}
for (RateRule rule : rateLimiter.rules()) {
args[index++] = rule.count();
args[index++] = rule.timeUnit().toMillis(rule.time());
}
return args;
} getCombineKeybuilds prefix:ip/userId:className-methodName based on limitType.
Reference Implementation
Full source code available at: https://gitee.com/y_project/RuoYi-Vue/blob/master/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/RateLimiterAspect.java
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Architect's Guide
Dedicated to sharing programmer-architect skills—Java backend, system, microservice, and distributed architectures—to help you become a senior architect.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
