Redis 기반 분산 락 구현 with SpringBoot

1. 의존성 추가

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

2. Redis 연결 설정

spring:
  redis:
    host: 192.168.80.88
    port: 6379
    password:
    database: 5
    timeout: 2000ms
    jedis:
      pool:
        max-idle: 20
        max-wait: 1000ms
        max-active: 100

3. 요청 ID 생성 필터 등록

@Configuration
public class RedisDistributedLockConfig {

    @Bean
    public FilterRegistrationBean requestIdFilter() {
        RequestIdFilter filter = new RequestIdFilter();
        FilterRegistrationBean bean = new FilterRegistrationBean();
        bean.setFilter(filter);
        bean.setUrlPatterns(Collections.singletonList("/*"));
        bean.setOrder(Ordered.HIGHEST_PRECEDENCE + 1);
        return bean;
    }
}

4. 핵심 컴포넌트

4.1 락 어노테이션 정의

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DistributedLockable {
    String key();
    String prefix() default "dislock:";
    long ttl() default 10L; // 기본 만료 10초
}

4.2 AOP 처리기

@Aspect
@Slf4j
@Component
public class DistributedLockAspect {

    private final DistributedLockManager lockManager;

    public DistributedLockAspect(DistributedLockManager lockManager) {
        this.lockManager = lockManager;
    }

    @Around("@annotation(lockable)")
    public Object handleLock(ProceedingJoinPoint joinPoint, DistributedLockable lockable) throws Throwable {
        boolean acquired = false;
        String lockKey = lockable.prefix() + lockable.key();
        String requestId = WebUtil.getRequestId();

        try {
            acquired = lockManager.tryAcquire(lockKey, requestId, lockable.ttl());
            if (!acquired) {
                log.error("Failed to acquire lock for key: {}", lockKey);
                throw new RuntimeException("Unable to acquire distributed lock");
            }
            return joinPoint.proceed();
        } catch (Exception e) {
            throw e;
        } finally {
            if (acquired) {
                boolean released = lockManager.release(lockKey, requestId);
                if (!released) {
                    log.warn("Lock release failed for key: {}, may have been stolen", lockable.key());
                }
            }
        }
    }
}

4.3 Redis 락 관리자

@Component
@Slf4j
public class DistributedLockManager {

    @Autowired
    private StringRedisTemplate redisTemplate;

    private static final String UNLOCK_SCRIPT = 
        "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
    private static final long LOCK_SUCCESS = 1L;
    private final long pollingIntervalMs = 1000L;

    // 기본 락 획득 (즉시 반환)
    public boolean tryAcquire(String key, String value, long ttlSeconds) {
        return Boolean.TRUE.equals(
            redisTemplate.opsForValue().setIfAbsent(key, value, ttlSeconds, TimeUnit.SECONDS)
        );
    }

    // 대기 가능한 락 획득
    public boolean tryAcquireWithWait(String key, String value, long ttlSeconds, long timeout, TimeUnit unit) {
        long deadline = unit.toMillis(timeout);
        long elapsed = 0;

        while (elapsed < deadline) {
            if (Boolean.TRUE.equals(
                redisTemplate.opsForValue().setIfAbsent(key, value, ttlSeconds, TimeUnit.SECONDS))) {
                return true;
            }
            try {
                Thread.sleep(pollingIntervalMs);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                log.error("Lock acquisition interrupted for key: {}", key, e);
                return false;
            }
            elapsed += pollingIntervalMs;
        }

        log.warn("Lock acquisition timed out for key: {}, waited {}ms", key, elapsed);
        return false;
    }

    // 락 해제 (Lua 스크립트 사용)
    public boolean release(String key, String value) {
        DefaultRedisScript<Long> script = new DefaultRedisScript<>(UNLOCK_SCRIPT, Long.class);
        Long result = redisTemplate.execute(script, Collections.singletonList(key), value);
        return LOCK_SUCCESS.equals(result);
    }

    // 일반 캐시 작업
    public void cache(String key, String value) {
        redisTemplate.opsForValue().set(key, value);
    }

    public String getCached(String key) {
        return redisTemplate.opsForValue().get(key);
    }

    public void evict(String key) {
        redisTemplate.delete(key);
    }
}

4.4 요청 ID 유틸리티

public class WebUtil {
    public static final String REQ_ID_HEADER = "X-Request-Id";
    private static final ThreadLocal<String> currentRequestId = new ThreadLocal<>();

    public static void setRequestId(String id) {
        currentRequestId.set(id);
    }

    public static String getRequestId() {
        String id = currentRequestId.get();
        if (id == null) {
            id = generateUid();
            currentRequestId.set(id);
        }
        return id;
    }

    public static void clear() {
        currentRequestId.remove();
    }

    public static String generateUid() {
        return UUID.randomUUID().toString().replace("-", "").toUpperCase();
    }
}

4.5 요청 ID 인터셉터

public class RequestIdFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        String reqId = httpRequest.getHeader(WebUtil.REQ_ID_HEADER);
        if (reqId == null || reqId.isEmpty()) {
            reqId = WebUtil.generateUid();
        }

        WebUtil.setRequestId(reqId);
        try {
            chain.doFilter(request, response);
        } finally {
            WebUtil.clear();
        }
    }
}

5. 사용 예시

@DistributedLockable(key = "order_lock", ttl = 30)
public void processOrder(String orderId) {
    // 비즈니스 로직
    System.out.println("Processing order: " + orderId);
}

태그: SpringBoot Redis Distributed Lock AOP Lua Script

7월 22일 07:25에 게시됨