20240221 (수) 대용량 트래픽 프로젝트 - 동시성 제어 프로젝트 6일차

2024. 2. 21. 20:22TIL

오늘 공부한 내용

Lettuce클라이언트를 이용한 Lock 구현

 

처음 동시성을 제어하기 위해 @Synchronized 를 이용했다.

우리가 만들어놓은 테스트코드로는 싱크로나이즈드만으로도 해결이 가능했다. 하지만

  • 자바의 Sychronized는 하나의 프로세스 안에서만 보장이 됩니다.
  • 즉, 서버가 1대일때는 문제가 없지만 서버가 2대 이상일 경우 데이터에 대한 접근을 막을 수가 없습니다.

이러한 문제점이 있었다.

그러기에 @ Synchronized 를 사용하기보단 직접 Lock을 구현하여 동시성을 제어하는게 맞다고 판단되었다.

우선 이 Lock 이란것부터 알아보자.

락에는 비관락, 낙관락이 있다.

낙관락은

  • 실제로 Lock 을 이용하지 않고 버전을 이용함으로써 정합성을 맞추는 방법입니다.
  • 먼저 데이터를 읽은 후에 update 를 수행할 떄 현재 내가 읽은 버전이 맞는지 확인하며 업데이트 합니다.
  • 자원에 락을 걸어서 선점하지 않고, 동시성 문제가 발생하면 그때가서 처리하는 낙관적 락 방식입니다.
  • 내가 읽은 버전에서 수정사항이 생겼을 경우에는 application에서 다시 읽은 후에 작업을 수행하는 롤백 작업을 수행해야 합니다.

이런식으로 실제로 Lock을 사용하지않기에 비관락보다는 성능적으로 이점을 가지지만

충돌이 일어났을 시점엔 롤백을 해야하고 이러한 과정이 잦을시에는 비관락이 조금더 유리한 경우가 있다.

비관락은

  • 실제로 데이터에 Lock을 걸어서 정합성을 맞추는 방법입니다.
  • exclusive lock(베타적 잠금) 을 걸게되면 다른 트랜잭션에서는 lock 이 해제되기전에 데이터를 가져갈 수 없게됩니다.
  • 자원 요청에 따른 동시성문제가 발생할 것이라고 예상하고 락을 걸어버리는 비관적 락 방식입니다.
  • 하지만, 데드락이 걸릴 수 있기 때문에 주의하여 사용해야합니다.

우리가 만든 어플리케이션은 티켓팅 어플리케이션이기에 비관락이 조금더 맞을것 같고,

비관락에도 Lettuce, Redisson 등의 방식이 있지만 우선 Lettuce를 이용한 분산락을 구현해보기로 하였다.

Lettuce 를 이용하는 분산락은 spin lock 방식으로만 구현이 가능하고

SETNX(**SET if Not eXist)**등의 명령어를 이용하여 구현한다.

(우리가 구현할때 이용한 코드는 setIfAbsent 이다)

package com.a03.concurrencycontrolproject.common.redis.service

import com.a03.concurrencycontrolproject.common.redis.repository.RedisLockRepository
import com.a03.concurrencycontrolproject.domain.ticket.dto.CreateTicketRequest
import com.a03.concurrencycontrolproject.domain.ticket.service.TicketService
import org.springframework.stereotype.Service

@Service
class RedisServiceImpl(
    private val redisLockRepository: RedisLockRepository,
    private val ticketService: TicketService
): RedisService {

    override fun createTicket(userId: Long, request: CreateTicketRequest) {
        try {
            // Lock 획득 시도
            while (!redisLockRepository.lock(request.goodsId)) {
                //SpinLock 방식이 redis 에게 주는 부하를 줄여주기위한 sleep
                Thread.sleep(100)
            }
            //lock 획득 성공 시
            ticketService.createTicket(userId, request)
        } finally {
            //락 해제
            redisLockRepository.unlock(request.goodsId)
        }
    }
}

여기서 구체적인 로직이 작성된다.

RedisLockRepository객체를 생성해서 lock, unlock 등의 메소드를 이용해 락을 점유, 해제한다.

package com.a03.concurrencycontrolproject.common.redis.repository

import org.springframework.data.redis.core.RedisTemplate
import org.springframework.stereotype.Repository

@Repository
class RedisLockRepository(
    private val redisTemplate: RedisTemplate<String,String>
) {
    fun lock(id: Long): Boolean {
        return redisTemplate
            .opsForValue()
            .setIfAbsent(generateKey(id), "lock", java.time.Duration.ofMillis(3000))!!
    }

    fun unlock(id: Long): Boolean {
        return redisTemplate.delete(generateKey(id));
    }

    private fun generateKey(id: Long): String {
        return id.toString();
    }
}

 


이번에도 이쪽 글의 도움을 많이 받았다.

https://velog.io/@guns95/%EB%8F%99%EC%8B%9C%EC%84%B1-%EB%AC%B8%EC%A0%9C-Lettuce-%EC%82%AC%EC%9A%A9

생각보다 구현이 빠르게 되어서 내일은 Redisson과 시간이 남는다면 MySQL을 이용한 락도 구현해볼 예정이다.