이전

Race Condition (동시성이슈 해결방법 with Redis)

Lettuce

  • setnx 명령어 사용
  • spin lock 방식
    • retry 로직 필요
@Component
class RedisLockRepository(
        private val redisTemplate: RedisTemplate<String, String>,
) {
    fun lock(key: Long): Boolean {
        val isSuccess: Boolean? = redisTemplate.opsForValue()
                .setIfAbsent(key.toString(), "lock", Duration.ofSeconds(3L))

        return isSuccess ?: false
    }

    fun unlock(key: Long): Boolean {
        return redisTemplate.delete(key.toString())
    }
}

@Service
class RedisLockStockService(
        private val redisLockRepository: RedisLockRepository,
) {
    fun decrease(id: Long, quantity: Long) {
        while (redisLockRepository.lock(id).not()) {
            TimeUnit.MILLISECONDS.sleep(100L) // 100 milliseconds 동안 sleep 하며 주기적으로 요청
        }

        try {
            ...
            // 재고 감소 로직 수행
        } finally {
            redisLockRepository.unlock(id)
        }
    }
}

Redisson

  • Pub-Sub 기반의 Lock 구현 제공
  • 채널을 만들고 Lock을 획득한 Thread가 Lock획득을 시도하는 Thread에게 해제되었음을 알려주는 방식
@Service
class RedissonLockStockFacade(
        private val stockService: StockService,
        private val redissonClient: RedissonClient,
) {
    private val log: Logger = LoggerFactory.getLogger(this::class.java)

    fun decrease(id: Long, quantity: Long) {
        val lock: RLock = redissonClient.getLock(id.toString())

        try {
            val available: Boolean = lock.tryLock(5L, 1L, TimeUnit.SECONDS)
            if (available.not()) {
                log.error("lock 획득 실패")
                return
            }
            stockService.decrease(id, quantity)
        } finally {
            lock.unlock()
        }
    }
}