- redis 분산락(Distributed lock)을 이용하여 분산환경으로부터 공유된 자원을 보호하기 위한 예제입니다.
- 예제는 게시판 서비스에서 게시글의 좋아요 count에 대한 정합성을 보장합니다.
- Spring Integration 의 RedisLockRegistry의 분산 락을 Standard 하게 사용할 수 있게끔 구현했습니다.
- docker로 redis 실행
docker run --name redis -p 6379:6379 -d redis
- BoardLikeTest.kt를 통해 게시글의 좋아요 count 정합성 테스트를 할 수 있습니다.
/**
* RedisLockerService 클래스는 RedisLocker를 생성하는 컴포넌트
*
* @property redisLockRegistry [RedisLockRegistry] 인스턴스로, 락(lock)을 관리
*/
@Component
class RedisLockerComponent(
private val redisLockRegistry: RedisLockRegistry,
) {
fun createLocker(registryKey: String, defaultObtainLockWaitingTimeSec: Int = 5) =
RedisLocker(registryKey, defaultObtainLockWaitingTimeSec, redisLockRegistry)
}
/**
* RedisLocker 클래스는 RedisLockRegistry를 사용하여 동시성 제어를 위한 락(lock)
*
* @property registryKeyPrefix Redis 레지스트리의 키(key)에 접두사(prefix)로 사용될 문자열
* @property defaultObtainWaitingTimeSec 락 획득을 시도할 때 기본 대기 시간(초). default 5초
* @property lockRegistry 락(lock)을 관리하는 RedisLockRegistry
*/
class RedisLocker(
private val registryKeyPrefix: String,
private val defaultObtainWaitingTimeSec: Int = 5,
private val lockRegistry: RedisLockRegistry,
) {
private val logger = KotlinLogging.logger { }
/**
* 지정된 키(key)에 대한 락(lock)을 획득하고, 주어진 람다 함수를 실행
*
* @param key 락(lock)을 획득할 키(key)로 사용될 문자열
* @param obtainWaitingTimeSec 락을 획득하기 위해 대기할 최대 시간(초). default [defaultObtainWaitingTimeSec]
* @param runnable 락(lock)을 획득한 후 실행할 람다 함수. 실행 결과를 반환.
* @return 람다 함수의 실행 결과를 반환. 락 획득에 실패한 경우 null을 반환.
*/
fun <T> lock(
key: String,
obtainWaitingTimeSec: Int = defaultObtainWaitingTimeSec,
runnable: () -> T,
): T? {
var t: T? = null
val lock = lockRegistry.obtain("$registryKeyPrefix:$key")
if (lock.tryLock(obtainWaitingTimeSec.toLong(), TimeUnit.SECONDS)) {
try {
logger.trace { "LockStart - $registryKeyPrefix:$key" }
t = runnable()
logger.trace { "LockEnd - $registryKeyPrefix:$key" }
} finally {
lock.unlock()
}
} else {
logger.trace { "LockFailed - $registryKeyPrefix:$key" }
}
return t
}
}
RedisLockRegistryConfig.kt
@Configuration
class RedisLockRegistryConfig(
private val redisConnectionFactory: RedisConnectionFactory
) {
companion object {
const val REGISTRY_PREFIX = "example:lock"
const val REDIS_KEY_TTL_SEC = 60 // 장애 복구 시간을 고려한 시간으로 설정해야함.
}
@Bean
fun redisLockRegistry() = RedisLockRegistry(
redisConnectionFactory,
REGISTRY_PREFIX,
REDIS_KEY_TTL_SEC * 1000L
).also {
it.setRedisLockType(RedisLockRegistry.RedisLockType.PUB_SUB_LOCK) // spring docs 에 따라 PUB_SUB_LOCK 으로 설정
}
}
- key가 만료되었다는 것은 대부분 redis 장애와 연관되므로 장애 유연성에 따라
REDIS_KEY_TTL_SEC
의 시간을 설정해야함. - master/replica connection은 pub/sub방식의
RedisLockType.PUB_SUB_LOCK
을 원활히 지원하지 않으므로, 상황에 따라 RedisLockType을 설정해야함. (Spring Doc 참고)