Overview
This article describes a clean approach to implementing distributed locking and rate limiting using Redis in Spring Boot applications. The solution leverages Redis atomic operations for thread-safe state management across multiple service instances.
Why Redis?
Redis provides atomic operations that guarantee complete execution or no execution at all. This makes it ideal for:
- Counting operations where accuracy matters
- Distributed key-value storage across multiple services
- Time-based lock expiration through built-in TTL functionality
The implementation uses Redis for both locking (checking key existence) and rate limiting (atomic increment/decrement operations).
Implementation
Redis Configuration
Configure Redis connection propertise:
server.port=8080
spring.redis.host=localhost
spring.redis.database=0
spring.redis.port=6379
Create a Redis configuration class:
package com.example.distributed.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfiguration {
@Bean
public RedisTemplate<String, Object> redisTemplate(
RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(new StringRedisSerializer());
return template;
}
}
Redis Service Abstraction
Define the service interface:
package com.example.distributed.service;
public interface CacheService {
void store(String key, String value);
void store(String key, String value, int expirationMinutes);
String retrieve(String key);
Long fetchAsLong(String key);
Long incrementValue(String key);
Long decrementValue(String key);
String fetchAndRemove(String key);
}
Implement the service:
package com.example.distributed.service.impl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import com.example.distributed.service.CacheService;
import java.util.concurrent.TimeUnit;
@Service
public class CacheServiceImpl implements CacheService {
@Autowired
private RedisTemplate<String, String> cacheTemplate;
@Override
public void store(String key, String value) {
cacheTemplate.opsForValue().set(key, value);
}
@Override
public void store(String key, String value, int expirationMinutes) {
cacheTemplate.opsForValue().set(key, value, expirationMinutes, TimeUnit.MINUTES);
}
@Override
public String retrieve(String key) {
return cacheTemplate.opsForValue().get(key);
}
@Override
public Long fetchAsLong(String key) {
String val = retrieve(key);
return val == null ? 0L : Long.parseLong(val);
}
@Override
public Long incrementValue(String key) {
return cacheTemplate.opsForValue().increment(key);
}
@Override
public Long decrementValue(String key) {
return cacheTemplate.opsForValue().decrement(key);
}
@Override
public String fetchAndRemove(String key) {
return cacheTemplate.opsForValue().getAndDelete(key);
}
}
Custom Annotations
Define lock type enum:
package com.example.distributed.annotation;
public enum LockScope {
BY_CLIENT_IP(1),
BY_USER_ID(2),
BY_CONCURRENT_ACCESS(3),
BY_SINGLE_INSTANCE(4);
private final int code;
LockScope(int code) {
this.code = code;
}
public int getCode() {
return code;
}
}
Create the annotation:
package com.example.distributed.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DistributedLock {
LockScope scope() default LockScope.BY_CLIENT_IP;
}
Interceptor Configuration
Register the intercepter in web configuration:
package com.example.distributed.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import com.example.distributed.interceptor.LockInterceptor;
@Component
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private LockInterceptor lockInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(lockInterceptor).addPathPatterns("/**");
}
}
Lock Interceptor Implementation
package com.example.distributed.interceptor;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.servlet.ServletUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import com.example.distributed.annotation.DistributedLock;
import com.example.distributed.annotation.LockScope;
import com.example.distributed.service.CacheService;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
@Slf4j
public class LockInterceptor implements HandlerInterceptor {
@Autowired
private CacheService cacheService;
private static final String LOCK_PREFIX = "app:lock:";
@Override
public boolean preHandle(
HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
if (handler instanceof HandlerMethod) {
HandlerMethod method = (HandlerMethod) handler;
DistributedLock lock = method.getMethodAnnotation(DistributedLock.class);
if (lock == null) {
return true;
}
switch (lock.scope()) {
case BY_CLIENT_IP:
applyIpLock(request);
break;
case BY_USER_ID:
applyUserLock(request);
break;
case BY_CONCURRENT_ACCESS:
applyConcurrencyLock(method);
break;
case BY_SINGLE_INSTANCE:
applySingletonLock(method);
break;
}
}
return true;
}
@Override
public void afterCompletion(
HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex) throws Exception {
if (handler instanceof HandlerMethod) {
HandlerMethod method = (HandlerMethod) handler;
DistributedLock lock = method.getMethodAnnotation(DistributedLock.class);
if (lock == null) {
return;
}
switch (lock.scope()) {
case BY_CLIENT_IP:
releaseIpLock(request);
break;
case BY_USER_ID:
releaseUserLock(request);
break;
case BY_CONCURRENT_ACCESS:
releaseConcurrencyLock(method);
break;
case BY_SINGLE_INSTANCE:
releaseSingletonLock(method);
break;
}
}
}
private void applyConcurrencyLock(HandlerMethod method) {
String lockKey = LOCK_PREFIX + "concurrent:"
+ method.getMethod().getDeclaringClass().getName()
+ ":" + method.getMethod().toGenericString().replace(" ", "");
long currentCount = cacheService.fetchAsLong(lockKey);
log.info("Active requests: {}", currentCount);
Assert.isTrue(currentCount < 10,
"System busy. Active requests: " + currentCount);
cacheService.incrementValue(lockKey);
}
private void releaseConcurrencyLock(HandlerMethod method) {
String lockKey = LOCK_PREFIX + "concurrent:"
+ method.getMethod().getDeclaringClass().getName()
+ ":" + method.getMethod().toGenericString().replace(" ", "");
long currentCount = cacheService.decrementValue(lockKey);
log.info("Active requests after release: {}", currentCount);
}
private void applyIpLock(HttpServletRequest request) {
String clientIp = ServletUtil.getClientIP(request);
String lockKey = LOCK_PREFIX + "ip:" + clientIp;
if (StrUtil.isEmpty(cacheService.retrieve(lockKey))) {
cacheService.store(lockKey, clientIp, 1);
return;
}
throw new RuntimeException("Please wait before retrying");
}
private void releaseIpLock(HttpServletRequest request) {
String clientIp = ServletUtil.getClientIP(request);
String lockKey = LOCK_PREFIX + "ip:" + clientIp;
cacheService.fetchAndRemove(lockKey);
}
private void applyUserLock(HttpServletRequest request) {
String userId = request.getHeader("X-User-Id");
if (StrUtil.isEmpty(userId)) {
return;
}
String lockKey = LOCK_PREFIX + "user:" + userId;
if (StrUtil.isEmpty(cacheService.retrieve(lockKey))) {
cacheService.store(lockKey, userId, 5);
return;
}
throw new RuntimeException("User action in progress");
}
private void releaseUserLock(HttpServletRequest request) {
String userId = request.getHeader("X-User-Id");
if (StrUtil.isEmpty(userId)) {
return;
}
String lockKey = LOCK_PREFIX + "user:" + userId;
cacheService.fetchAndRemove(lockKey);
}
private void applySingletonLock(HandlerMethod method) {
String lockKey = LOCK_PREFIX + "singleton:"
+ method.getMethod().getDeclaringClass().getName()
+ ":" + method.getMethod().getName();
if (StrUtil.isEmpty(cacheService.retrieve(lockKey))) {
cacheService.store(lockKey, "locked", 1);
return;
}
throw new RuntimeException("Method currently executing");
}
private void releaseSingletonLock(HandlerMethod method) {
String lockKey = LOCK_PREFIX + "singleton:"
+ method.getMethod().getDeclaringClass().getName()
+ ":" + method.getMethod().getName();
cacheService.fetchAndRemove(lockKey);
}
}
Exception Handler
package com.example.distributed.exception;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
@ResponseBody
public String handleException(Exception e) {
e.printStackTrace();
return e.getMessage();
}
}
Controller Usage
Apply locks to controller methods using annotations:
package com.example.distributed.controller;
import com.example.distributed.annotation.DistributedLock;
import com.example.distributed.annotation.LockScope;
import com.example.distributed.service.CacheService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api")
public class ResourceController {
@Autowired
private CacheService cacheService;
@DistributedLock(scope = LockScope.BY_CLIENT_IP)
@GetMapping("/process/{id}")
public String processRequest(@PathVariable String id) throws InterruptedException {
Thread.sleep(5000);
return "Completed: " + id;
}
@DistributedLock(scope = LockScope.BY_CONCURRENT_ACCESS)
@GetMapping("/heavy-operation")
public String heavyOperation() throws InterruptedException {
Thread.sleep(10000);
return "Heavy operation done";
}
@DistributedLock(scope = LockScope.BY_SINGLE_INSTANCE)
@PostMapping("/batch-update")
public String batchUpdate() {
return "Batch update executed";
}
}
Lock Types
| Lock Type | Use Case |
|---|---|
| BY_CLIENT_IP | Prevant same IP from making rapid repeated requests |
| BY_USER_ID | Prevent same user from triggering duplicate operations |
| BY_CONCURRENT_ACCESS | Limit total concurrent requests to a method |
| BY_SINGLE_INSTANCE | Ensure only one request executes a method at a time |