Database Lock은 Lock 획득을 위해 대기하는 connection을 증가시켜서 부하를 준다.
Java의 synchronized
나 ReentrantLock
을 사용하여 애플리케이션 수준에서의 동시성 제어도 가능하겠지만, 우리는 트랜잭션 관리나 분산 락 구현을 통해 해결할 것이다.
데이터베이스의 Lock 전략
1. 낙관적 잠금 : 충돌이 감지되면 그때 처리하자!
JPA의 @Version
어노테이션을 사용하여 엔티티의 버전을 관리하고, 업데이트 시 버전을 체크하여 충돌을 방지할 수 있다
- 충돌 안나면 처리 성능이 좋지만, 충돌이 빈번한 경우 오히려 비용이 크다.
- 다른 트랜잭션의 접근을 제어하지 않기에 동시성이 높지만 충돌을 방지하지 못한다.
- 충돌시 롤백 처리를 구현해야한다
Table의 @Version
이나 Timetable
을 통해 데이터의 상태 구분을 이용한다.
2. 비관적 잠금(Pessimistic Lock): 미리 Lock 걸어서 방지하자!
선점 잠금. JPA의 @Lock
어노테이션을 사용
LockMode 종류
- 배타적 잠금(xlock) :
LockModeType.PESSIMISTIC_WRITE
- 데이터베이스에 대한 쓰기 잠금(defualt)
- 다른 트랜잭션에서 읽기도 쓰기도 못함
- 공유 잠금(slock) :
LockModeType.PESSIMISTIC_READ
- 반복 읽기만하고 수정하지 않는 용도로 락을 걸 때 사용
- 다른 트랜잭션에서 읽기는 가능함
LockModeType.PESSINISTIC\_FORCE_INCREMENT
- Version 정보를 사용하는 비관적 락
비관적 락 - 공유락/ 베타락 시도
비관적 락의 종류로 lock을 걸어서 트랜잭션에서 선점한 리소스에 대해 접근을 제한시킨다.
- 공유 락: 리소스의 쓰기 작업 제한
- 베타락 : 리소스의 읽기, 쓰기 작업을 모두 제한
Repository에서 메소드에 다음과 같이 처리한다.
public Item findOne(Long id, LockModeType lock){
return em.find(item.class, id ,lock);
}
= 리소스 제가 쓰고 있으니 아무도 읽거나 쓰지 마세요
격리 수준 설정과 락 잠금시 데드락(DeadLock)에 유의해야한다.
환형 대기를 없애기 위해 모든 트랜잭션이 얻는 락의 순서를 항상 유지해야한다.
@Lock(LockModeType.PESSIMISTIC_WRITE)으로 비관적 락 구현하기
public interface HomeRepository extends JpaRepository<Home, Long> {
Home findByName(String name);
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("select h from Home h where h.name = :name")
Home findWithNameForUpdate(@Param("name") String name);
}
함수 실행시 쿼리 로그를 찍어보면 SELECT FOR ~ UPDATE
쿼리문을 볼 수 있다
동시성 제어를 위해 내부적으로 특정 row에 대해 배타적 Lock을 거는 셈이다.
= 데이터 수정하려고 찾은거니 다른분들은 건들지 마셔요
임계 구역의 상호 배제를 보장하기 위해 Lock을 사용한다.
Java의 synchronized
처럼 언어에서 제공하는 Lock 이외에도, 여러 서버에서 임계 구역을 접근하는 경우에는 분산 락(Distributed Lock) 기법이 필요하다.
일반적인 Distributed Lock의 구현 방법
- Redis
- SETNXEX 명령어
- SETNX는 key가 존재하면 SET이 실패하고, key가 없으면 SET이 성공한다.
- 이 과정에서 원자성(Atomic)이 보장된다.
- SET의 성공 여부를 Lock 획득 성공 여부로 간주하면 Distributed Lock을 간단하게 구현할 수 있다.
- Redisson
Redisson
에서 제공하는 Lock과 성격이 다르지만,Lettuce
에도 Lock을 제공한다.setnx
메서드를 이용해 사용자가 직접 스핀락 형태로 구성한다.- 락이 점유 시도를 실패한 경우, 계속해서 시도하기에 redis는 계속 부하를 받아 응답시간이 지연되는 단점이 있다
- SETNXEX 명령어
- SQL DB Lock
SELECT ~ FOR UPDATE
등의 row lock이나USER-LEVEL Lock
등을 활용하는 방법
Redis의 Distributed Lock
싱글쓰레드로 작동하는 Redis의 특성을 이용해서 동시성 제어를 처리하는 방법론도 있다.
Java에서 구현된 Redisson 라이브러리를 통해 Distributed Lock을 지원한다.
https://redisson.org/docs/data-and-services/locks-and-synchronizers/
Distributed locks and synchronizers - Redisson Reference Guide
Distributed locks and synchronizers Lock Redis or Valkey based distributed reentrant Lock object for Java and implements Lock interface. Uses pub/sub channel to notify other threads across all Redisson instances waiting to acquire a lock. If Redisson insta
redisson.org
build.gradle에 Redisson 의존성 주입
dependencies {
...
//Redisson
implementation 'org.redisson:redisson:3.17.5'
}
RedissonConfig.java
@Configuration
@RequiredArgsConstructor
public class RedissonConfig {
@Value("${redis.host}")
private String host;
@Value("${redis.port}")
private int port;
@Bean
public RedissonClient redissonClient(){
Config config = new Config();
config.useSingleServer()
.setAddress("redis://"+ host+":"+ port);
return Redisson.create(config);
}
}
그냥 RedissonClient
빈 생성해서 redis에 bind하는게 다임
비즈니스 로직
final String lockName = key + ":lock";
final RLock lock = redissonClient.getLock(lockName);
try{
if(!lock.tryLock(1,3, TimeUnit.SECONDS))
return;
logic
} catch (InterruptedException e){
e.printStackTrace();
} finally{
if(lock!=null && lock.isLocked()) {
lock.unlock();
}
}
tryLock : param2 시간 동안 Lock 점유를 시도한다.
- param1 시간까지 락을 사용할 수 있을지 대기한다.
- param2 시간이 지나면 자동으로 Lock을 해제한다.
이때 선행 쓰레드가 존재하면 waitTime(param2) 동안 Lock 점유를 기다린다.
leaseTime(param1) 시간 이후로 Lock 이 해제되기 때문에 다른 쓰레드도 일정 시간이 지나면 Lock을 점유할 수 있다.
https://github.com/downfa11/kafka-concurrency/issues/1
분산 락(Distributed Lock) 구현 · Issue #1 · downfa11/kafka-concurrency
우리는 비즈니스 로직의 임계 구역의 상호 배제를 보장하기 위해 Lock을 사용한다. Java의 synchronized처럼 언어에서 제공하는 Lock 이외에도, 여러 서버에서 임계 구역을 접근하는 경우에는 분산 락
github.com
'backend' 카테고리의 다른 글
Axon와 Kafka는 어떻게 다른가? (0) | 2024.11.23 |
---|---|
토이프로젝트 - 트랜잭션 관리를 통한 베타락(Exclusive Lock) 구현 (0) | 2024.11.22 |
HTTP multipart/form-data 파일 업로드 문제 해결 (0) | 2024.11.21 |
Hexagonal 아키텍처와 MVC 패턴 비교 (0) | 2024.11.21 |
Spring Cloud를 뜯어보자 (Gateway, Config, Netflix Eureka) (0) | 2024.11.19 |