HomeAboutMeBlogGuest
© 2025 Sejin Cha. All rights reserved.
Built with Next.js, deployed on Vercel
✍🏻
Learnary (learn - diary)
/
Redis의 분산락 Redisson

Redis의 분산락 Redisson

progress
Done
Tags
Database
분산락Redisson VS LettuceRedisson 사용 방법
 

분산락

분산환경에서 독립된 프로세스로 부터 공유자원에 대한 동시성 이슈를 해결하기 위한 기법이다.
 
[RDB의 격리레벨 또는 잠금기법으로 해결 할 수 있는데 하는 이유는?]
RDB 트랜잭션 격리레벨로 해결할 수 있지만 I/O에 대한 성능적 측면 및 데드락 발생위험이 있기 때문이다.
또한 각 잠금 기법 중 Pessimistic Lock은 timeout 구성이 까다로워서 무한 대기 현상에 빠질 위험이 있다. 오히려 NamedLock은 timeout 설정이 간단하다.
[RDB의 Named Lock이 있는데 사용하지 않는 이유는?]
별도의 커넥션 풀을 관리해야 하며 RDB 자체에 대한 성능 측면 또는 상대적인 편리성 차이가 있기 때문이다.
public interface LockRepository extends JpaRepository<Stock, Long>{ @Query(value = "select get_lock(:key, 3000)", nativeQuery = true) void getLock(@Param("key") String key); @Query(value = "select release_lock(:key)", nativeQuery = true) void releaseLock(@Param("key") String key); } ==================== client 부분 ========================= @Component @RequiredArgsConstructor public class NamedLockFacade { private final LockRepository lockRepository; private final StockService stockService; @Transactional public void decreaseWithNamedLock(Long id, Long qty) { try { lockRepository.getLock(id.toString()); stockService.decrease(id, qty); }finally { lockRepository.releaseLock(id.toString()); } } }
[별도의 커넥션 풀을 관리하는 이유는?]
비즈니스 로직을 수행하는 부분에 영향을 줄 수 있기 때문이다.
일반적으로 잠금 기법은 최대한 범위를 좁게 가져가야 성능이 좋기 때문이다.
그래서 비즈니스적 부분에 영향을 최소화하기 위해 보통 락을 얻는 커넥션을 따로 사용한다.
 
 

Redisson VS Lettuce

 
공통점
Non-Blocking I/O를 지원하는 고성능의 분산락 Redis 클라이언트 이다.
차이점
  1. Redisson은 Lecttue와의 핵심적인 차이는 잠금을 얻기 위한 행위가 다르다
Lettuce는 잠금을 얻기위해 Spin-Lock 방식으로 동작하고 Redisson은 pub-sub 기반으로 동작한다.
 
[Redissn 키 획득 과정]
notion image
실생활의 예
SprinLock 방식은 될 때까지 문을 노크하는 방식이다.
notion image
pub-sub은 유투브에있는 구독을 누르게 된 경우 해당 구독된 채널에 실시간 방송, 혹은 영상이 올라오면 알림을 보내주는 방식입니다.
 
전체적으로 보면 Lock을 획득하기 위해 내가 예의주시하느냐 OR 안하느냐 차이 입니다.
 
SpringLock은 Lock을 획득하기 위해 끊임없이 시도하는데 코드 기반으로 보면 While문을 통해 될때까지 시도한다.
Pub-Sub은 Lock을 획득한자가 “나 끗!” 하고 알림을 보내면 다른 자원들이 Lock을 획득하려고 경쟁 한다.
notion image
 
  1. 편리성을 위한 서비스가 다르다.
    1. Redisson 은 별도의 Lock interface를 지원한다.
      락에 대해 타임아웃과 같은 설정을 지원하기에 락을 보다 안전하게 사용할 수 있다.
       
      락이 일부시간동안 너무 오래 걸리게 되면 지정된 시간에 무조건 나오는 시간을 제공하여 무한 점유 상태를 방지한다. 하지만 처리시간이 2초인데 1초로 설정하게 되면 모든 서비스가 장애가 나므로 시간 측정에 대한 평균 및 최대시간으로 설정해야 한다.
      그렇지 않으면 MonitorExcepion이 발생한다.
 
 

Redisson 사용 방법

의존성 추가
dependencies { // redisson implementation 'org.redisson:redisson-spring-boot-starter:3.18.0' }
설정 코드 추가
/* * RedissonClient Configuration */ @Configuration public class RedissonConfig { @Value("${spring.redis.host}") private String redisHost; @Value("${spring.redis.port}") private int redisPort; private static final String REDISSON_HOST_PREFIX = "redis://"; @Bean public RedissonClient redissonClient() { RedissonClient redisson = null; Config config = new Config(); config.useSingleServer().setAddress(REDISSON_HOST_PREFIX + redisHost + ":" + redisPort); redisson = Redisson.create(config); return redisson;
 
락은 얻고 해제하는 반복적인 코드 모듈화를 위한 AOP
/** * Redisson Distributed Lock annotation */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface DistributedLock { /** * 락의 이름 */ String key(); /** * 락의 시간 단위 */ TimeUnit timeUnit() default TimeUnit.SECONDS; /** * 락을 기다리는 시간 (default - 5s) * 락 획득을 위해 waitTime 만큼 대기한다 */ long waitTime() default 5L; /** * 락 임대 시간 (default - 3s) * 락을 획득한 이후 leaseTime 이 지나면 락을 해제한다 */ long leaseTime() default 3L; }
AOP 설정
/** * @DistributedLock 선언 시 수행되는 Aop class */ @Aspect @Component @RequiredArgsConstructor @Sl4j public class DistributedLockAop { private static final String REDISSON_LOCK_PREFIX = "LOCK:"; private final RedissonClient redissonClient; private final AopForTransaction aopForTransaction; @Around("@annotation(com.comus.aop.DistributedLock)") public Object lock(final ProceedingJoinPoint joinPoint) throws Throwable { MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method = signature.getMethod(); DistributedLock distributedLock = method.getAnnotation(DistributedLock.class); String key = REDISSON_LOCK_PREFIX + CustomSpringELParser.getDynamicValue(signature.getParameterNames(), joinPoint.getArgs(), distributedLock.key()); RLock rLock = redissonClient.getLock(key); // (1) try { boolean available = rLock.tryLock(distributedLock.waitTime(), distributedLock.leaseTime(), distributedLock.timeUnit()); // (2) if (!available) { return false; } return aopForTransaction.proceed(joinPoint); // (3) } catch (InterruptedException e) { throw new InterruptedException(); } finally { try { rLock.unlock(); // (4) } catch (IllegalMonitorStateException e) { log.info("Redisson Lock Already UnLock {} {}", kv("serviceName", method.getName()), kv("key", key) ); } } }
SpringSPEL로 동적 커스텀 키 구성
/** * Spring Expression Language Parser */ public class CustomSpringELParser { private CustomSpringELParser() { } public static Object getDynamicValue(String[] parameterNames, Object[] args, String key) { ExpressionParser parser = new SpelExpressionParser(); StandardEvaluationContext context = new StandardEvaluationContext(); for (int i = 0; i < parameterNames.length; i++) { context.setVariable(parameterNames[i], args[i]); } return parser.parseExpression(key).getValue(context, Object.class); } }
[SPEL 파서 적용이유]
잠금 범위는 최소한의 스코프로 관리해야한다. 그렇지않으면 성능이 크게 저하된다. 또한 애플레키이션 전체 멈춤이랑 다를거 없다.
각 API마다 서비스에 사용되어 지고 있는 공유자원도 다르고 서보 독립된 영역에서 처리하여 성능이 좋다.
// (1) @DistributedLock(key = "#lockName") public void shipment(String lockName) { ... } // (2) @DistributedLock(key = "#model.getName().concat('-').concat(#model.getShipmentOrderNumber())") public void shipment(ShipmentModel model) { ... }
 
트랜잭션 분리
/** * AOP에서 트랜잭션 분리를 위한 클래스 */ @Component public class AopForTransaction { @Transactional(propagation = Propagation.REQUIRES_NEW) public Object proceed(final ProceedingJoinPoint joinPoint) throws Throwable { return joinPoint.proceed(); } }
[트랜잭션 전파레벨을 추가한 이유]
트랜잭션 커밋시점 이후에 락이 해제되기 때문이다.
[AOP] - 트랜잭션 전파속성
하나의 락을 거는 트랜잭션(RedisService)과 비즈니스 로직의 트랜잭션(StockService)을 같은 트랜잭션으로 사용할 경우 commit이 되기전에 unlock을 수행할 수 있고, unlock 과 commit 사이의 시점에 또 다른 요청이 들어올 때 커밋이 되기 전이므로 동시성 문제가 발생할 수 있게 된다
 
  • https://www.baeldung.com/redis-redisson
  • https://github.com/redisson/redisson
  • https://techblog.woowahan.com/2631/
  • https://helloworld.kurly.com/blog/distributed-redisson-lock/
  • https://velog.io/@chullll/JPA-락을-이용한-재고-관리