코드는 이곳에 !
https://github.com/progress0407/jvm-tools/tree/main/redis
과거에 맡았던 도메인 중에 동일 요청건에 대한 중복 결제 요청을 막아야할 일이 있었다 (자세히 기술할 수 없다!)
그때 사용했던 방식이다
과거에 최범균님의 DDD에서 본 방법인데, 이름은 오프라인 선점 잠금으로 알고 있다
마침 사내에서 Redis를 사용했고 락을 짧은 시간 동안 유지 + 분산 환경임을 고려하여 Redis를 사용했다
세간에 알려진 오프라인 선점 락과는 다소 다른 것은... 시간 순서에 따른 락을 선점 -> 해제는 아니었다
즉 아래와 같은 상황이 아니었다
API 1 접근: Lock 획득
API2 접근: Lock 방출
정확히는 아래와 같은 상황이다
API 1에 동시 접근 가능성이 존재하고API 2에서 또한 API 1을 수행하는 동안 접근이 가능
문제 사항
짧은 시간 동안(* ms ~ 5sec) 같은 결제건에 대한 중복 요청이 온다
기존의 최초로 먼저 온 요청건은 먼저 처리한다
해당 요청은 한 API만으로 오지 않을 수 있다 -> 즉 @Tx 하나만 Thread-Safe 하게 처리하는 방법으로는 승부볼 수 없다
Core 코드
아래와 같이 LockManager를 구현해서 처리할 수 있다
Lock을 관리하는 핵심 코드
Lock을 구현하는 방식이 JdbcTemplate, RDBMS JPA, Redis 등 다양하게 있을 수 있기에 인터페이스를 추출했다
/**
* Lock을 관리하는 역할
*/
interface LockManager {
/**
* Lock 선점
*/
fun preemptLock(lockId: String)
/**
* Lock 보유 확인
*/
fun checkLock(lockId: String): Boolean
/**
* 락 방출
*/
fun releaseLock(lockId: String)
/**
* 기존 락 보유시간 연장
*/
fun extendLockExpiration(lockId: String, extendSeconds: Int)
}
@Component
class RedisLockManager(private val repository: LockRepository) : LockManager {
override fun preemptLock(lockId: String) {
if (checkLock(lockId)) throw LockException("Lock이 이미 존재합니다.")
val lock = Lock(lockId, 10)
repository.save(lock)
}
override fun checkLock(lockId: String): Boolean {
return repository.existsById(lockId)
}
override fun releaseLock(lockId: String) {
if (checkLock(lockId).not()) throw LockException("Lock이 존재하지 않습니다.")
repository.deleteById(lockId)
}
override fun extendLockExpiration(lockId: String, extendSeconds: Int) {
val lock = repository.findByIdOrNull(lockId) ?: throw LockException("Lock이 존재하지 않습니다.")
lock.update(extendSeconds)
}
}
Repository
Lock을 다루는 Repository (다음 포스팅에서 다룰 예정)
interface LockRepository : ExtendedCrudRepository<Lock, String> {
}
interface ExtendedCrudRepository<T, ID>: CrudRepository<T, ID> {
fun findByIdOrNull(id: ID): T? {
val optionalEntity: Optional<T> = findById(id)
return optionalEntity.orElse(null)
}
}
Entity
@RedisHash(value = "lock")
class Lock(
@Id
private val paymentTxId: String,
@TimeToLive(unit = SECONDS)
private var ttl: Int
) {
fun update(ttl: Int): Lock {
this.ttl = ttl
return this
}
override fun toString(): String {
return "Lock(paymentTxId='$paymentTxId', ttl=$ttl)"
}
}
Service
@Service
class LockService(private val lockManager: LockManager) {
@Transactional
fun doPaymentProcess(txId: String): Any {
if(lockManager.checkLock(txId)) {
return "이미 결제 처리 중입니다."
}
lockManager.preemptLock(txId)
Thread.sleep(6_000)
lockManager.releaseLock(txId)
return "결제 처리 완료 및 락 해제"
}
}
Controller
락으로 보호해야하는 API는 2개라고 가정하였다
@RestController
@RequestMapping("/test/redis")
class LockController(private val lockService: LockService) {
@RequestMapping("/proceed-payment/{txId}")
fun doSomething(@PathVariable txId: String): Any {
return lockService.doPaymentProcess(txId)
}
@RequestMapping("/proceed-payment-2/{txId}")
fun doSomething2(@PathVariable txId: String): Any {
return lockService.doPaymentProcess(txId)
}
}
'Back-end > 기타 (BE)' 카테고리의 다른 글
엑셀 VBA 자동화 기록 (4) | 2023.12.20 |
---|---|
@Transactional 어노테이션 넌 뭐야? (0) | 2023.08.31 |
스프링 배치 getting start (0) | 2021.03.25 |
뉴렉쳐 Servlet & JSP [61 ~ 70강] (0) | 2020.11.01 |
뉴렉쳐 Servlet & JSP [51 ~ 60강] (0) | 2020.10.07 |
hi hello... World >< 가장 아름다운 하나의 해답이 존재한다
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!