API 동시 호출로 인하여 데이터의 무결성이 깨지는 문제가 있었고 트랜잭션 동시성 제어의 필요성을 느꼈다.
동시성 제어 (Concurrency Control) 란?
: 동시에 실행되는 여러 개의 트랜잭션이 작업을 성공적으로 마칠 수 있도록 트랜잭션의 실행순서를 제어하는 기법
동시성 제어의 목적
- 트랜잭션의 직렬성 보장
- 공유도 최대, 응답시간 최소, 시스템 활동의 최대 보장
- 데이터 무결성 및 일관성 보장
그렇다면 동시성 제어를 어떻게 할 수 있을까?
- 락킹(locking) : 트랜잭션이 데이터에 잠금을 설정하면 다른 트랜잭션에서 해당 데이터에 대해 잠금이 해제되기 전까지 접근/수정/삭제를 불가능하도록 하여, 트랜잭션이 사용하는 자원에 대하여 상호 배제 기능을 제공하는 방식
- 타임스탬프 : 시스템에서 생성하는 고유번호인 타임스탬프를 트랜잭션에 부여함으로써 트랜잭션 간의 접근 순서를 미리 정하는 방식
- 적합성 검증 : 트랜잭션을 먼저 수행한 후 트랜잭션을 종료할 때 적합성을 검증하여 데이터베이스에 최종 반영하는 방식
이중 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 데이터를 수정할 수 없다.
트랜잭션이 종료되기 전까지 대기하다가 종료된 후 트랜잭션이 시작된다.
<참고>
'Programming > Ruby On Rails' 카테고리의 다른 글
[Ruby On Rails] Module Mixin (2) - ActiveSupport::Concern (0) | 2023.04.30 |
---|---|
[Ruby On Rails] Module Mixin (1) - include, prepend, extend (0) | 2023.04.18 |
[Ruby On Rails] Eager loading으로 N+1 문제 해결하기 (0) | 2023.04.16 |
[Ruby On Rails] Elasticsearch 검색 템플릿(Search Template) 사용하기 (0) | 2023.03.28 |
[Ruby On Rails] enum 경고 문구 - Overwriting existing method (0) | 2023.03.26 |