두 개 이상의 트랜잭션이 동일한 데이터를 동시에 읽고 수정할 때,
한 트랜잭션의 수정 내용이 다른 트랜잭션의 수정 내용을 덮어쓰는 현상
ex) 트랜잭션 A가 x = 10을 읽고 x = 20으로 업데이트, 트랜잭션 B가 동시에 x = 10 을 읽고 x = 30으로 업데이트 후 커밋
A의 업데이트 결과(20)가 B의 업데이트 결과(30)으로 덮여쓰여짐
트랜잭션이 아직 커밋되지 않은 다른 트랜잭션의 데이터를 읽어 연산에 사용함.
이때, 다른 트랜잭션이 롤백되면 읽었던 값이 잘못된 데이터가 되는 현상
ex) 트랜잭션 A가 x = 20으로 수정한 뒤 커밋하지 않음, 트랜잭션 B가 A의 수정된 값을 읽고 x = 20 기반으로 연산 수행. A가 롤백되면서 x = 10으로 변경됨 (트랜잭션 B는 잘못된 데이터 사용한 게 됨)
트랜잭션이 같은 데이터를 여러 번 읽었는데,
다른 트랜잭션이 그 사이에 해당 데이터를 수정하여 처음 읽었을 때와 다른 값을 읽게 되는 현상
ex) 트랜잭션 A가 x = 10을 읽고, 이후 트랜잭션 B가 x = 20으로 값을 수정한 후 트랜잭션 A가 x 를 읽을 때, x = 20을 읽는 현상
트랜잭션이 범위 내에서 여러 행을 읽었을 때, 다른 트랜잭션이 새로운 데이터를 삽입하거나 삭제하여 읽을 데이터의 범위가 바뀌는 현상
트랜잭션 A가 특정 조건에 맞는 레코드를 읽고, 트랜잭션 B가 해당 조건을 만족하는 레코드를 삽입한 후,
트랜잭션 A가 다시 데이터를 읽을 때 새로운 레코드가 포함됨
애플리케이션 레벨에서 락을 관리하는 방법이다.
synchronized, ReentrantLock, Semaphore 등을 사용한다.
Semaphore
: 동시 접근 가능한 자원의 수를 제한하는 동기화 메커니즘. 리소스 풀이나 스레드 풀과 같은 한정된 자원을 관리하는데 유용함
특징 | synchronized | ReentrantLock | Semaphore |
락 획득/해제 방식 | 자동(자바 VM이 관리) | 명시적(lock()/unlock()) | 명시적(acquire()/release()) |
재진입성 | 불가능 | 가능(자기 자신이 락을 여러 번 획득 가능) | 없음 |
데드락 방지 |
X | X | X |
성능 | 낮은 성능 (경쟁 상태 시 성능 저하) | 높은 성능(다중 스레드 환경에서 효율적) | 자원 개수에 따라 성능이 달라짐 |
비차단 락 지원 | 없음 | 가능(tryLock()) | 가능(tryAcquire()) |
기타 기능 | 없음 | 공정성 설정, 타임아웃 등 | 자원 제한 개수 조정 |
간단하고 자동으로 관리되는 락을 사용할 때 적합 성능이 중요하지 않거나 락이 간단한 경우에 유용 |
타임아웃을 설정하거나 락을 시도하고 실패했을 때 다른 작업을 하고 싶을 때 사용 |
데이터베이스 연결 풀, 동시 접근이 제한된 자원 관리할 때 |
실제로 데이에 Lock을 걸어서 정합성을 맞추는 방법이다.
execlusive lock을 걸게 되며, 다른 트랜잭션에서는 lock이 해제되기 전에 데이터를 가져갈 수 없게 된다.
데드락이 걸릴 수 있기 때문에 주의하여 사용해야 한다.
장점
1. 데이터베이스 내에서 동시성 문제를 해결할 수 있다.
2. 트랜잭션의 일관성을 보장한다.
단점
트래픽이 높은 환경에서는 락 경쟁이 심해져 지연이 발생할 수 있다.
특정 행에 대해서 락을 걸어 다른 트랜잭션들이 해당 행을 수정하지 못하도록 한다.
-- 행 레벨 락
SELECT * FROM users WHERE user_id = 1 FOR UPDATE;
테이블 전체에 락을 걸어 동시에 여러 트랜잭션이 접근하지 못하도록 한다.
-- 테이블 레벨 락
LOCK TABLE users IN EXCLUSIVE MODE;
업데이트 전에 버전 번호를 체크하거나 타임스탬프를 확인하여 데이터 변경이 일어났는지 판별하는 방법이다.
데이터 충돌이 발생하면 롤백하거나 다시 시도한다.
JPA에서는 @Version 어노테이션을 사용하여 엔티티의 버전을 관리한다.
@Entity
public class Product {
@Id
private Long id;
@Version
private Long version;
private String name;
private int price;
}
충돌을 예방하면서도 성능을 최적화할 수 있기에 낙관적 처리에 적합하다.
자주 변경되지 않는 데이터에 대해 효율적이다.
재시도 로직이 필요하다.
서버가 여러 대일 경우, 분산 환경에서 동기화 문제를 해결하기 위해 분산 락을 사용한다.
Redis, ZooKeeper, Etcd와 같은 분산 시스템을 사용하여 락을 관리한다.
- Lettuce
SETNX 명령어(set if not exists)를 사용하여 하나의 서버만 자원을 사용할 수 있도록 락을 걸 수 있다.
Spin Lock 방식으로 Retry 로직을 개발자가 작성해주어야 한다.
- Redisson
pub-sub 기반으로 Lock 구현을 제공한다.
pub-sub 방식이란 채널을 하나 만들고, 락을 점유중인 스레드가 락을 해제했음을 대기 중인 스레드에 알려줌으로써 대기중인 스레드가 락 점유를 시도하는 방식이다.
ZooKeeper/Etcd: 분산 시스템에서 자원의 동기화를 위해 여러 서버 간의 락을 관리한다.
// Redis에서 분산 락 구현
Jedis jedis = new Jedis("localhost");
boolean isLockAcquired = jedis.setnx("lock_key", "locked") == 1;
if (isLockAcquired) {
// 작업 수행
jedis.del("lock_key"); // 작업 완료 후 락 해제
}