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

학습 목표
  1. 비관적 동시성 제어(Pessimistic Concurrency Control)와 낙관적 동시성 제어(Optimistic Concurrency Control)의 의미 이해
  2. 비관적, 낙관적 동시성 제어의 구현 사례 이해 및 동시성 저하 예방 방안 이해
    • 많은 경우에 있어서 트랜젝션의 동시성 제어를 개발자가 직접 구현해야 한다
    • DBMS가 제공하는 트랜젝션 고립화 수준 변경 기능을 사용할 수 없는 경우가 많음 : n-Tier 구조 등
  3. ORA_ROWSCN 활용 사례의 이해

(1) 비관적 동시성 제어

사용자들이 같은 데이터를 동시에 수정할 것이다
  • 한 사용자가 데이터를 읽는 시점에 Lock을 걸고 조회/갱신이 완료될 때까지 유지
  • 동시성 저해 우려 존재
예시)
  • 우수고객 대상으로 적립포인트를 추가시켜 주는 이벤트
  • 다양한 실적정보, 산출공식에 의한 적립포인트 계산중 다른 트랜젝션이 고객 레코드 변경시 문제 발생
  • select for update를 사용하여 Lock을 걸어서 데이터가 잘못 갱신되는 문제 방지

select 적립포인트, 방문횟수, 최근방문일시, 구매실적
  from 고객
 where 고객번호 = :cust_num
   for update nowait ;

-- 새로운 적립포인트 계산

update 고객
   set 적립포인트 = :적립포인트
 where 고객번호 = :cust_num

  • wait 또는 nowait 옵션을 사용하여 동시성 저해 방지

for update nowait --> 대기없이 Exception(ORA-00054)을 던짐
for update wait 3 --> 3초 대기 후 Exception(ORA-30006)을 던짐

(2) 낙관적 동시성 제어

사용자들이 같은 데이터를 동시에 수정하지 않을 것이다
  • 데이터를 읽을때 Lock을 설정하지 않음
  • 데이터 수정 시점에 앞서 읽은 데이터의 변경여부 반드시 체크해야 함

온라인 쇼핑몰 주문처리 트랜젝션 구현 사례


insert into 주문
select :상품코드, :고객ID, :주문일시, :상점번호, ...
  from 상품
 where 상품코드 = :상품코드
   and 가격 = :가격 ; -- 주문을 시작한 시점 가격

if sql%rowcount = 0 then
  alert('상품가격이 변경되었습니다.');
end if;

  • Lock이 유지되는 시간이 매우 짧아져 동시성을 높이는데 유리
  • '동시성 제어 없는 낙관적 프로그래밍' 우려
예시1)
  • 고객번호를 제외한 4개의 컬럼을 참조한 경우
  • select문에서 읽은 컬럼들이 많은 경우 update문에 조건절을 일일이 기술하는 번거로움 존재

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;

예시2)
  • update 대상 테이블에 최종변경일시를 관리하여 조건절에 사용시 간단하게 레코드 갱신여부 확인
  • for update 사용으로 동시성이 저하되는 것을 예방

select 적립포인트, 방문횟수, 최근방문일시, 구매실적, 변경일시
  into :a, :b, :c, :d, :mod_dt
  from 고객
 where 고객번호 = :cust_num;

-- 새로운 적립포인트 계산

select 고객번호
  from 고객
 where 고객번호 = :cust_num
   and 변경일시 = :mod_dt
   for update nowait ; -- 다른 트랜젝션에 의해 설정된 Lock 때문에 동시성이 저하되는 것을 예방

update 고객
   set 적립포인트 = :적립포인트,
       변경일시 = SYSDATE
 where 고객번호 = :cust_num
   and 변경일시 = :mod_dt ; --> 최종 변경일시가 앞서 읽은 값과 같은지 비교

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

예시3) ora_rowscn 활용
  • 오라클 10g부터 제공하는 Pseudo 컬럼 ora_rowscn을 활용한다면 Timestamp를 오라클이 직접 관리해 주므로 쉽고 완벽하게 동시성 제어 가능

select 적립포인트, 방문횟수, 최근방문일시, 구매실적, ora_rowscn
  into :a, :b, :c, :d, :rowscn
  from 고객
 where 고객번호 = :cust_num;

-- 새로운 적립포인트 계산

update 고객
   set 적립포인트 = :적립포인트,
       변경일시 = SYSDATE
 where 고객번호 = :cust_num
   and ora_rowscn = :rowscn ;

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

  • ora_rowscn 이라는 Pseudo 컬럼을 이용하면 특정 레코드가 변경된 후 커밋된 시점을 추적할 수 있음
  • 따로 변경일시 컬럼을 만들지 않고도 동시성 제어 가능
  • ora_rowscn을 사용하기 위해서 테이블 생성시 ROWDEPENDENCIES 옵션을 사용해야 함

create table t
ROWDEPENDENCIES
nologging
as
select * from scott.emp;

    • 기본값은 NOROWDEPENDENCIES인데, 이때는 블록단위로 ora_rowscn이 변경된다.
  • 주의할 점
    • ora_rowscn이 변경일시 컬럼을 대체할 수는 없음(동시성 제어 용도로만 사용)
    • ora_rowscn은 영구히 저장되는 값이지만 이를 시간정보로 변환하는 데 사용되는 매핑정보 테이블(sys.smon_scn_time)의 보관주기는 5일이므로 5일 넘게 지난 ora_rowscn 정보의 Timestamp 값 (SCN_TO_TIMESTAMP(ORA_ROWSCN))을 구하려 하면 에러(ORA-08181) 발생