Understanding Interface Idempotency and Distributed Rate Limiting with Token Bucket, Leaky Bucket, Guava RateLimiter, Nginx, and Redis+Lua
This article explains the concept of interface idempotency, demonstrates how to achieve idempotent update operations using version control and token mechanisms, and provides a comprehensive guide to distributed rate limiting—including time‑window and resource‑based dimensions, token‑bucket and leaky‑bucket algorithms, and practical implementations with Guava RateLimiter, Nginx, and Redis‑Lua scripts.
1. Interface Idempotency
Interface idempotency means that multiple identical requests produce the same result without side effects; a typical example is a payment request that could be retried due to network failure, potentially causing double charging.
The core idea is to guarantee idempotency by using a unique business identifier and, in concurrent scenarios, applying a lock during the operation.
1) Idempotent Update Operations
1) Update based on a unique business identifier
Version‑based optimistic locking can ensure idempotent updates: the client reads the current version, submits the new data together with the version, and the backend updates only when the version matches.
update set version = version + 1, xxx=${xxx} where id = xxx and version = ${version};2) Token mechanism for update/insert idempotency
1) Operations without a unique business identifier
When a user accesses the registration page, the backend generates a token and returns it in a hidden field. The token is sent back on submission, used to acquire a distributed lock, and the insert operation proceeds only if the lock is held; the lock is released automatically after expiration.
2. Distributed Rate Limiting
1) Dimensions of distributed rate limiting
Time‑based limiting uses a time window (e.g., per second, per minute). Resource‑based limiting caps the number of accesses or concurrent connections.
In real scenarios, multiple rules are combined, such as limiting each IP to 10 requests per second, limiting connections per server, and applying higher‑level limits for a whole server group.
2) Common rate‑limiting algorithms
1) Token Bucket Algorithm
The token bucket algorithm has two key components: a bucket that stores tokens and a token generator that adds tokens at a fixed rate. A request can proceed only when it successfully acquires a token; otherwise it is queued or rejected.
Token generation is performed at a steady rate (e.g., 100 tokens per second). If the bucket is full, new tokens are discarded.
When a request arrives, it must obtain a token. If tokens are exhausted, the request can be placed in a buffer queue (optional) or rejected.
2) Leaky Bucket Algorithm
The leaky bucket algorithm is similar to the token bucket but operates on request packets instead of tokens. Packets are added to the bucket; if the bucket is full, excess packets are dropped.
The bucket drains at a constant rate, ensuring a steady output flow regardless of bursty input.
3) Main distributed rate‑limiting solutions
1) Guava RateLimiter client‑side limiting
Add the Guava dependency:
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>18.0</version>
</dependency>Example controller:
@RestController
@Slf4j
public class Controller {
// 2 tokens per second
RateLimiter limiter = RateLimiter.create(2.0);
// Non‑blocking limit
@GetMapping("/tryAcquire")
public String tryAcquire(Integer count) {
if (limiter.tryAcquire(count)) {
log.info("Success, rate = {}", limiter.getRate());
return "success";
} else {
log.info("Rejected, rate = {}", limiter.getRate());
return "fail";
}
}
// Blocking limit with timeout
@GetMapping("/tryAcquireWithTimeout")
public String tryAcquireWithTimeout(Integer count, Integer timeout) {
if (limiter.tryAcquire(count, timeout, TimeUnit.SECONDS)) {
log.info("Success, rate = {}", limiter.getRate());
return "success";
} else {
log.info("Rejected, rate = {}", limiter.getRate());
return "fail";
}
}
// Synchronous blocking limit
@GetMapping("/acquire")
public String acquire(Integer count) {
limiter.acquire(count);
log.info("Success, rate = {}", limiter.getRate());
return "success";
}
}2) Nginx rate limiting
Configure host mapping: 127.0.0.1 www.test.com Edit /usr/local/nginx/conf/nginx.conf and add a limit zone:
# Define a memory zone for IP‑based limiting
limit_req_zone $binary_remote_addr zone=iplimit:20m rate=1r/s;
server {
server_name www.test.com;
location /access-limit/ {
proxy_pass http://127.0.0.1:8080/;
# Apply the limit with a burst of 2 and no delay
limit_req zone=iplimit burst=2 nodelay;
}
}3) Redis + Lua distributed limiting
Lua script (saved as rateLimiter.lua):
-- Get the method key
local methodKey = KEYS[1]
local limit = tonumber(ARGV[1])
local count = tonumber(redis.call('get', methodKey) or "0")
if count + 1 > limit then
return false
else
redis.call('INCRBY', methodKey, 1)
redis.call('EXPIRE', methodKey, 1)
return true
endSpring configuration to load the script:
@Configuration
public class RedisConfiguration {
@Bean
public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory factory) {
return new StringRedisTemplate(factory);
}
@Bean
public DefaultRedisScript<Boolean> loadRedisScript() {
DefaultRedisScript<Boolean> script = new DefaultRedisScript<>();
script.setLocation(new ClassPathResource("rateLimiter.lua"));
script.setResultType(Boolean.class);
return script;
}
}Rate‑limiting service using the script:
@Service
@Slf4j
public class AccessLimiter {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private RedisScript<Boolean> rateLimitLua;
public void limitAccess(String key, Integer limit) {
Boolean allowed = stringRedisTemplate.execute(rateLimitLua, Collections.singletonList(key), limit.toString());
if (!allowed) {
log.error("Your access is blocked, key={}", key);
throw new RuntimeException("Your access is blocked");
}
}
}Controller that uses the limiter:
@RestController
@Slf4j
public class Controller {
@Autowired
private AccessLimiter accessLimiter;
@GetMapping("/test")
public String test() {
accessLimiter.limitAccess("ratelimiter-test", 1);
return "success";
}
}4) Custom annotation and AOP for automatic limiting
Define annotation:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AccessLimiterAop {
int limit();
String methodKey() default "";
}Aspect that applies the limiter before method execution:
@Aspect
@Component
@Slf4j
public class AccessLimiterAspect {
@Autowired
private AccessLimiter accessLimiter;
@Pointcut("@annotation(com.example.AccessLimiterAop)")
public void cut() {}
@Before("cut()")
public void before(JoinPoint joinPoint) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
AccessLimiterAop annotation = method.getAnnotation(AccessLimiterAop.class);
if (annotation == null) return;
String key = annotation.methodKey();
int limit = annotation.limit();
if (StringUtils.isEmpty(key)) {
key = method.getName();
Class<?>[] types = method.getParameterTypes();
if (types != null) {
String params = Arrays.stream(types).map(Class::getName).collect(Collectors.joining(","));
key += "#" + params;
}
}
accessLimiter.limitAccess(key, limit);
}
}Usage in a controller:
@RestController
@Slf4j
public class Controller {
@Autowired
private AccessLimiter accessLimiter;
@GetMapping("/test")
@AccessLimiterAop(limit = 1)
public String test() {
return "success";
}
}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.
Top Architect
Top Architect focuses on sharing practical architecture knowledge, covering enterprise, system, website, large‑scale distributed, and high‑availability architectures, plus architecture adjustments using internet technologies. We welcome idea‑driven, sharing‑oriented architects to exchange and learn together.
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.
