object OrderNumberGenerator {
fun generateOrderNo(): String {
val timestamp = Instant.now().epochSecond.toString().substring(1, 10)
val microsecond = LocalDateTime.now().format(DateTimeFormatter.ofPattern("SSSSSS")).toString()
return timestamp + microsecond[2] + "-" + microsecond.substring(3)
}
}
Dependency
implementation ("org.redisson:redisson-spring-boot-starter:3.23.2")
Config
@Bean
fun redissonClient(): RedissonClient? {
var redisson: RedissonClient? = null
val config = Config()
config.useSingleServer().setAddress("$REDISSON_HOST_PREFIX$host:$port")
logger.info(config.useSingleServer().address)
redisson = Redisson.create(config)
return redisson
}
DistributedLockAspect
@Aspect
@Component
class DistributedLockAspect(
private val redissonClient: RedissonClient,
private val aopForTransaction: AopForTransaction,
): Log {
companion object {
private const val REDISSON_KEY_PREFIX = "RLOCK_"
}
@Around("@annotation(kr.co.annotation.DistributedLock)")
fun lock(joinPoint: ProceedingJoinPoint): Any {
val signature = joinPoint.signature as MethodSignature
val method = signature.method
val distributedLock = method.getAnnotation(DistributedLock::class.java)
val key: String = REDISSON_KEY_PREFIX + getDynamicValue(
signature.parameterNames,
joinPoint.args,
distributedLock.key
)
val rLock = redissonClient.getLock(key) // (1)
try {
val available = rLock.tryLock(
distributedLock.waitTime,
distributedLock.leaseTime,
distributedLock.timeUnit
) // (2)
if (!available) {
logger.info("get lock failure {}", key)
return false
}
logger.info("get lock success {}", key)
return aopForTransaction.proceed(joinPoint) // (3)
} catch (e: Exception) {
Thread.currentThread().interrupt()
throw InterruptedException()
} finally {
rLock.unlock() // (4)
}
}
}
1) 락의 이름으로 RLock 인스턴스를 가져온다.
2) 정의된 waitTime까지 획득을 시도한다, 정의된 leaseTime이 지나면 잠금을 해제한다.
3) DistributedLock 어노테이션이 선언된 메서드를 별도의 트랜잭션으로 실행한다.
4) 종료 시 무조건 락을 해제한다.
DistributedLock Annotation
/**
* Redisson Distributed Lock annotation
*/
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class DistributedLock(
/**
* 락의 이름
*/
val key: String,
/**
* 락의 시간 단위
*/
val timeUnit: TimeUnit = TimeUnit.SECONDS,
/**
* 락을 기다리는 시간 (default - 5s)
* 락 획득을 위해 waitTime 만큼 대기한다
*/
val waitTime: Long = 1L,
/**
* 락 임대 시간 (default - 3s)
* 락을 획득한 이후 leaseTime 이 지나면 락을 해제한다
*/
val leaseTime: Long = 3L,
)
CustomSpringELParser
/**
* Spring Expression Language Parser
*/
object CustomSpringELParser {
fun getDynamicValue(parameterNames: Array<String>, args: Array<Any>, key: String): String? {
val parser: ExpressionParser = SpelExpressionParser()
val context = StandardEvaluationContext()
for (i in parameterNames.indices) {
context.setVariable(parameterNames[i], args[i])
}
return parser.parseExpression(key).getValue(context, String::class.java)
}
}
AopForTransaction
@Component
class AopForTransaction {
@Transactional(propagation = Propagation.REQUIRES_NEW)
fun proceed(joinPoint: ProceedingJoinPoint): Any {
return joinPoint.proceed()
}
}
@DistributedLock
이 선언된 메서드는 Propagation.REQUIRES_NEW
옵션을 지정해 부모 트랜잭션의 유무에 관계없이 별도의 트랜잭션으로 동작하게끔 설정했습니다.락이 획득 됨 -> 트랜잭션 실행 -> 트랜잭션 커밋 -> 락 해제
순으로 동작하게 구현했습니다.@SpringBootTest
class RedissonLockServiceTest(
val testRedissonLockService: TestRedissonLockService,
): BehaviorSpec({
afterContainer {
clearAllMocks()
}
Given("스레드가 주어지고") {
val numberOfThreads = 20
val executorService = Executors.newFixedThreadPool(numberOfThreads)
val orderNo = generateOrderNo()
When("20명이 동시에 락을 얻으려고 요청한다.") {
for (i in 0 until numberOfThreads) {
executorService.submit {
testRedissonLockService.testLock(orderNo)
}
}
}
})
@Service
class TestRedissonLockService(
): Log {
@DistributedLock(key = "#orderNo", waitTime = 0L, leaseTime = 1L)
fun testLock(orderNo: String) {
logger.info(orderNo)
}
}
output
2023-08-03 15:05:48.825 INFO 3952 --- [ool-4-thread-15] kr.co.aop.DistributedLockAspect : get lock success RLOCK_6910427482-971
2023-08-03 15:05:48.826 INFO 3952 --- [pool-4-thread-1] kr.co.aop.DistributedLockAspect : get lock failure RLOCK_6910427482-971
2023-08-03 15:05:48.826 INFO 3952 --- [ool-4-thread-19] kr.co.aop.DistributedLockAspect : get lock failure RLOCK_6910427482-971
2023-08-03 15:05:48.826 INFO 3952 --- [pool-4-thread-3] kr.co.aop.DistributedLockAspect : get lock failure RLOCK_6910427482-971
2023-08-03 15:05:48.826 INFO 3952 --- [pool-4-thread-2] kr.co.aop.DistributedLockAspect : get lock failure RLOCK_6910427482-971
2023-08-03 15:05:48.827 INFO 3952 --- [ool-4-thread-14] kr.co.aop.DistributedLockAspect : get lock failure RLOCK_6910427482-971
2023-08-03 15:05:48.827 INFO 3952 --- [ool-4-thread-18] kr.co.aop.DistributedLockAspect : get lock failure RLOCK_6910427482-971
2023-08-03 15:05:48.826 INFO 3952 --- [pool-4-thread-8] kr.co.aop.DistributedLockAspect : get lock failure RLOCK_6910427482-971
2023-08-03 15:05:48.827 INFO 3952 --- [pool-4-thread-6] kr.co.aop.DistributedLockAspect : get lock failure RLOCK_6910427482-971
2023-08-03 15:05:48.828 INFO 3952 --- [pool-4-thread-5] kr.co.aop.DistributedLockAspect : get lock failure RLOCK_6910427482-971
2023-08-03 15:05:48.827 INFO 3952 --- [ool-4-thread-16] kr.co.aop.DistributedLockAspect : get lock failure RLOCK_6910427482-971
2023-08-03 15:05:48.829 INFO 3952 --- [ool-4-thread-12] kr.co.aop.DistributedLockAspect : get lock failure RLOCK_6910427482-971
2023-08-03 15:05:48.828 INFO 3952 --- [ool-4-thread-17] kr.co.aop.DistributedLockAspect : get lock failure RLOCK_6910427482-971
2023-08-03 15:05:48.829 INFO 3952 --- [ool-4-thread-11] kr.co.aop.DistributedLockAspect : get lock failure RLOCK_6910427482-971
2023-08-03 15:05:48.830 INFO 3952 --- [pool-4-thread-9] kr.co.aop.DistributedLockAspect : get lock failure RLOCK_6910427482-971
2023-08-03 15:05:48.830 INFO 3952 --- [pool-4-thread-7] kr.co.aop.DistributedLockAspect : get lock failure RLOCK_6910427482-971
2023-08-03 15:05:48.831 INFO 3952 --- [ool-4-thread-20] kr.co.aop.DistributedLockAspect : get lock failure RLOCK_6910427482-971
2023-08-03 15:05:48.832 INFO 3952 --- [ool-4-thread-13] kr.co.aop.DistributedLockAspect : get lock failure RLOCK_6910427482-971
2023-08-03 15:05:48.832 INFO 3952 --- [ool-4-thread-10] kr.co.aop.DistributedLockAspect : get lock failure RLOCK_6910427482-971
2023-08-03 15:05:48.834 INFO 3952 --- [pool-4-thread-4] kr.co.aop.DistributedLockAspect : get lock failure RLOCK_6910427482-971
2023-08-03 15:05:48.856 INFO 3952 --- [ool-4-thread-15] k.c.m.l.service.TestRedissonLockService : 6910427482-971
[pool-4-thread-15]
가 락을 얻었다고 로그가 찍히고 나머지 스레드들은 락을 못 얻어서 실패 로그가 찍힙니다.[pool-4-thread-15]
주문번호를 INFO 레벨의 로그를 찍습니다.