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.

Architect's Guide
Architect's Guide
Architect's Guide
Redis Multi-Rule Rate Limiting & Duplicate Submission Prevention in Distributed Systems

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 false

Approach 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 false

Approach 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
)
@RateLimiter

attributes: 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

RateLimiterAspect

uses @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;
}
getCombineKey

builds 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

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

distributed systemsJavaRedisrate limitingZsetSpring AOPLua scriptingduplicate prevention
Architect's Guide
Written by

Architect's Guide

Dedicated to sharing programmer-architect skills—Java backend, system, microservice, and distributed architectures—to help you become a senior architect.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.