lock이 사용되는 경우 phantom Read가 발생하게 된다. lock을 사용하면 undo log를 읽는 것이 아니라 테이블에서 조회하게 된다. 아래와 같이 쓰기 잠금(for update) / 읽기 잠금(for share)을 사용한 경우가 해당 된다.
일발적인 RDBMS
Gap Lock이 존재하지 않음. id = 50인 레코드만 잠금이걸린 상태이고, 사용자 A의 요청은 잠금 없이 즉시 실행 ➡️ 이때 사용자 B가 동일한 쓰기 잠금 쿼리로 다시 한번 데이터를 조회하면, 이번에는 2건의 데이터가 조회된다. 이게 Phantom Read
MySQL에는 Gap Lock이 존재한다. MySQL에는 갭 락이 존재하기 때문에 위의 상황에서 문제가 발생하지 않는다. 사용자 B가 SELECT FOR UPDATE로 데이터를 조회한 경우에 MySQL은 id가 50인 레코드에는 레코드 락, id가 50보다 큰 범위에는 갭 락으로 넥스트 키 락을 건다. 따라서 사용자 A가 id가 51인 member를 INSERT 시도한다면, B의 트랜잭션이 종료(커밋 또는 롤백)될 때 까지 기다리다가, 대기를 지나치게 오래 하면 락 타임아웃이 발생하게 된다.
select for update 잠금기반이니 → 테이블 데이터를 읽음
MySQL 기준으로 정리된 내용이다. (mysql의 기본 트랜잭션 단위: repeatable read) → gap lock 적용
SELECT FOR UPDATE(갭락) 이후 SELECT: 갭락 때문에 팬텀리드 X
SELECT FOR UPDATE (갭락)이후 SELECT FOR UPDATE(갭락): 갭락 때문에 팬텀리드 X
SELECT(mvcc) 이후 SELECT(mvcc): MVCC 때문에 팬텀리드 X
SELECT(mvcc) 이후 SELECT FOR UPDATE(현재 테이블에 갭락걸고 테이블 데이터 읽음): 팬텀 리드 O
3. 코드 분석
테이블과 로직은 아래와 같다.
Like 테이블
@Entity
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Table(name = "likes")
public class Like {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "Like_Id")
private Long id;
@ManyToOne
@JoinColumn(name = "Member_Id", nullable = false)
private Member member;
@ManyToOne
@JoinColumn(name = "Board_Id", nullable = false)
private Board board;
}
SELECT ENGINE_TRANSACTION_ID AS Trx_Id,
OBJECT_NAME AS `Table`,
INDEX_NAME AS `Index`,
LOCK_DATA AS Data,
LOCK_MODE AS Mode,
LOCK_STATUS AS Status,
LOCK_TYPE AS Type
FROM performance_schema.data_locks;
1. t1(2457) 좋아요 추가 -> t2(2458) 좋아요 추가
사진을 보면 서로 다른 트랜잭션 id가 각각 s-lock을 획득한걸 볼 수 있다.
2. t1(2457) 게시글 좋아요 수 업데이트
t1게시글 좋아요 업데이트를 위해 xlock 획득해야함 → 대기중
여기서 t2에서 동일한 게시글의 좋아요 수를 업데이트하면?
3. t2(2458) 게시글 좋아요 수 업데이트
t1에서 먼저 xlock을 요청했고, 결국 t1이 xlock을 획득함. t2의 트랜잭션은 모두 롤백된걸 볼 수 있음