본문 바로가기
Programming/Ruby On Rails

[Ruby On Rails] 동시성 제어하기 - ActiveRecord Locking

by 가론노미 2023. 3. 22.

 

API 동시 호출로 인하여 데이터의 무결성이 깨지는 문제가 있었고 트랜잭션 동시성 제어의 필요성을 느꼈다.

 

동시성 제어 (Concurrency Control) 란?

: 동시에 실행되는 여러 개의 트랜잭션이 작업을 성공적으로 마칠 수 있도록 트랜잭션의 실행순서를 제어하는 기법

동시성 제어의 목적

  • 트랜잭션의 직렬성 보장
  • 공유도 최대, 응답시간 최소, 시스템 활동의 최대 보장
  • 데이터 무결성 및 일관성 보장


그렇다면 동시성 제어를 어떻게 할 수 있을까?

  1. 락킹(locking) : 트랜잭션이 데이터에 잠금을 설정하면 다른 트랜잭션에서 해당 데이터에 대해 잠금이 해제되기 전까지 접근/수정/삭제를 불가능하도록 하여, 트랜잭션이 사용하는 자원에 대하여 상호 배제 기능을 제공하는 방식
  2. 타임스탬프 : 시스템에서 생성하는 고유번호인 타임스탬프를 트랜잭션에 부여함으로써 트랜잭션 간의 접근 순서를 미리 정하는 방식
  3. 적합성 검증 : 트랜잭션을 먼저 수행한 후 트랜잭션을 종료할 때 적합성을 검증하여 데이터베이스에 최종 반영하는 방식

 

이중 Locking을 통한 동시성 제어에 대해 알아보려고 한다. rails의 ActiveRecord에서 Locking을 지원하고 있다.




낙관적 잠금(Optimisitic Lock)

: 데이터 잠금을 사용하지는 않는 RDBMS에서 사용되는 동시성 제어 방법이다.

  • 여러 트랜잭션이 동일한 데이터에 업데이트를 시도할 수 있고, 커밋할 때만 유효성이 검사된다.
  • 데이터베이스 수준의 Rollback이 없기에, 충돌 시 대처방안을 구현해야 한다.
  • Application level에서 동작한다.

 

이런 상황에서 사용한다

  • 동시 레코드 업데이트가 드물거나, lock 오버헤드가 높은 것으로 예상될 때
  • 데이터베이스에 연결을 유지하고 있을 필요가 없는 대용량 시스템, 3계층 시스템 등
  • 데이터 충돌이 거의 일어나지 않을 것이라고 가정하는 낙관적으로 바라보는 곳에서 사용
  • 데이터 충돌이 일어나 dirty read가 되더라도 무관한 상황

 

ActiveRecord::Locking::Optimistic

lock_version 필드가 존재할 경우 낙관적 잠금을 지원한다.

레코드를 업데이트 할 때마다 lock_version 을 증가시키고,
lock_version을 통해 레코드가 열린 이후에 다른 프로세스가 해당 레코드를 변경하지는 않았는지 확인한다.

변경이 발생한 경우 ActiveRecord::StaleObjectError 예외가 발생하며 업데이트는 무시된다.

p1 = Person.find(1)
p2 = Person.find(1)

p1.first_name = "Michael"
p1.save # increments the lock_version column

p2.first_name = "should fail"
p2.save # Raises an ActiveRecord::StaleObjectError

locking_column 속성을 통해 lock_version 열의 이름을 재정의할 수 있다.

class Person < ActiveRecord::Base
  self.locking_column = :lock_person
end

 

물론, 낙관적 잠금이 동작하지 않도록 설정할 수도 있다.

ActiveRecord::Base.lock_optimistically = false





비관적 잠금(Perssimstic Lock)

: 데이터에 대한 동시 업데이트를 방지 하는 방법이다.

  • 하나의 트랜잭션이 레코드를 업데이트하기 시작하자마자 잠금이 설정된다.
  • Database level의 트랜잭션을 이용한다.

 

이런 상황에서 사용한다

  • 짧은 업데이트 시간 간격을 가지고 있어 직렬적으로 대기 할 수 있는 경우
  • 데이터베이스에 직접 연결하는 시스템, 2계층 시스템
  • 데이터 충돌이 분명 일어날 것이라고 가정하는 비관적으로 바라보는 곳에서 사용

 

ActiveRecord::Locking:Pessmistic

SELECT ... FOR UPDATE 및 기타 잠금 유형을 사용하여 행 수준 잠금을 지원한다.

Account.transaction do
    # select * from accounts where id=1 for update
    Account.find(1).lock!
end

with_lock을 사용하면 트랜잭션을 시작하는 동시에 lock을 걸 수 있다.

account = Account.find(1)
account.with_lock do
  # This block is called within a transaction,
  # account is already locked.
  account.balance -= 100
  account.save!
end

위 트랜잭션이 종료되기 전까지는 다른 프로세스에서 id가 1인 Account 데이터를 수정할 수 없다.

트랜잭션이 종료되기 전까지 대기하다가 종료된 후 트랜잭션이 시작된다.




<참고>