데이터베이스 동시성
다수의 사용자들이 데이터베이스에서 읽고 쓰는 권한을 가질 때, 한 명 이상의 사용자가 동시에 같은 데이터에 접근할 가능성이 있다. 데이터베이스의 무결성을 보호하고, 사용자와 트랜잭션이 항상 정확하고 일관된 데이터를 지니기 위해서는 다중 사용자 환경에서의 접근과 갱신에 대한 통제가 필수적이다. 적정한 통제가 없으면 데이터는 어긋난 순서로 부정확하게 갱신될 수 있다.
대부분의 상용 데이터베이스 시스템과 마찬가지로 CUBRID도 데이터베이스 내의 동시성(concurrency)을 위한 기본 요소인 직렬성(serializability)을 수용한다. 직렬성은 여러 트랜잭션을 동시에 수행할 때마다 요구되며, 여러 트랜잭션을 수행할 때의 영향은 하나의 트랜잭션에서 순차적으로 수행되는 것과 동일해야 한다. 이 원칙은 각각의 트랜잭션이 원자성(atomic, 트랜잭션의 모든 영향들은 커밋되거나 롤백되어야 함)을 가지고 수행한다면, 데이터베이스의 동시성이 보호된다는 가정에 기초하고 있다. CUBRID에서 직렬성은 잘 알려진
2단계 잠금 기법을 통해 관리된다. 이것은 잠금 프로토콜에서 설명한다.
커밋하고자 하는 트랜잭션은 데이터베이스의 동시성을 보호해야 하고 각각의 트랜잭션은 적합한 결과를 보장해야 한다. 다른 트랜잭션이 수행 중일 때 트랜잭션 내의 이벤트는 다른 트랜잭션에게 보이지 않아야 하며, 이를 격리성(isolation)이라 한다. 트랜잭션의 격리 수준(isolation level)은 동시에 수행되는 다른 트랜잭션으로부터 간섭 받는 것을 허용하는 정도의 단위이다. 격리 수준이 높을수록 간섭을 덜 받게 되고 낮을수록 동시성이 높아진다. 일관성(consistency)과 동시성(concurrency), 이 두 개의 요소는 격리 수준에 의해 조정되며,
적용하고자 하는 서비스에 따라 격리 수준을 결정하여야 한다.
CUBRID가 지원하는 격리 수준은 성능 튜닝 > 데이터베이스 서버 설정 > 동시성/잠금 파라미터를 참조한다.
CUBRID는 정교한 잠금 테크닉을 이용하여 동시 접근을 통제한다. 어떤 유형의 데이터라도 접근에 필요한 잠금은 응용 프로그램의 힌트 없이 자동으로 결정된다(사용자의 개입이 필요 없다). 요청된 연산에 의하여 검색과 갱신 연산을 수행하기 위해, CUBRID는 각각 읽기 잠금과 쓰기 잠금을 획득한다. 데이터베이스 전체를 잠그는 것뿐만 아니라 특정 클래스, 인스턴스, 인덱스를 잠글 수도 있다.
격리 수준은 다음의 세가지 개념의 용어로 설명될 수 있다.
- 더티 읽기(dirty read): 더티 읽기는, 트랜잭션이 객체를 갱신하고 갱신이 커밋되기 전에, 다른 트랜잭션이 이 객체를 검색/갱신하도록 허용되었을 때 발생한다. 이 때 갱신이 커밋되지 않을 수도 있다(트랜잭션이 롤백될 수도 있다). 이 경우에 두 번째 트랜잭션은 실제로는 존재하지 않는 객체의 상태에 접근할지도 모른다. 다음의 예는 더티 읽기를 설명한다.
- 트랜잭션 T1은 객체를 갱신한다. 그리고 트랜잭션 T2는 T1이 커밋되거나 중단되기 전에 그 객체를 읽는다. 만약 T1이 중단되면 T2는 커밋되지 않을 객체의 내용을 보게 된다.
- 트랜잭션 T1은 클래스 C의 인스턴스 O1을 삭제하고 O2를 삽입한다. 그리고 트랜잭션 T2는 클래스 C에 대한 질의를 수행한다. 만약 T1이 중단되면 T2는 커밋되지 않을 O2 인스턴스를 보거나 지워지지 않을 O1을 보지 못하게 된다.
- 두 트랜잭션이 동시에 같은 객체를 갱신한다. 두 트랜잭션은 커밋되지 않은 값에 근거하여 객체로부터 유도된 새로운 값을 가질 수 있다. 만약 한 트랜잭션이 중단되거나 또는 두 트랜잭션이 모두 커밋되면 데이터베이스에 저장되거나 잃어버리게 되는 값이 어떤 것이 될지는 알 수 없다. 이것을 때때로 잃어버린 갱신이라고 말하기도 한다.
- 반복할 수 없는 읽기(Non-repeatable read): 만약 트랜잭션이 한 객체를 여러 번 읽는다면 객체의 다른 값을 읽을 수 있다. 아래의 예제는 이것을 묘사한다.
- 트랜잭션 T1이 한 객체를 읽는다. 그리고 트랜잭션 T2는 그 객체를 갱신(삭제)하고 커밋한다. 만약 T1이 객체를 다시 읽으려고 시도하면 객체의 새로운 내용을 보게 된다. (더 이상 존재하지 않는 객체를 찾을 지도 모른다)
- 트랜잭션 T1은 클래스 C에 대해 질의를 수행한다. 그리고 트랜잭션 T2는 C의 여러 인스턴스를 지우고 삽입한 후 커밋한다. 만약 T1이 같은 질의를 다시 수행하면 삽입되고 지워진 객체들로 인해 다른 결과를 얻을 수 있다.
- 트랜잭션 T1은 클래스 C에 대한 질의를 수행한다. 그리고 트랜잭션 T2는 클래스의 C의 한 속성을 제거하고 커밋한다. 만약 T1이 그 질의를 다시 수행하면 제거된 속성은 보지 못할 수 있다.
- 유령 읽기(Phantom read): 트랜잭션이 하나의 검색 조건으로 여러 번 객체를 검색하는 경우 새롭게 삽입된 객체를 볼 수 있는데 이것은 유령이라 부른다. 다음의 예제는 유령 읽기를 설명한다.
- 트랜잭션 T1은 한 검색 조건으로 객체를 검색한다. 그리고 트랜잭션 T2는 그 검색조건에 맞는 새로운 인스턴스를 삽입하고 커밋한다. 만약 T1이 그 검색조건으로 다시 한번 객체에 대한 읽기를 시도하면 T2에서 삽입된 새로운 객체를 볼 수도 있다.
- 트랜잭션 T1은 한 검색 조건으로 객체를 검색한다. 그리고 트랜잭션 T2는 그 검색조건에 해당하지 않는 객체를 갱신하고 커밋한다. 만약 T1이 동일한 검색조건으로 다시 한번 객체에 대한 읽기를 시도하면 T2에서 갱신된 객체를 볼 수도 있다.
- 트랜잭션 T1은 트랜잭션 T2가 T1의 검색조건에 만족하는 객체들 중에 하나를 삭제한 후에 객체를 검색한다. 그리고 T2가 롤백한다. 만약 T1인 같은 검색 조건으로 다시 검색을 하면 T1은 T2에서 삭제를 시도했던 객체를 볼 수도 있다.