본문 바로가기
스터디/오라클 성능고도화 원리와 해법1

CH2. 트랜잭션과 LOCK - 03. 비관적 vs. 낙관적 동시성 제어, 04.동시성 구현 사례

by 취미툰 2019. 12. 28.
반응형

03. 비관적 vs. 낙관적 동시성 제어

n-Tier 구조가 지배적인 요즘 같은 개발 환경에서는 트랜잭션 고립화 수준을 변경하는 DBMS 기능을 사용할 수 없는 경우가 있습니다.
동시성 제어는 비관적 동시성 제어와 낙관적 동시성 제어로 나뉩니다.
비관적 동시성 제어사용자들이 같은 데이터를 동시에 수정할 것이라고 가정합니다. 따라서 한 사용자가 데이터를 읽는 시점에 Lock을 걸고 조회 또는 갱신처리가 완료될 때까지 이를 유지합니다. Locking은 첫 번째 사용자가 트랜잭션을 완료하기 전까지 다른 사용자들이 그 데이터를 수정할 수 없게 만들기 때문에 비관적 동시성 제어를 잘못 사용하면 동시성을 저해 받게 됩니다.
낙관적 동시성 제어사용자들이 같은 데이터를 동시에 수정하지 않을 것이라고 가정합니다. 따라서 데이터를 읽을 때는 Lock을 설정하지 않습니다. 그런데 낙관적 입장에 섰다고 해서 동시 트랜잭션에 의한 데이터의 잘못된 갱신을 신경 쓰지 않아도 되는것은 절대 아닙니다. 읽는 시점에 Lock을 사용하지 않았지만, 데이터를 수정하고자 하는 시점에 앞서 읽은 데이터가 다른 사용자에 의해 변경되었는지를 반드시 검사해야 합니다. 낙관적 동시성 제어를 사용하면 Lock이 유지되는 시간이 매우 짧아져 동시성을 높이는 데에 유리합니다.

(1) 비관적 동시성 제어
아래의 예를 통해 알아보겠습니다.
우수 고객을 대상으로 적립포인트를 추가시켜주는 이벤트를 실시한다고 가정하겠습니다. 고객의 다양한 실적정보를 읽고 복잡한 산출공식을 이용해 적립포인트를 계산하는 동안 다른 트랜잭션이 같은 고객 레코드를 변경한다면 문제가 발생할 수 있습니다.

select 적립포인트, 방문횟수,최근방문일시,구매실적 from 고객
where 고객번호 = :cust_num for update;
—- 새로운 적립포인트 계산
update 고객 set 적립포인트 = :적립포인트 where 고객번호 = :cust_num

위와 같이 select문에 for update를 사용하서 해당 고객 레코드에 Lock을 걸어둔다면 데이터가 잘못 갱신되는 문제를 방지할 수 있습니다.
하지만 이러한 조치는 시스템 동시성을 심각하게 떨어뜨릴 이유가 있습니다. wait 또는 nowait옵션을 함께 사용하여 해결합니다.
for update nowait —> 대기없이 ORA-00054를 던짐
for update wait 3 —> 3초 대기 후 ORA-00054를 던짐
해당 옵션을 사용하면 다른트랜잭션에 의해 Lock이 걸렸을때 에러를 뱉으며 트랜잭션을 종료할 수 있기 때문에 동시성을 증가시킬 수 있습니다.

(2) 낙관적 동시성 제어
아래의 예시를 통해 알아보겠습니다.
select 적립포인트,방문횟수,최근방문일시,구매실적 into :a, :b, :c, :d
from 고객
where 고객변호 = :cust_num
——-새로운 적립포인트 계산
update 고객 set 적립포인트 = :적립포인트
where 고객번호 = :cust_num
and 적립포인트 = :a
and 방문횟수 = :b
and 최근방문일시 = :c
and 구매실적 = :d

if sql%rowcount =0 then
alert (‘다른 사용자에 의해 변경되었습니다.’);
end if;

앞선 select문에서 읽은 컬럼들이 매우 많다면 update문에 조건절을 일일이 기술하는 것이 귀찮은 일이 될 수 있습니다. 만약 update 대상 테이블에 최종변경일시를 관리하는 컬럼이 있다면 이를 조건절에 넣어 간단히 해당 레코드의 갱신여부를 판단할 수 있습니다.

select 적립포인트,방문횟수,최근방문일시,구매실적,변경일시 into :a,:b,:c,:d,:mod_dt
from 고객
where 고객번호 = :cust_num
—-새로운 적립포인트 계산
update 고객 set 적립포인트 = :적립포인트, 변경일시 = sysdate
where 고객번호 = :cust_num
and 변경일시 :=mod_dt

if sql%rowcount =0 then alert (‘다른 사용자에 의해 변경되었습니다.’); end if;

낙관적 동시성 제어에서도 update전에 select 문에 for update nowait 같은 옵션을 부여해서 수행하면 다른 트랜잭션에 의해 설정된 Lock 때문에 동시성이 저하되는 것을 예방할 수 있습니다.

Tip. Oracle 10g부터 사용가능한 낙관적 동시성 제어를 위해 활용할 수 있는 기능
위에서 처럼 별도의 컬럼을 두고 동시성 제어를 하려면 테이블 레코드가 갱신이 있을때마다 변경일시 컬럼을 변경하도록 빠짐없이 구현해야 합니다. 그런데 애플리케이션을 통하지 않고 직접 값을 바꾸는 일이 생긴다면 변경일시변경규칙은 지켜지지 않고 Lost Update문제가 발생하게 됩니다.
Pseudo 컬럼 ora_rowscn을 활용한다면 Timestamp를 오라클이 직접 관리해주므로 쉽고 완벽하게 동시성을 제어할 수 있습니다.
ora_rowscn 컬럼값을 사용한다면 특정 레코드가 변경 후 커밋된 시점을 추적할 수 있습니다. 따라서 변경일시 컬럼을 따로 디자인하지 않더라도 동시성 제어에 활용할 수 있습니다. 사용하려면 테이블 생성 시 아래와 같은 옵션을 넣어서 생성해야 로우 단위로 SCN을 기록합니다.
create table test
ROWDEPENDENCIES
as
select * from scott.emp;
테이블 생성 시 기본값은 norowdependencies이고 이때는 ora_rowscn컬럼에 블록 SCN이 출력됩니다. 따라서 한 레코드만 변경해도 블록 내 모든 레코드의 ora_rowscn이 변경되는 셈이 됩니다.
단점으로는 영구히 로우 단위 SCN을 저장하지 않고 기한이 있다는 것입니다. 내부적으로 5일동안 보관하게 됩니다.
하지만, ora_rowscn을 사용해서 동시성 제어를 하다보면 lost update가 발생하는 버그가 발견되었으며 11gR2에서도 fix되지 않았습니다. 알고만 계시고 컬럼추가를 통한 제어를 하셔야 합니다.

04.동시성 구현 사례

(1) 일련변호 채번 동시성 높이기
Locking을 최소화하면서 채번 테이블로부터 일련번호를 채번하고자 할 때 사용할 수 있는 사례입니다. 일련번호를 채번하고자 할때 가장 좋은 선택은 DBMS 내의 sequence 기능을 이용하는 것입니다. 하지만 이런 기능을 사용할 수 없는 경우 주로 사용하는 방법이 1.데이터가 삽입되는 시점에 실시간으로 현재의 MAX값을 취해 1만큼 증가시킨 값을 이용하거나, 2.MAX값을 관리하는 별도의 채번 테이블에서 값을 가져오는 방식입니다.

1.실시간으로 MAX값을 얻어 처리할 때는 두개의 트랜잭션이 동시에 같은 값을 읽었을 경우, insert 하는 순간 PK제약에 위배되므로 예외처리를 통해 어렵지 않게 동시성을 제어할 수 있습니다.
2.채번 테이블을 사용할 때는 채번 후 다음 처리로 진행하기 전에 채번 테이블 값을 1만큼 증가시키는 갱신을 수행하는 어려움이 있습니다.

(2) 선분이력 정합성 유지
선분이력모델은 시작시점과 종료시점을 관리하는 모델입니다. 선분이력모델은 여러 측면에서 장점이 있지만 잘못하면 데이터 정합성이 쉽게 깨질 수 있다는 단점이 있습니다.


위와 같은 구조를 가지고 있는 테이블들이 있습니다.

declare
cur_dt varchar2(14);
begin
1. cur_dt := to_char(sysdate,’yyyymmddhh24miss’);

2.update 부가서비스이력
set 종료일시 = to_char(:cur_dt, ‘yyyymmddhh24miss’) - 1/24/60/60
where 고객ID = 1
and 부가서비스 ID = ‘A’
and 종료일시 = to_date(‘99991231235959’,’yyyymmddhh24miss’);

3.insert into 부가서비스이력 (고객ID,부가서비스ID,시작일시,종료일시)
values ( 1,’A’,to_date(:cur_dt,’yyyymmddhh24miss’),to_date(‘99991231235959’,’yyyymmddhh24miss’);

4.commit;
end;

위 트랜잭션은 기존 최종 선분이력을 끊고 새로운 이력 레코드를 추가하는 전형적인 처리 루틴입니다. 신규등록 건이면 2번 update에서 실패하고 3번 insert가 한건이 될것입니다.

첫번 째 트랜잭션이 1번을 수행하고 2번으로 진입하기 직전에 어떤 이유에서든 두 번째 트랜잭션이 동일 이력에 대해 1~4번을 먼저 진행해 버린다면 선분이력이 깨지게 됩니다. 따라서 트랜잭션이 순차적으로 진행할 수 있도록 직렬화 장치를 마련해야하며 1번 문장을 수행하지 직전에 select for update문을 이용해 해당 레코드에 Lock을 걸면 됩니다.
그런데 신규고객의 경우에는 Lock이 걸리지 않습니다. 레코드가 기존에 없었기 때문입니다. 그러면 동시에 두개의 트랜잭션이 3번 insert문으로 진입할 수 있고 결과적으로 시작일시는 다르면서 종료일시는 같은 두개의 이력레코드가 생길 수 있습니다.
select 고객ID from 고객 where 고객ID=1 for update nowait;
부가서비스 이력의 상위 엔터티인 고객 테이블에 Lock을 걸면 완벽하게 동시성 제어를 할 수 있습니다.

반응형

댓글