Lock, 트랜잭션, 동시성 제어에 대해 알아보자
같은 자원
을 액세스하려는 다중 트랜잭션 환경
에서 데이터베이스의 일관성
과 무결성
을 유지하려면 트랜잭션 순차적 진행
을 보장할 수 있는 직렬화(serialization)
장치 필요
=> Lock
을 통해 가능케 함
트랜잭션 오퍼레이션별
로 적당한 수준의 Lock
을 자동으로 설정
공유 Lock
: 데이터를 읽을 때
사용
=> 다른 공유 Lock과 호환
=> 배타적 Lock
과 호환X
배타적 Lock
: 데이터를 변경
할 때 사용
=> 트랜잭션 완료
까지 유지
=> 해제될 때 까지 다른 트랜잭션은 해당 리소스
에 접근 불가
=> 공유 Lock
, 배타적 Lock
과 호환X
블로킹
: Lock 경합
이 발생해 특정 세션이 작업을 진행하지 못하고 멈춰 선 상태
=> 해소하는 방법은 커밋(또는 롤백)
뿐
=> Lock 경합
이 발생하면 먼저 Lock
을 설정한 트랜잭션이 완료
될 때까지 후행 트랜잭션
은 기다려야 하며, 이런 현상이 자주 나타나면 사용자가 느끼는 성능 안좋음
=> Lock
에 의한 성능 저하 최소화
방안
원자성
을 훼손하지 않는 선에서 트랜잭션을 가능하면 짧게 정의
동시에 수행되지 않도록
설계, 트랜잭션이 활발한 주간
에 대용량 갱신 작업 수행X
블로킹 현상
에 대해 사용자가 무한정 기다리지 않도록 적절한 프로그래밍 기법
도입 (ex. SQL Server의 LOCK_TIMEOUT
)--sql server
set lock_timeout 2000 --대기 시간이 최대 2초를 넘지 않도록
--oracle
select * from t where no = 1 for update nowait -- 대기없이 Exception을 던짐
select * from t where no = 1 for update wait 3 -- 3초 대기 후 Exception을 던짐
트랜잭션 격리성 수준
을 불필요하게 상향 조정X
결과가 가장 빨리 나오는 SQL
을 작성하는 것이 Lock 튜닝 기본
교착상태
: 두 세션
이 각각 Lock
을 설정한 리소스를 서로 액세스
하려고 마주 보며 진행하는 상황
=> 둘 중 하나가 뒤로 물러나
지 않으면 영영 안풀림
=> DBMS가 둘 중 한 세션
에 에러 발생
을 시켜 문제 해결
=> 교착상태 발생 가능성
줄이는 방안
테이블 접근 순서
를 같게 처리
마스터 테이블 다음에 상세 테이블 갱신
하기로 규칙을 정하고, 모든 애플리케이션 개발자가 이 규칙을 지키면 교착상태 발생XSQL Server
는 갱신(update Lock)
사용해 발생 가능성 줄임공유 Lock
=> 트랜잭션이나 쿼리 수행이 완료될 때까지 유지되는 것이 아니라, 다음 레코드를 읽으면 곧바로 해제
(기본 트랜잭션 격리성 수준(Read committed)
에서만)
=> 트랜잭션 내
에서 공유 Lock
을 유지
하려면, 테이블 힌트
로 holdlock
지정
-- 변경할 목적으로 레코드 읽을 땐 반드시 아래처럼 처리해야 함
begin tran
select 적립포인트, 방문횟수, 최근방문일시, 구매실적
from 고객 with (holdlock)
where 고객번호 = :cust_num
-- 새로운 적립포인트 계산
update 고객 set 적립포인트 = :적립포인트 where 고객번호 = :cust_num
commit
배타적 Lock
=> 앞에서 설명한 내용과 동일
갱신 Lock
=> ex. 위 예제의 트랜잭션이 같은 고객에 대해 동시에 수행됐다고 가정, 두 트랜잭션 모두 처음에는 공유 Lock
을 설정했다가 적립포인트 변경 직전에 배타적 Lock
을 설정하려고 할 것. 그러면 상대편 트랜잭션
에 의한 공유 Lock
이 해제되기만
을 기다리는 교착상태
에 빠짐
=> 위 경우를 방지
하려고 갱신(Update) Lock
을 두었으며, uplock 힌트
를 지정
=> 한 자원에 대한 갱신 Lock
은 한 트랜잭션만
설정
=> 갱신 Lock
은 공유 Lock
과는 호환
begin tran
select 적립포인트, 방문횟수, 최근방문일시, 구매실적
from 고객 with (updlock)
where 고객번호 = :cust_num
-- 새로운 적립포인트 계산
update 고객 set 적립포인트 = :적립포인트 where 고객번호 = :cust_num
commit
의도 Lock
=> 특정 로우에 Lock
을 설정하면 그와 동시에 상위 레벨 개체(페이지, 익스텐트, 테이블)
에 내부적으로 의도(Intent) Lock
이 설정
=> Lock을 설정하려는 개체의 하위 레벨에서 선행 트랜잭션
이 어떤 작업을 수행
중인지를 알리는 용도
=> 일종의 푯말
=> ex. 구조 변경을 위해 테이블을 잠그려 할 때, 그 하위의 모든 페이지나 익스텐트, 심지어 로우에 어떤 Lock
이 설정돼 있는지 일일이 검사하려면 오래걸리는데, 이런 현상 방지. 즉 해당 테이블
에 어떤 모드
의 의도 Lock
이 설정돼 있는지만 보고도 작업을 진행
할지 아니면 기다릴지
결정 가능
스키마 Lock
=> 테이블 스키마에 의존적인 작업
을 수행할 때 사용
Sch-S(Schema Stability)
: SQL을 컴파일하면서 오브젝트 스키마
를 참조할 때 발생, 읽는 스키마 정보
를 수정
하거나 삭제
하지 못하도록 함Sch-M(Schema Modification)
: 테이블 구조를 변경하는 DDL문
을 수행할 때 발생, 수정중인 스키마 정보를 다른 세션이 참조하지 못하도록
함Bulk Update Lock
=> 테이블 Lock의 일종으로, 테이블에 데이터
를 Bulk Copy
할 때 발생
=> 병렬 데이터 로딩(Bulk Insert나 bcp)
은 허용, 일반적인 트랜잭션
작업 허용X
Lock Escalation
: 관리할 Lock 리소스가 정해진 임계치
를 넘으면서, 로우 레벨 락
이 페이지, 익스텐트, 테이블 락
으로 점점 확장
되는 것
Lock 레벨이 낮을
수록 동시성
증가, 관리해야 할 Lock 개수 증가
로 리소스 많이 소비
Lock 레벨이 높을
수록 적은 양의 리소스
사용, 하나의 Lock으로 수많은 레코드를 한꺼번에 잠그기 때문에 동시성 나빠짐
호환된다
: 한 리소스에 두 개 이상의 Lock 동시 설정
가능
스키마 Lock 호환성
Sch-S
는 Sch-M 제외
모든 Lock과 호환
Sch-M
은 어떤 Lock
도 호환X
DML Lock, DDL Lock, 래치, 버퍼, 라이브러리 캐시 Lock/Pin
등 다양한 종류의 Lock을 사용
DML Lock
: 다중 사용자
에 의해 동시에 액세스
되는 사용자 데이터의 무결성
을 보호해줌
=> 로우 Lock
과 테이블 Lock
오라클 로우 Lock
은 항상 배타적
=> insert, update, delete, select ~ for update 문
을 수행한 트랜잭션에 의해 설정
=> 해당 트랜잭션이 커밋
또는 롤백
할 때까지 다른 트랜잭션은 해당 로우 변경 불가
일반 select문
에 의해 읽힌 레코드에는 어떤 Lock도 설정X
=> 오라클의 읽기
와 갱신
은 서로 방해X
읽
으려는 데이터를 다른 트랜잭션이 갱신중
이더라도 기다리지X
갱신
하려는 데이터를 다른 트랜잭션이 읽는중
이더라도 기다리지X(select ~ for update 제외)
갱신
하려는 데이터를 다른 트랜잭션이 갱신중
이면 기다림오라클은 아무리 많은 레코드를 갱신
하더라도 Lock Escalation 발생X
한 트랜잭션이 로우 Lock
을 얻을 때 해당 테이블의 테이블 Lock
도 동시에 얻음
=> 현재 트랜잭션이 갱신중
인 테이블의 구조
를 변경(DDL
)하지 못하게 막기 위해서
테이블 Lock 종류
select .. for update 문
수행 시 RS 모드 테이블 Lock
insert, update, delete 문
수행 시 RX 모드 테이블 Lock
-- 명시적으로 Table Lock 얻기
lock table emp in row share mode;
lock table emp in row exclusive mode;
lock table emp in share mode;
lock table emp in share row exclusive mode;
lock table emp in exclusive mode;
오라클의 테이블 Lock
은 Lock을 획득한 선행 트랜잭션이 해당 테이블에서 현재 어떤 작업을 수행 중
인지를 알리는 일종의 푯말(Flag)
=> 후행 트랜잭션은 어떤 테이블 Lock이 설정
돼 있는지만 보고 그 테이블로의 진입 여부 결정
트랜잭션(Transaction)
: 업무 처리를 위한 논리적인 작업 단위
=> 여러 개의 갱신 연산
이 하나의 작업
처럼 전부 처리
되거나 아예 하나도 처리되지 않도록
(All or Nothing
) 동시 실행 구현
원자성(Atomicity)
: 트랜잭션은 분해 불가능
한 업무의 최소단위이므로 전부 처리
되거나 아예 하나도 처리되지 않아야
함일관성(Consistency)
: 일관된 상태의 데이터베이스에서 하나의 트랜잭션을 성공적으로 완료
하고 나면 그 데이터베이스는 여전히 일관된 상태
여야 함. 즉, 트랜잭션 실행 결과
로 데이터베이스 상태가 모순되지 않아야
함격리성(Isolation)
: 실행 중
인 트랜잭션의 중간결과
를 다른 트랜잭션이 접근 불가
영속성(Durability)
: 트랜잭션이 일단 그 실행을 성공적으로 완료하면 그 결과
는 데이터베이스에 영속적으로 저장
트랜잭션 격리성
: 일관성
과 마찬가지로, Lock
을 강
하게 오래
유지할수록 강화
, Lock을 최소화
할 수록 약화
Dirty Read
: 다른 트랜잭션이 수정
한 후 커밋하지 않은
데이터를 읽는 것
=> 변경 후 아직 커밋되지 않은 값을 읽었는데, 변경을 가한 트랜잭션이 최종적으로 롤백
된다면 그 값을 읽은 트랜잭션은 비일관된 상태
Non-Repeatable Read
: 한 트랜잭션 내에서 같은 쿼리
를 두 번
수행했는데, 그 사이 다른 트랜잭션
이 값을 수정
또는 삭제
하는 바람에 두 쿼리 결과
가 다르게
나타나는 현상
Phantom Read
: 한 트랜잭션 내에서 같은 쿼리
를 두 번 수행했는데, 첫 번째 쿼리에서 없던 유령 레코드
가 두 번째 쿼리에서 나타나는 현상Read Uncommitted
: 트랜잭션에서 처리 중인 아직 커밋되지 않은
데이터를 다른 트랜잭션이 읽는 것 허용
Read Committed
: 트랜잭션이 커밋
돼 확정된 데이터만 다른 트랜잭션이 읽도록 허용
함으로써 Dirty Read 방지
Non-Repeatable Read
와 Phantom Read
현상은 못막음Repeatable Read
: 트랜잭션 내에서 쿼리를 두 번 이상 수행
할 때, 첫 번째 쿼리에 있던 레코드가 사라지거나 값이 바뀌는 현상
을 방지Phantom Read
현상 못막음Serializable Read
: 트랜잭션 내에서 쿼리를 두 번 이상 수행
할 때, 첫 번째 쿼리에 있던 레코드가 사라지거나 값이 바뀌지 않고 새로운 레코드가 나타나지 않음
다중 트랜잭션 환경
에서 DBMS가 제공하는 기능을 이용해 동시성
을 제어하려면 트랜잭션 시작 전
에 명시적으로 Set Transaction
명령어를 수행하기만 하면 됨
-- serializable Read로 상향 조정
set transaction isolation level read serializable;
Repeatable Read
나 Serializable Read
로 올리면, 이를 구현하기 위해 Locking 메커니즘
에 의존
=> 공유 Lock
을 트랜잭션이 끝날때 까지 유지
다중버전 동시성 제어(Multiversion Concurrency Control)
: 진행 중인 트랜잭션에 의해 변경된 데이터
를 읽고자 할 때는 변경 이전 상태로 되돌린 버전을 읽는 것
=> 변경이 아직 확정되지 않은 값을 읽으려는 것이 아니므로 공유 Lock 설정 X
=> 읽는 세션과 변경하는 세션이 서로 간섭현상 일으키지 X
동시성 제어
: 동시에 작동하는 다중 트랜잭션
의 상호 간섭 작용
에서 데이터베이스를 보호
하는 것
동시성
과 일관성
은 트레이드 오프
관계
=> 동시성
을 높이려고 Lock 사용 최소화
하면 일관성
유지 어려움
=> 일관성
을 높이려고 Lock 적극적
으로 사용하면 동시성
저하
비관적 동시성 제어(Pessimistic Concurrency Control)
: 사용자들이 같은 데이터를 동시에 수정
할 것이라고 가정
=> 데이터를 읽는 시점
에 Lock
을 걸고 트랜잭션이 완료될 때 까지
유지
select 적립포인트, 방문횟수, 최근방문일시, 구매실적
from 고객
where 고객번호 = :cust_num for update;
-- 새로운 적립포인트 계산
update 고객 set 적립포인트 = :적립포인트 where 고객번호 = :cust_num;
select 시점
에 Lock
을 거는 것은 동시성
을 떨어뜨릴 수 있으므로 wait
또는 nowait 옵션
을 함께 사용
for update nowait -- 대기없이 Exception을 던짐
for update wait 3 -- 3초 대기 후 Exception을 던짐
SQL Server
에서는 명시적으로 cursor
를 선언할 때만 for update 절
이 가능하므로 holdlock
이나 updlock힌트
를 사용하는 것이 편리
낙관적 동시성 제어(Optimistic Concurrency Control)
: 사용자들이 같은 데이터를 동시에 수정하지 않을
것이라고 가정
=> 데이터 읽을 때 Lock 설정X
=> 수정 시점에 다른 사용자에 의해 값이 변경
됐는지 반드시 검사
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;
읽기 작업에 공유 Lock
을 사용하는 Locking 메커니즘
에서는 읽기 작업
과 쓰기 작업
이 서로 방해
를 일으켜 동시성에 문제
잔고 총합
을 구하는 아래 쿼리가 TX1 트랜잭션
에서 수행
TX1> select sum(잔고) from 계좌 ;
계좌이체
를 처리하는 TX2 트랜잭션
도 작업 시작
TX2> update 계좌 set 잔고 = 잔고 + 100 where 계좌번호 = 7; -- 1
TX2> update 계좌 set 잔고 = 잔고 - 100 where 계좌번호 = 3; -- 2
TX2> commit;
TX1
: 2번 계좌까지 읽는다. 현재 잔고 총합은 2000원TX2
: 1번 Update
를 실행한다. 7번 계좌 잔고는 1100원이 됐고, 아직 커밋 XTX1
: 6번 계좌까지 읽어 내려간다. 현재까지 잔고 총합은 5000원이다.TX2
: 2번 Update
를 실행함으로써 3번 계좌
는 900원, 7번 계좌는 1100원인 상태에서 커밋
TX1
: 10번 계좌까지 읽어 내려간다. 7번 계좌 잔고를 1100원으로 바꾼 TX2 트랜잭션
이 커밋
됐으므로 이 값을 읽어서 구한 잔고 총합은 10100
원이 된다.트랜잭션 격리성 수준을 상향 조정
하면 일관성이 높아지지만 동시성을 저하
시키고 교착상태
가 발생할 가능성이 높다.
=> 트랜잭션 격리성 수준
을 Repeatable Read
로 설정하면, TX2
가 1번 Update
를 통해 7번 레코드에 배타적 Lock
을 설정하고, TX1
은 3번 레코드
에 공유 Lock
을 설정하면, TX2
는 2번 update
를 실행하는 단계에서 3번 레코드
에 걸린 공유 Lock
을 대기하고, TX1
은 7번 레코드
를 읽으려는 순간 영원히 Lock
이 풀릴 수 없는 교착상태
에 빠짐
다중버전 동시성 제어(Multiversion Concurrency Control, MVCC)
: 동시성
과 일관성
을 동시에 높이는 모델
MVCC 메커니즘
변경할 때
마다 그 변경사항을 Undo 영역
에 저장쿼리(또는 트랜잭션)
시작 시점 이후에 변경된(변경 진행중 또는 커밋된)값
을 발견하면 Undo 영역
에 저장된 정보를 이용해 쿼리 시작 시점
의 일관성 있는 버전(CR copy)
을 생성하고 그것을 읽는다
쿼리
도중에 배타적 Lock
이 걸린 레코드를 만나더라도 대기X
-> 동시성 측면 유리
사용자에게 제공되는 데이터의 기준 시점
이 쿼리 시작 시점
으로 고정
-> 일관성 측면 유리
MVCC
를 이용한 읽기 일관성에는 문장 수준
과 트랜잭션 수준
2가지가 있음
문장수준 읽기 일관성(Statement-Level Read Consistency)
: 다른 트랜잭션에 의해 데이터의 추가, 변경, 삭제
가 발생하더라도 단일 SQL문 내
에서 일관성 있게 값을 읽는 것
=> 일관성 기준 시점 : 쿼리 시작 시점
쿼리가 10023 시점 이후 변경된 데이터 블록을 만났을 때 Undo 세그먼트
에 저장된 정보를 이용해 10023 이전 시점
으로 되돌리고서 값을 읽음
--sql server에서 문장수준 읽기 일관성 모드로 DB 운영
alter database <데이터베이스 이름> set read_committed_snapshot on;
트랜잭션 수준 읽기 일관성(Transaction-Level Read Consistency)
: 다른 트랜잭션에 의해 데이터가 추가, 변경, 삭제
가 발생하더라도 트랜잭션 내
에서 일관성 있게 값을 읽는 것
=> 격리성 수준
을 Serializable Read
로 올려야 함
--sql server에서 트랜잭션 읽기 일관성 모드로 DB 운영
alter database <데이터베이스 이름> set allow_snapshot_isolation on;
--트랜잭션 시작 전 트랜잭션 격리성 수준을 sanpshot으로 변경
set transaction isolation level snapshot
begin tran
select ... ;
update ... ;
commit;
Snapshot too old
: Undo 영역
에 저장된 Undo 정보
가 다른 트랜잭션
에 의해 재사용
돼 필요한 CR Copy
을 생성할 수 없을 때
발생
Snapshot too old
에러 발생 가능성 줄이는 방법
Undo 영역
의 크기 증가
커밋 수행X
fetch across commit
형태의 프로그램 작성을 피해 다른 방식으로 구현커밋 이전에 열려 있던 커서
는 더는 Fetch하면 안됨
. 또는 커밋 횟수 줄임오래 걸리는 쿼리
가 같이 수행되지 않도록
시간 조정일정 범위
로 나누어 읽고 단계적
으로 실행할 수 있도록 코딩일관성
에 문제 없을 때만같은 블록
을 여러 번 방문
하는 Nested Loop 조인문
또는 인덱스를 경유한 테이블 액세스
를 수반하는 프로그램이 있는지 확인하고, 회피할 수 있는 방법 찾는다소트 부하
를 감수하더라도 order by 등
을 강제로 삽입
해 소트연산
이 발생하도록 한다대량 업데이트 후
에 곧바로 해당 테이블
또는 인덱스
를 Full Scan
하도록 쿼리를 수행select /*+ full(t) */ count(*) from table_name t
select count(*) from table_name where index_column > 0