@Component public class RedisLockRepository { private RedisTemplate<String, String> redisTemplate; public RedisLockRepository(RedisTemplate<String, String> redisTemplate) { this.redisTemplate = redisTemplate; } //redis 명령어 실행 위한 템플릿 di public boolean lock(Long key){ return redisTemplate .opsForValue() .setIfAbsent(generateKey(key), "lock", Duration.ofMillis(3_000)); } //key 에 사용할 변수를 받아야 하기 때문에 key 매개변수 추가 public boolean unLock(Long key){ return redisTemplate.delete(generateKey(key)); } //로직 실행전에 key 와 setnx 명령어를 활용해서 lock을 하고 unlock 메시지를 통해 lock을 해제하는 방식 private String generateKey(Long key){ return key.toString(); } }
- redis 명령어를 이용할수 있어야 하기 때문에 레디스 레파지토리 생성
@Component public class LettuceLockStockFacade { private final RedisLockRepository redisLockRepository; private final StockService stockService; public LettuceLockStockFacade(RedisLockRepository redisLockRepository, StockService stockService) { this.redisLockRepository = redisLockRepository; this.stockService = stockService; } public void decrease(Long id, Long quantity) throws InterruptedException { while(!redisLockRepository.lock(id)){ Thread.sleep(100); } try{ stockService.decrease(id, quantity); }finally { redisLockRepository.unLock(id); } } }
- while문 활용해서 lock 획득 시도 lock 획득에 실패 하였다면 100밀리 세컨즈 텀을 두고 lock 재시도, redis 부하 줄이기
- lock 획득에 성공하였다면서 stockservice활용해서 재고 감소, 로직 종료시 unlock메서드로 lock 해제
장점 : lettuce를 활용한 방법은 구현이 간단
단점 : spin lock 방식으로는 redis 부하를 줄수 있어서 Thread.sleep()을 통해 재시도간에 텀을 둬야 한다.
redisson을 사용하기 위해 의존성 추가
pub/sub 실습을 위해 두개의 cli가 필요하다
1. 첫번째 터미널에서 subscribe ch1 명령어 통해 ch1 이라는 채널 구독
2. 두번째 터미널은 publish ch1 hello 명령어를 통해 hello 라는 메시지를 보내준다.
3. ch1 채널을 구독하는 터미널에서 hello 메시지를 받는다.
4. redisson 은 자신이 점유하고 있는 lock을 해제할때 채널에 메시지를 보내줌으로 lock 획득을 해야하는 스레드들에게 lock 획득을 하라고 전달. 메시지를 받은 스레드는 lock 획득을 시도.
5. lettuce는 계속 lock 획득을 시도하는 반면, redisson은 lock 해제가 되었을때 한번 혹은 몇번한 시도하고 pub/sub 형식이기에 redis의 부하를 줄여준다.
redisson 같은 경우엔 lock 관련 된 클래스를 라이브러리에서 제공해주므로 별도의 Repository는 작성할 필요 없지만 로직 실행 된 전 후로 lock 획득 해제는 해주어야 하므로 facade 클래스는 필요
@Component public class RedissonLockStockFacade { private RedissonClient redissonClient; private StockService stockService; public RedissonLockStockFacade(RedissonClient redissonClient, StockService stockService) { this.redissonClient = redissonClient; this.stockService = stockService; } public void decrease(Long id, Long quantity){ RLock lock = redissonClient.getLock(id.toString()); //lock 객체 가져오기 try{ boolean available = lock.tryLock(15, 1, TimeUnit.SECONDS); //몇초동안 lock 획득 시도할것인지, 점유할것인지 설정 if(!available){ System.out.println("lock 획득 실패"); return; } stockService.decrease(id, quantity); } catch (InterruptedException e) { throw new RuntimeException(e); } finally { //로직 정상 종료 시 lock.unlock(); } }
pub-sub 기반 구현이기에 redis의 부담을 줄여주지만, 구현이 조금 복잡하거나 별도의 라이브러리를 사용해야 하는 부담감이 있다.
정리
실무에서는 재시도가 필요하지 않은 lock 경우에는 lettuce를 활용
재시도가 필요한 경우에는 redisson 활용재시도가 필요하지 않은 예시 :
유저 1명당 로지텍의 A 상품을 1개밖에 구매할 수 없다고 가정해보겠습니다.
이 상황에서 userid_로지텍A상품을 key 로 lock 을 잡은상태에서 동일한 요청이 들부어온다면 락을 기다릴 필요가 없을겁니다.
먼저 들어온 요청이 처리가 된다면 이후의 요청이 lock 을 잡더라도 할 일이 없기때문입니다.
이러한 상황이 재시도가 필요가 없는 상황입니다.
서버가 2대가 있다고 가정해보겠습니다.
그렇다면 서버1 과 서버2에 각각 Redis 가 있는것이 아니라 서버 1과 서버 2가 Redis 가 있는 서버 3을 바라보는 것 입니다.
분산락이란 여러서버에서 공유된 데이터를 제어하기 위해 사용하는 기술이고 여러서버에서 레디스를 사용하여 공유된 데이터를 제어하고있기에 분산락이라고 표현하는 것입니다.
Mysql 과 비교해선 활용중인 Redis가 없다면 별도의 구축 비용과 인프라 관리비용이 필요
Mysql보다 성능이 좋다