Concurrency 제어 - Locking

낙관적 락, 비관적 락 등

Posted by Yan on September 15, 2024

동시성 제어와 락 메커니즘

lock이란?

  • Lock이란 트랜잭션 처리의 순차성을 보장하기 위한 방법이다.
  • Lock을 적용한다는 것의 의미란, 공유자원을 동시에 접근해서 문제가 될 수 있는 부분(critical section)을 순차적으로 처리하는 것을 의미한다.
  • 그런데 lock은 처리량의 병목을 발생시킨다.
  • Locking 기법은 병행 수행되는 트랜잭션들이 동일한 데이터에 동시에 접근하지 못하도록 lock과 unlock이라는 2개의 연산을 이용해 제어한다. 기본 원리는 한 트랜잭션이 먼저 접근한 데이터에 대한 연산을 모두 마칠 때까지, 다른 트랜잭션이 접근하지 못하도록 상호 배제하여 직렬 가능성을 보장하는 것이다.

  • lock 연산은 트랜잭션이 사용할 데이터에 대한 독점권을 가지기 위해 사용한다.
  • unlock 연산은 트랜잭션이 데이터에 대한 독점권을 반납하기 위해 사용한다.

Concurrency control이란?

다중 사용자 환경을 지원하는 데이터베이스 시스템에서 동시에 실행되는 여러 트랜잭션 간의 간섭으로 문제가 발생하지 않도록 트랜잭션 실행 순서를 제어하는 기법

Write lock

  • exclusive lock
  • Read/write할 때 사용한다
  • 다른 트랜잭션이 같은 데이터를 read/write하는 것을 허용하지 않는다.

Read lock

  • shared lock
  • Read할때 사용한다.
  • 다른 트랜잭션이 같은 데이터를 read하는 것을 허용한다.

lock의 호환성

분류 Read lock Write lock
Read lock O X
Write lock X X

Lock만으로는 serializable을 보장할 수 없다.

2PL protocol : two-phase locking

트랜잭션의 모든 locking operation이 최초의 unlock operation보다 먼저 수행되도록 하는 것

  • serializability보장!
Expanding phase(growing phase)

Lock을 취득하기만 하고 반환하지는 않는 phase

Shrinking phase(contracting phase)

lock을 반환만 하고 취득하지는 않는 phase

Deadlock
  • 2PL protocol을 사용하면 상황에 따라 데드락이 발생할 수 있다.

Conservative 2PL

  • 모든 lock을 취득한 뒤 트랜잭션 시작
  • deadlock-free
  • 실용적이지 않다.

strict 2PL (S2PL)

  • strict schedule을 보장하는 2PL
  • Recoverability 보장
  • write-lock을 commit/rollback 될 때 반환한다.

strong strict 2PL (SS2PL)

  • strict schedule을 보장하는 2PL
  • Recoverability 보장
  • write-lock, read-lock모두 commit/rollback될 때 반환한다.
  • S2PL보다 구현이 쉽다.
  • 단점: lock을 조금 더 오래 가지고 있음

MVCC (mulitversion concurrency control)

  • read-read를 제외하고는 한쪽이 block되니까 전체 처리량이 좋지 않다.
  • read, write가 서로를 block하는 것이라도 해결해보기 위해 나온 개념
  • commit된 데이터만 읽는다.
  • 데이터를 읽을 때 특정 시점 기준으로 가장 최근에 commit된 데이터를 읽는다. = consistent read
  • 데이터 변화(write)이력을 관리한다.
  • read와 write는 서로를 block하지 않는다.
  • MVCC는 commit된 데이터를 읽기 때문에 read uncommitted부터는 적용되지 않는다.

비관적 락 vs 낙관적 락

비관적 락

  • 데이터에 접근할 때마다 락을 걸어 다른 트랜잭션이 접근하지 못하게 하는 방식이다. 데이터 충돌을 미리 방지하기 위해 사용된다.
  • 트랜잭션이 시작될 때 Shared Lock 또는 Exclusive Lock을 걸고 시작하는 방법이다.
    • Shared Lock을 걸면 read 연산밖에 못하기 때문에, write 연산을 하기 위해서는 Exclusive Lock을 얻어야 한다.
    • 하지만, 만약 Shared Lock이 다른 트랜잭션에 의해 걸려 있으면 해당 Lock을 얻지 못하므로 업데이트를 할 수 없다.
    • 데이터를 수정하기 위해서는 해당 트랜잭션을 제외한 모든 트랜잭션이 종료(Commit)되어야 한다.
  • Repeatable_Read 또는 Serializable 정도의 격리성 수준을 제공한다.
  • Lock을 획득할 때까지 트랜잭션은 대기하므로, Timeout을 설정할 수 있다.

예시) SELECT FOR UPDATE

BEGIN TRANSACTION;
SELECT * FROM orders WHERE order_id = 1 FOR UPDATE;
COMMIT;

특정 주문(order_id = 1)에 대해 업데이트를 수행하기 전에 락을 걸어 다른 트랜잭션이 접근하지 못하게 한다.

낙관적 락

데이터 변경 시점에만 충돌을 검사하는 방식이다. 낙관적 락은 데이터 충돌이 자주 발생하지 않는 상황에서 성능을 최적화하기 위해 사용된다.

주로 DB가 아니라 애플리케이션 레벨에서 사용되며, 데이터 변경 시점에 버전 번호를 비교하여 충돌을 검사한다.

트랜잭션을 커밋하기 전까지는 트랜잭션의 충돌을 알 수 없다.

예시) 주문 객체의 버전번호 비교하여 충돌 검사

1
2
3
4
5
6
7
8
public void updateOrder(Order order) {
    Order existingOrder = orderRepository.findById(order.getId());
    if (existingOrder.getVersion() != order.getVersion()) {
        throw new OptimisticLockException("Order has been updated by another transaction");
    }
    order.setVersion(order.getVersion() + 1);
    orderRepository.save(order);
}
낙관적 읽기 & 낙관적 쓰기
  • 낙관적 READ : OPTIMISTIC - 잠금 모드를 요청할 때마다 entityManager는 dirty read 뿐만 아니라 unrepeatable read도 방지한다.
  • 낙관적 WRITE : OPTIMISTIC INCREMENT - 버전 속성 값을 증가시킨다.

비관적 락과 낙관적 락의 비교

분류 장점 단점
낙관적 락 충돌이 안난다는 가정하에, 동시 요청에 대해서 처리 성능이 좋다. 충돌이 발생할 경우 예외 처리가 필요하다, 충돌이 나면 롤백 처리를 수동으로 해줘야 한다.
비관적 락 데이터 충돌이 자주 발생하지 않는 상황에서 성능을 최적화할 수 있다, 데이터의 무결성을 보장하는 수준이 매우 높다. 서로 자원이 필요한 경우에, 락이 걸려있으면 Deadlock이 일어날 가능성이 있다. 락을 걸고 있는 동안 다른 트랜잭션이 대기해야 하므로 성능 저하가 발생할 수 있다.

Named Lock

락의 이름을 임의로 설정하고, 해당 락을 사용하여 동시성을 처리하는 방식

  • 비관적락과 마찬가지로 DB단에서 Lock을 설정하여 동시성을 처리한다.
  • 애플리케이션 코드 단에서 해당 로직을 사용할 때는 개발자가 지정한 이름의 Lock을 사용하게 함으로써 의도적으로 동시성을 보장하기 위해 개발자가 Lock을 설정하는 것
  • 분산 락을 사용하려고 할 때 많이 사용하는 방식이다.
  • Named Lock을 사용할 때는 네임드 락을 설정하는 부분과 비즈니스 로직의 트랜잭션을 분리해야 한다.
차이점 비관적 락 네임드 락
lock의 해제 여부 비관적 락은 Lock을 가진 트랜잭션이 종료되면 자동으로 Lock이 해제되어 대기중인 트랜잭션 작업을 수행할 수 있다. 트랜잭션이 종료되더라도 Lock이 자동으로 해제되지 않기 때문에 Lock 해제를 구현하거나 Lock Timeout 시간이 끝나야 Lock이 해제된다.
Lock을 설정하는 대상 해당 테이블의 인덱스 레코드에 Lock을 설정한다. 해당 테이블이 아닌 별도의 MySQL 공간에 지정한 이름의 Lock을 설정한다.

구현방법

낙관적 락

JPA @Version 어노테이션

@Version: 버전 관리용 필드를 추가해 트랜잭션 내에서 처음 조회되었을 때의 버전과 수정 후 커밋될 때의 버전을 비교한다. 해당 어노테이션이 붙은 컬럼이 업데이트 될 때 값을 1씩 더해서 저장한다. (컬럼을 직접 추가하면 된다)

  • 처음 조회될 때의 버전과 commit될 때의 버전이 서로 다르면 충돌이 발생한 것으로 판단하고 예외를 발생시키다.
  • Spring 기반의 JPA에서 낙관적락을 사용하게 되면 충돌시 Hibernate에서 StaleStateException 을 발생시킨다.
  • 그리고 Spring에서 이 에외를 OptimisticLockingFailureException 로 감싸서 응답하여, 충돌이 발생했는지 알 수 있다.

예시)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Entity
public class Student {

    @Id
    private Long id;

    private String name;

    private String lastName;

    @Version
    private Integer version;

    // getters and setters

}
  • version 어노테이션을 사용하는 엔티티의 필드는 오직 1개여야만 한다.
  • primary table이어야 한다.
  • 어노테이션을 사용할 수 있는 필드는 int, Integer, long, Long, short, Short, Timestamp 중 하나여야 한다.
reference

Optimistic Locking in JPA
낙관적 락 테스트하기
LOCK을 활용한 concurrency control 기법을 배워봅니다. 2PL(two-phase locking)도 같이 설명드려요
cs study