[동시성] DB Lock 실습 & 데드락

SIK407·2025년 6월 25일

Backend

목록 보기
7/10
post-thumbnail

◽ 서론

이전글: [동시성] 낙관적 락 & 비관적 락
이번엔 실습을 통해 눈으로 느낄 수 있는 특징이나 에러들을 정리했다.

@Entity
@NoArgsConstructor
@Data
public class Post {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    private int likes;

    @CreationTimestamp
    @Column(name = "created_at")
    private Timestamp createdAt;

    @UpdateTimestamp
    @Column(name = "updated_at")
    private Timestamp updatedAt;
}

이번에 사용한 엔티티 코드다.
그냥 단순하게 좋아요 수를 가지고 비교를 한다.

조건은
"좋아요를 약 500번 누른다고 요청이 들어왔을 때, 결과가 어떻게 나오는가?"
를 K6를 통해 "비관적 락"만 테스트 할 예정이다.


◽ 공유락 (Shared-Lock)

데이터를 변경하지 않는 Read에 대해 주어지는 Lock

read-lock, s-lock 이라고도 불린다.

@Lock(LockModeType.PESSIMISTIC_READ)
@Query("SELECT P FROM Post P WHERE P.id = :id")
Optional<Post> findByIdWithShareLock(@Param("id") Long id);

(우리의 친절한 JPA는 모든것을 알아서 지원해준다.)
저렇게 @Lock(LockModeType.PESSIMISTIC_READ) 을 붙여놓으면...

# 1은 예시입니다.
SELECT * FROM post WHERE id = 1 FOR SHARE;

이런식으로 SQL Query가 나간다.

그리고 500개를 요청하니까...

[java.sql.SQLTransactionRollbackException]

Deadlock found when trying to get lock; 
try restarting transaction

데드락이 발생했다...😱😱


◽ 데드락 (Dead-Lock)

잘못된 자원 관리로, 둘 이상의 프로세스 또는 스레드들이 아무것도 진행하지 않는 상태로 서로 영원히 대기하는 상황

아마 한국어로는 "교착상태"라고 많이 들어봤을 상황이다.

알기 쉽게 표현하면 이런 상황이다.

프로세스A(경찰)가 자원(돈)을 점유한 상태다.
프로세스A(경찰)은 또 다른 자원(인질)이 있어야 종료되고 자원을 반납한다.

하지만..
프로세스B(범인)은 또 다른 자원(인질)을 점유한 상태다.
B(범인) 역시 또 다른 자원(돈)이 있어야 종료되고 자원을 반납한다.

서로 필요한 자원을 서로 점유하고 있으니...
서로 무한정 대기하는 좋지 않은 상황이 발생한다.


◽ 공유락에서 데드락이 발생한 이유?

A가 가져온 행의 값을 변경 후, DB에 save()를 할려고 하는 순간..
쓰기가 가능한 X-Lock(베타락)을 얻을려고 시도한다.

S-Lock은 Read가 가능하기에,
B도 S-Lock을 통해 A와 같은 행을 Read하고 S-Lock을 걸어버렸다.

하지만, B가 S-Lock을 걸고 있는 탓에 A는 X-Lock을 얻을 수 없고 대기 상태에 빠진다.
물론 B역시 마찬가지로 A 때문에 대기 상태에 걸리며, 데드락이 발생한다.

MariaDB MySQL 같은 경우,
자동으로 데드락을 감지하고, 하나의 트랜잭션을 롤백하여 데드락을 해소한다고 한다.

그래서 500개 중에, 125개 정도만 살아버렸다...


◽ 베타락 (Exclusive-Lock)

동일한 행에 다른 트랜잭션을 생성하는 것을 허용하지 않는 Lock

Write Lock으로도 불리며, X-Lock 이라고도 한다.

@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT P FROM Post P WHERE P.id = :id")
Optional<Post> findByIdWithExclusiveLock(@Param("id") Long id);

이런식으로 LockModeType.PESSIMISTIC_WRITE를 붙여놓으면 된다.

SELECT * FROM post WHERE id = ? FOR UPDATE;

그럼 이런식으로 Sql Query가 나가게 되고...


정상적으로 500개의 요청이 간다.

500개도 제대로 들어온다.


◽ 결론

동시성을 해결할 수 있는 방법이

단일서버: synchronized
다중서버: DB Lock - 베타락

다음엔 한번 DB-Lock의 단점 & Redis를 이용한 동시성 제어를 해봐야겠다.

profile
감자 그 자체

0개의 댓글