트랜잭션이 동시에 실행됐을 때 발생할 수 있는 문제 관련한 상황을 DB동시성 문제 라고 한다.
1. Dirty Read
- 한 트랜잭션이 다른 트랜잭션이 수정 중인 데이터를 읽을 수 있는 문제
- 아직 commit되지 않은 데이터가 읽힘으로서, 추후 rollback 될 가능성이 있는 데이터 read (실제로는 커밋을 했는데, 반영이 안됐다 라는 뜻
해결방법 : Read Committed 격리성 : 즉 커밋된 내용만 읽겠다는 뜻
2. Non-Repeatable Read .(UPDATE 문제)
- mariaDB의 기본 설정은 Repeatable Read 이다.
- 한 트랜잭션에서 동일한 조회 쿼리를 두 번 이상 실행할때에, 그 중간에 다른 트랜잭션에서 데이터를 수정하여 한 트랜잭션의 결과가 다르게 나타나는 문제 예시) 재고 업데이트 전 현재 재고 조회 -> 수량업데이트 -> 변경된 재고 수량을 다시 조회(한트랜잭션에서). 그러나 만일 이를 중간에 누가 수정을 가하면 재조회시 오차 발생하여 에러.
해결방법
- Repeatable Read 격리성 (격리 레벨을 올린다)-> 한 트랜잭션이 조회하는 동안 다른 트랜잭션이 update를 하더라도 현재 트랜잭션의 read값이 변경되지 않도록 하는 격리성. 그러나, 만약 다른 트랜잭션에서 update를 통해 값을 변경해버렸다면, read한 값 자체에 문제 발생. => 타 트랜잭션의 update를 막기 위한 select for update 쿼리 필요
3. Phantom Read (없던 데이터가 유령처럼 읽힌다. insert 문제)
- 한 트랜잭션이 같은 조회쿼리를 여러 번 실행했을 때, 그 중간에 다른 트랜잭션에서 새로운 데이터를 추가/삭제하여 다르게 나타나는 문제
해결책 :
- Repeatable Read 격리성. 팬텀(유령) 읽기 또한 repeatable Read격리성으로 해결이 가능하나, 이 부분은 DB마다 차이가 있어 phantom read를 해결하기 위해 Serializable 격리수준이 필요할수도 있음. 다만, DB에서 주로 발생하는 문제는 동시에 수정하는 상황이므로, 수정에 초점을 두고 해결 전략을 살펴봐도 좋을것.
- 사실 동시성 문제는 read만이 문제는 아니고, read이후 DB에 어떠한 수정사항을 가할때도 read의 오차로 인한 또다른 오차가 발생하여 DB 전체에 영향이 발생하므로 DB 전체에 대한 동시성이라 보면 될것.
DB 격리 수준 : DB 동시성 문제를 해결하기 위한 격리수준
Read Uncommitted
- 즉, 데이터가 변경되었다면, 커밋되지 않았다 하더라도 읽을 수 있도록 하는 격리수준
- dirty read 발생 가능
Read Committed
- 다른 트랜잭션이 커밋된 데이터만 읽을 수 있는 격리수준.
- 다만, 나의 트랜잭션에 여러 select 문이 있을 경우에, 그 사이에 다른 트랜잭션에서 update 또는 insert 등을 발생시키고 commit하게 될시 phantom read 또는 non-repeatable-read 발생가능
Repeatable Read
- 한 번 읽은 데이터는 같은 트랜잭션 내에서는 항상 같은 값을 갖도록 하는 격리수준 나의 트랜잭션에서 먼저 read하는 동안 다른 트랜잭션에서는 변경,추가 하더라도 같은 read값을 보장하는 것. -> Non-Repeatable Read과 Phantom read를 해결
- 실제로는 바뀌어 있지만, 신경을 안 쓰는 것이다. ⇒문제발생
- repeatable read를 하더라도 두 가지 문제가 발생할 가능성 존재
- 나의 트랜잭션이 read하는 동안 타 트랜잭션에서 update하게 되면 read해온 값이 달라지는 문제 발생
- 그러나, select for update도 두 다른 트랜잭션이 동시에 read하는 것은 가능하여 lost update 문제 가능성 존재 ex)상품주문의 최종 수량이 1개 -> transaction에 read && update가 있을때 -> 내 tran에서 1 read -> 타 트랜잭션이 1 read -> 내 tran에서 0으로 update -> 타 tran에서 0으로 update -> 최종 수량에 오류 발생
Serializable
- 동시에 실행되는 여러 트랜잭션들을 순차적으로 실행한 것과 같은 결과를 보장 -> 즉 데이터베이스 차원에서 동시에 특정 데이터에 접근하는 것을 차단
- 단점 느리거나, 한 사용자만 사용하게 된다.
REPEATABLE READ와 READ COMMITTED의 차이는 언두 영역에 백업된 레코드의 여러 버전 가운데 몇 번째 이전 버전까지 찾아 들어가야 하는지에 있다고 한다.
DB 동시성 관련 해결책 중 레디스를 사용한 분산 락 방법 에 대해 알아보겠다.
분산락 (Distributed lock)
DB의 하나의 공유 자원에 대한 경쟁 상황에서 데이터에 접근할 때, 데이터의 결함이 발생하지 않도록 원자성(atomic)을 보장하는 기법이다.
- 서버가 여러 대인 상황에서 동일한 데이터에 대한 동기화를 보장하기 위해 사용한다.
- 서버들 간 동기화된 처리가 필요하고, 여러 서버에 공통된 락을 적용해야 하기 때문에 redis 를 이용하여 분산락을 이용한다.
- 분산락 같은 경우 공통된 데이터 저장소(DB)를 이용해 자원이 사용중인지 확인하기 때문에 전체 서버에 동기화된 처리가 가능하다.
하나의 DB의 두 명의 Client_A, Client_B 가 요청을 했을때, Client_A 가 락 을 획득하여 소지 중일때는 Client_B 는 요청은 했지만, 락은 획득하지 못하여 락 획득 대기중이다.
이때 Client_A가 락을 반납하면 Client_B 가 락을 가져오는데 이때 pub/sub, 혹은 스핀락 방식으로 락을 획득한다.
한 번 실습을 해보도록 하겠다.
Redisson RLock 라이브러리를 지원한다.
boolean tryLock(long waitTime, long leaseTime, TimeUnit timeUnit) throws InterruptedException;
- waitTime: 락 획득을 위해 기다리는 시간
- leaseTime: 락을 임대하는 시간
- timeUnit: 시간 단위
- waitTime: 잠금을 시도하는 최대 시간. 이 시간 동안 잠금이 가능해질 때까지 기다린다
- leaseTime: 잠금이 성공했을 경우, 잠금이 유지되는 시간. 이 시간이 지나면 잠금이 자동으로 해제된다.
- timeUnit: waitTime과 leaseTime의 시간 단위를 지정. TimeUnit은 나노초, 마이크로초, 밀리초, 초, 분, 시간, 일 등 여러 시간 단위를 지원.
@Bean
public RedissonClient redissonClient(){
log.info("RedissonClient 등록");
Config config = new Config();
config.useSingleServer().setAddress("redis://" + host + ":" + port);
log.info("config : {}", config);
return Redisson.create(config);
}
Client Config 생성 해준다.
서버주소나, 비밀번호, 타임아웃, 서버모드 등을 설정할 수 있다.
Config 객체를 선언하는 주요 이유는 Redisson 클라이언트의 동작을 구성하고 최적화하기 위해서이다. 이를 통해 Redis 서버와의 연결을 설정하고, 성능 및 보안 관련 설정을 일관되게 관리할 수 있으며, 다양한 Redis 배포 모드를 유연하게 지원할 수 있다. Config 객체를 사용하면 애플리케이션의 유지보수성과 확장성을 크게 향상시킬 수 있다.
게시판 프로젝트에서
postUpdate 부분에서 한번 사용을 해보려고한다.
public void update(long id, PostUpdateReqDto postUpdateReqDto) throws EntityNotFoundException {
log.info("업데이트 시작");
Post post = postRepository.findById(id).orElseThrow(EntityNotFoundException::new);
String lockName = "MEMBER" + id;
RLock lock = redissonClient.getLock(lockName);
long waitTime = 5L;
long leaseTime = 3L;
TimeUnit timeUnit = TimeUnit.SECONDS;
try {
boolean available = lock.tryLock(waitTime, leaseTime, timeUnit);
if (!available) {
throw new IllegalArgumentException("락 획득 실패");
}
// 락 획득후 수행하는 것
post.updatePost(postUpdateReqDto.getTitle(), postUpdateReqDto.getContents());
postRepository.save(post);
} catch (InterruptedException i) {
throw new IllegalArgumentException("락 얻으려고 했는데 뺏어감");
} finally {
try {
lock.unlock();
log.info("unlock complete: {}", lock.getName());
} catch (IllegalMonitorStateException e) {
throw new IllegalArgumentException("이미 종료된 락입니다.");
}
}
}
락의 이름을 MEMBER{id} 형식으로 설정하여 ID별로 고유한 락을 생성한다.
Redisson 클라이언트를 사용해 RLock 객체를 생성한다.
락을 획득한 후에 업데이트를 수행한다.
그리고 나서 락을 unlock을 해줘 놓아준다.
https://innovation123.tistory.com/185
[Redis] Redisson 분산 락을 간단하게 적용해보기
문제 상황 어떤 데이터에 대해 매우 빠르게 수정이 일어날 때 동시성 문제가 발생할 수 있다. 예를 들어 A라는 데이터를 수정하는 로직이 0.1초 소요되는데, 0.001초 간격으로 A라는 데이터를 수정
innovation123.tistory.com
[DB] Lock 총 정리 - 2 (낙관적 락과 비관적 락, 분산락, 데드락)
낙관적락, 비관적락, 분산락, 데드락에 대해서 알아보자
velog.io
https://sabarada.tistory.com/175
[database] 낙관적 락(Optimistic Lock)과 비관적 락(Pessimistic Lock)
안녕하세요. 오늘은 낙관적 락과 비관적 락의 개념에 대해서 알아보는 시간을 가져보도록 하겠습니다. DB 충돌 상황을 개선할 수 있는 방법 database에 접근해서 데이터를 수정할 때 동시에 수정이
sabarada.tistory.com
댓글