Redis-Based Distributed Lock and Rate Limiting in Spring Boot Applications

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

Tags: Redis Spring Boot Distributed Lock Rate Limiting Interceptors

Posted on Wed, 13 May 2026 10:33:40 +0000 by SQL Maestro