MonetDB의 트랜잭션 격리 수준 특성
MonetDB는 MVCC(Multi-Version Concurrency Control) 아키텍처를 기반으로 동작하지만, MySQL의 InnoDB 스토리지 엔진이 Read Committed(RC)와 Repeatable Read(RR)를 비롯한 다양한 격리 수준을 세분화하여 제공하는 것과 달리, 기본적으로 RR(Repeatable Read) 격리 수준만을 엄격하게 강제합니다. 이 글에서는 MonetDB의 소스 코드를 통해 RR 격리 수준이 어떻게 구현되어 있는지, 그리고 데이터의 가시성(Visibility)을 판별하는 핵심 로직을 분석합니다.
RR 격리 수준 동작 검증 시나리오
격리 수준의 동작 방식을 확인하기 위해 두 개의 독립적인 클라이언트 세션을 사용하여 동시성 테스트를 수행할 수 있습니다.
- 세션 A (Auto-commit 활성화): 지속적으로 새로운 레코드를 삽입합니다. (예:
INSERT INTO target_table VALUES (3);) - 세션 B (명시적 트랜잭션): 트랜잭션을 시작하고 레코드 수를 반복해서 조회합니다. (예:
START TRANSACTION; SELECT COUNT(*) FROM target_table;)
이 테스트를 수행하면 세션 A가 새로운 데이터를 삽입하고 자동으로 커밋하더라도, 세션 B는 자신이 트랜잭션을 시작한 시점의 데이터 스냅샷만 계속 읽게 됩니다. 즉, 세션 B의 COUNT 쿼리 결과는 세션 A의 커밋과 무관하게 일정하게 유지되며, 이는 전형적인 Repeatable Read(Snapshot Isolation)의 특징입니다.
내부 소스 코드 추적 및 가시성 로직 분석
세션 B가 왜 세션 A의 변경 사항을 감지하지 못하는지 이해하려면, SELECT COUNT(*) 쿼리가 실행될 때 MonetDB 엔진 내부에서 데이터 세그먼트를 필터링하는 과정을 추적해야 합니다.
1. 실행 경로 (Execution Path)
로그와 콜 스택을 분석하면 집계 함수가 호출될 때 다음과 같은 경로를 따르는 것을 확인할 수 있습니다. 쿼리는 MAL(MonetDB Assembly Language) 인터프리터를 거치며, sql.count 함수를 호출한 후 최종적으로 스토리지 레이어의 세그먼트 종료 지점을 계산하는 함수에 도달합니다.
runMALsequenceSQLbasecountcount_colcalculate_visible_segment_end(원본segs_end에 해당)
2. 핵심 데이터 구조 재구성
데이터의 가시성을 판단하기 위해 MonetDB는 트랜잭션 컨텍스트와 데이터 세그먼트 메타데이터를 활용합니다. 가독성과 구조적 명확성을 위해 핵심 구조체를 다음과 같이 재정의하여 분석할 수 있습니다.
/* 트랜잭션 상태 및 메타데이터를 관리하는 컨텍스트 */
typedef struct txn_context_t {
const char *txn_name;
uint64_t start_ts; /* 트랜잭션 시작 시점의 타임스탬프 */
uint64_t id; /* 고유 트랜잭션 식별자 */
storage_engine_t *storage;
thread_lock_t mtx; /* 동시성 제어를 위한 락 */
list_t *modified_items; /* 변경된 데이터 목록 */
list_t *deleted_items;
list_t *read_predicates; /* 업데이트 시 기록된 읽기 조건자 */
list_t *dependencies; /* 종속성 객체 ID 목록 */
list_t *modified_deps; /* 충돌 감지를 위한 변경된 종속성 */
int64_t wal_change_count; /* WAL에 기록될 변경 수 */
int is_active;
int last_status;
catalog_t *sys_catalog;
schema_t *temp_schema;
changeset_t local_temp;
memory_allocator_t *alloc;
struct txn_context_t *parent_txn; /* 중첩 트랜잭션 지원을 위한 포인터 */
} txn_context_t;
/* 컬럼 데이터를 저장하는 물리적 세그먼트 단위 */
typedef struct segment_node_t {
uint64_t start_offset;
uint64_t end_offset;
bool is_removed; /* 세그먼트 삭제 여부 플래그 */
uint64_t commit_ts; /* 세그먼트가 생성되거나 수정된 트랜잭션의 커밋 타임스탬프 */
uint64_t prev_commit_ts; /* 롤백 처리를 위해 보존하는 이전 타임스탬프 */
struct segment_node_t *next_node;
struct segment_node_t *prev_node;
} segment_node_t;
3. 세그먼트 가시성 판별 로직
집계 연산을 수행할 때 엔진은 연결 리스트 형태로 구성된 세그먼트들을 순회하며 현재 트랜잭션이 읽을 수 있는 유효한 구간을 탐색합니다. 이 과정은 세그먼트의 삭제 여부와 타임스탬프 비교를 통해 이루어집니다.
/* 세그먼트가 현재 트랜잭션 관점에서 읽기 가능한지 평가 */
static inline bool is_segment_visible(segment_node_t *seg, txn_context_t *txn) {
// 삭제되지 않았고, 읽기 유효성 검사를 통과한 경우
if (!seg->is_removed && check_read_validity(seg->commit_ts, txn)) {
return true;
}
// 다른 트랜잭션에 의해 삭제되었으나, 과거 시점에는 유효했던 데이터인 경우
if (seg->is_removed && check_old_read_validity(seg->commit_ts, seg->prev_commit_ts, txn)) {
return true;
}
return false;
}
/* 주어진 세그먼트를 순회하며 가시성이 보장되는 마지막 지점을 탐색 */
static size_t calculate_visible_segment_end(segment_list_t *list, txn_context_t *txn, table_meta_t *tbl) {
size_t visible_count = 0;
acquire_table_lock(txn->storage, tbl->id);
segment_node_t *curr = list->head;
segment_node_t *last_valid = NULL;
if (list->tail && is_segment_visible(list->tail, txn)) {
last_valid = curr = list->tail;
}
while (curr != NULL) {
if (is_segment_visible(curr, txn)) {
last_valid = curr;
}
curr = curr->next_node;
}
if (last_valid != NULL) {
visible_count = last_valid->end_offset;
}
release_table_lock(txn->storage, tbl->id);
return visible_count;
}
4. Repeatable Read를 강제하는 핵심 조건
가시성 판단의 가장 중요한 기준은 check_read_validity 함수 내에 존재합니다. 이 함수는 세그먼트의 타임스탬프와 트랜잭션의 타임스탬프를 비교합니다.
static inline bool check_read_validity(uint64_t seg_ts, txn_context_t *txn) {
// 1. 현재 트랜잭션 자신이 생성한 데이터인 경우
if (seg_ts == txn->id) return true;
// 2. 중첩 트랜잭션 환경에서 부모 트랜잭션의 버전인 경우
if (txn->parent_txn && is_parent_version_valid(txn, seg_ts)) return true;
// 3. [핵심] 세그먼트의 커밋 시점이 트랜잭션 시작 시점보다 과거인 경우
if (seg_ts < txn->start_ts) return true;
return false;
}
위 로직의 세 번째 조건(seg_ts < txn->start_ts)이 MonetDB에서 RR 격리 수준이 발생하는 근본적인 원인입니다. seg_ts는 데이터가 디스크에 확정적으로 기록된(커밋된) 시간이며, txn->start_ts는 읽기 트랜잭션이 시작된 시간입니다.
엔진은 트랜잭션이 시작된 이후에 커밋된 어떠한 세그먼트(seg_ts >= txn->start_ts)에도 접근을 허용하지 않습니다. 이로 인해 트랜잭션 수행 도중 다른 세션에서 커밋한 데이터는 철저히 차단되며, 트랜잭션 수명 주기 동안 동일한 스냅샷을 반복해서 읽게 되는 Repeatable Read 현상이 구현됩니다.
Read Committed(RC) 격리 수준 확장을 위한 접근 방식
현재 MonetDB의 아키텍처에 RC 격리 수준을 도입하려면, 기존 RR 로직을 제거하는 것이 아니라 가시성 평가 기준을 동적으로 전환할 수 있어야 합니다.
RC 환경에서는 트랜잭션이 시작된 시점(txn->start_ts)이 아닌, '쿼리 문장이 실행되는 현재 시점' 이전에 커밋된 데이터를 모두 볼 수 있어야 합니다. 따라서 다음과 같은 수정이 필요합니다.
- 격리 수준 설정 파라미터 추가: MySQL의
transaction_isolation시스템 변수와 유사하게, 세션 단위로 RR 또는 RC 모드를 지정할 수 있는 설정 값을txn_context_t구조체에 추가합니다. - 가시성 검사 로직 분기:
check_read_validity함수에서 트랜잭션 컨텍스트의 격리 수준 설정을 확인합니다. RC로 설정된 경우, 기준 타임스탬프를txn->start_ts대신 전역으로 관리되는 최신 커밋 타임스탬프(Global Commit Timestamp)나 현재 명령어의 실행 타임스탬프와 비교하도록 로직을 분기해야 합니다. - 스냅샷 갱신 허용: RC 모드에서는 단일 쿼리 실행 시마다 가시성 기준선이 업데이트되므로, 세그먼트 순회 로직이 매번 최신 커밋 상태를 반영할 수 있도록 락 관리 및 캐시 무효화 전략을 보완해야 합니다.