728x90
반응형
source 는 Github 에 있습니다.
목차는 상품 주문 에 있습니다.
[상품 주문 3편] Redis 설명
Webflux, 코틀린, Redis, 분산락 공부를 위해 작성했습니다.
Redis 분산락 로직 설명
- 메소드를 실행 하기 전에 lock 을 획득하고, 메소드를 실행합니다.
- 메소드 실행이 끝나면 lock release 가 이루어집니다.
로직 개선 사항
- jointPoint.proceed 부분이 비즈니스 로직을 실행하는 부분입니다. 해당 부분은 별도 트랜잭션으로 분리를 해야하는데, 비동기 + kotlin 환경에서 트랜잭션을 분리하는 방법을 찾지 못해 숙제로 남겨놨습니다.
- 트랜잭션을 별도로 분리해야하는 이유는 lock 을 거는 부분이 비즈니스 로직과 하나의 트랜잭션으로 묶여서 처리되면 안되기 때문입니다. lock 을 거는 부분에서 에러가 발생해도 비즈니스 로직의 트랜잭션에 영향을 줘선 안됩니다.
@Aspect
@Component
class DistributedLockAspect(private val distributedLockService: DistributedLockService) {
private val logger = LoggerFactory.getLogger(this.javaClass)
@Around("@annotation(DistributedLock)")
fun lock(joinPoint: ProceedingJoinPoint): Mono<Any?> {
var lockKey = getLockKey(joinPoint.args)
return distributedLockService.acquireLock(lockKey)
.flatMap { lockAcquired ->
if (lockAcquired) {
logger.debug("Lock acquired: $lockKey")
try {
joinPoint.proceed() as Mono<Any?>
} finally {
distributedLockService.releaseLock(lockKey).subscribe {
logger.debug("Lock released: $lockKey")
}
}
} else {
logger.debug("Failed to acquire lock: $lockKey")
Mono.empty<Any>()
}
}
.switchIfEmpty(Mono.error(Exception("락을 획득하지 못했습니다.")))
}
private fun getLockKey(args: Array<Any>): String {
require(args.isNotEmpty()) { "Arguments must not be empty" }
val arg = args[0]
return when (arg) {
is String -> arg
is OrderCommand.CreateOrder -> {
requireNotNull(arg.getProductId()) { "ProductId must not be null" }
"product:${arg.getProductId()}"
}
else -> throw IllegalArgumentException("Unsupported argument type: ${arg.javaClass.name}")
}
}
}
@Component
class DistributedLockService(
private val reactiveRedisTemplate: ReactiveRedisTemplate<String, String>,
) {
companion object {
private const val DEFAULT_TIMEOUT = 5000L
}
fun acquireLock(key: String): Mono<Boolean> {
// SETNX - lock 획득 (Atomic)
// key : 저장할 키, "" : 저장 value, timeout : TTL
return reactiveRedisTemplate.opsForValue()
.setIfAbsent(
key, "", Duration.ofMillis(DEFAULT_TIMEOUT)
)
}
fun releaseLock(key: String): Mono<Long> {
// DEL - 락 해제 (Atomic)
return reactiveRedisTemplate.delete(key)
}
}
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION)
annotation class DistributedLock(val key: String)
redis 분산락 동작 여부 확인 방법
redis console 접속 -> MONITOR 명령어 실행 -> SETNX, DEL 명령어 확인
REDISSON 을 통해 SETNX, DEL 명령어를 호출을 했지만, 모니터링 한 결과 이상한 값이 찍혀있는데요.
- REDISSON 내부에서 lua script 를 이용해서 처리한 결과입니다.
1683549724.618226 [0 172.21.0.1:47212] "EVAL" "if (redis.call('exists', KEYS[1]) == 0) then redis.call('hincrby', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then redis.call('hincrby', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; return redis.call('pttl', KEYS[1]);" "1" "product:product1002" "5000" "1c4eddfe-e10b-4e47-9e08-b510e610ca3c:3020424071110789844"
1683549724.618445 [0 lua] "exists" "product:product1002"
1683549724.618499 [0 lua] "hincrby" "product:product1002" "1c4eddfe-e10b-4e47-9e08-b510e610ca3c:3020424071110789844" "1"
1683549724.618547 [0 lua] "pexpire" "product:product1002" "5000"
1683549724.651154 [0 172.21.0.1:47238] "EVAL" "if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then return nil;end; local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); if (counter > 0) then redis.call('pexpire', KEYS[1], ARGV[2]); return 0; else redis.call('del', KEYS[1]); redis.call('publish', KEYS[2], ARGV[1]); return 1; end; return nil;" "2" "product:product1002" "redisson_lock__channel:{product:product1002}" "0" "30000" "1c4eddfe-e10b-4e47-9e08-b510e610ca3c:3020424071110789844"
1683549724.651322 [0 lua] "hexists" "product:product1002" "1c4eddfe-e10b-4e47-9e08-b510e610ca3c:3020424071110789844"
1683549724.651387 [0 lua] "hincrby" "product:product1002" "1c4eddfe-e10b-4e47-9e08-b510e610ca3c:3020424071110789844" "-1"
1683549724.651408 [0 lua] "del" "product:product1002"
1683549724.651441 [0 lua] "publish" "redisson_lock__channel:{product:product1002}" "0"
Redis client redisson
- redisson 를 선택한 이유는 비동기적으로 지원이 가능하며, Reactive 환경에서 동작 가능합니다.
- Jedis 는 비동기를 지원 안합니다.
- lettuce 는 비동기를 지원하지만 스핀락으로 인한 성능 문제, 기능을 (자료구조, 확장성 등) Redisson 이 더 많이 지원해서 Redisson 을 선택했습니다.
Redisson Reactive 락 주의사항
Redisson 에서 lock, unlock 을 할 때, 같은 ThreadId 를 사용하라고 돼있습니다. Reactive 환경에서 왜 이렇게 해놨는지는 모르겠는데, lock, unlock 할 때, 같은 threadId 를 넣어서 처리하도록 구현했습니다.
Reference
'토이프로젝트 > 상품주문' 카테고리의 다른 글
[상품 주문 4편] 동시성 테스트 (0) | 2023.05.08 |
---|---|
[상품 주문 2편] 비즈니스 로직 API 설명 (0) | 2023.05.08 |
[상품 주문 1편] 개요 (Webflux, 코틀린, Redis, 분산락) (0) | 2023.04.05 |
댓글