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);
}