TiKV 的 MVCC(Multi-Version Concurrency Control)機制

qiuyesuifeng發表於2016-11-24

併發控制簡介

事務隔離在資料庫系統中有著非常重要的作用,因為對於使用者來說資料庫必須提供這樣一個“假象”:當前只有這麼一個使用者連線到了資料庫中,這樣可以減輕應用層的開發難度。但是,對於資料庫系統來說,因為同一時間可能會存在很多使用者連線,那麼許多併發問題,比如資料競爭(data race),就必須解決。在這樣的背景下,資料庫管理系統(簡稱 DBMS)就必須保證併發操作產生的結果是安全的,通過可序列化(serializability)來保證。

雖然 Serilizability 是一個非常棒的概念,但是很難能夠有效的實現。一個經典的方法就是使用一種兩段鎖(2PL)。通過 2PL,DBMS 可以維護讀寫鎖來保證可能產生衝突的事務按照一個良好的次序(well-defined) 執行,這樣就可以保證 Serializability。但是,這種通過鎖的方式也有一些缺點:

  1. 讀鎖和寫鎖會相互阻滯(block)。
  2. 大部分事務都是隻讀(read-only)的,所以從事務序列(transaction-ordering)的角度來看是無害的。如果使用基於鎖的隔離機制,而且如果有一段很長的讀事務的話,在這段時間內這個物件就無法被改寫,後面的事務就會被阻塞直到這個事務完成。這種機制對於併發效能來說影響很大。

多版本併發控制(Multi-Version Concurrency Control, 以下簡稱 MVCC)以一種優雅的方式來解決這個問題。在 MVCC 中,每當想要更改或者刪除某個資料物件時,DBMS 不會在原地去刪除或這修改這個已有的資料物件本身,而是建立一個該資料物件的新的版本,這樣的話同時併發的讀取操作仍舊可以讀取老版本的資料,而寫操作就可以同時進行。這個模式的好處在於,可以讓讀取操作不再阻塞,事實上根本就不需要鎖。這是一種非常誘人的特型,以至於在很多主流的資料庫中都採用了 MVCC 的實現,比如說 PostgreSQL,Oracle,Microsoft SQL Server 等。

TiKV 中的 MVCC

讓我們深入到 TiKV 中的 MVCC,瞭解 MVCC 在 TiKV 中是如何實現的。

Timestamp Oracle(TSO)

因為TiKV 是一個分散式的儲存系統,它需要一個全球性的授時服務,下文都稱作 TSO(Timestamp Oracle),來分配一個單調遞增的時間戳。 這樣的功能在 TiKV 中是由 PD 提供的,在 Google 的 Spanner 中是由多個原子鐘和 GPS 來提供的。

Storage

從原始碼結構上來看,想要深入理解 TiKV 中的 MVCC 部分,src/storage 是一個非常好的入手點。 Storage 是實際上接受外部命令的結構體。

pub struct Storage {
    engine: Box<Engine>,
    sendch: SendCh<Msg>,
    handle: Arc<Mutex<StorageHandle>>,
}

impl Storage {
    pub fn start(&mut self, config: &Config) -> Result<()> {
        let mut handle = self.handle.lock().unwrap();
        if handle.handle.is_some() {
            return Err(box_err!("scheduler is already running"));
        }

        let engine = self.engine.clone();
        let builder = thread::Builder::new().name(thd_name!("storage-scheduler"));
        let mut el = handle.event_loop.take().unwrap();
        let sched_concurrency = config.sched_concurrency;
        let sched_worker_pool_size = config.sched_worker_pool_size;
        let sched_too_busy_threshold = config.sched_too_busy_threshold;
        let ch = self.sendch.clone();
        let h = try!(builder.spawn(move || {
            let mut sched = Scheduler::new(engine,
                                           ch,
                                           sched_concurrency,
                                           sched_worker_pool_size,
                                           sched_too_busy_threshold);
            if let Err(e) = el.run(&mut sched) {
                panic!("scheduler run err:{:?}", e);
            }
            info!("scheduler stopped");
        }));
        handle.handle = Some(h);

        Ok(())
    }
}

start 這個函式很好的解釋了一個 storage 是怎麼跑起來的。

Engine

首先是 EngineEngine 是一個描述了在儲存系統中接入的的實際上的資料庫的介面,raftkvEnginerocksdb 分別實現了這個介面。

StorageHandle

StorageHanle 是處理從sench 接受到指令,通過 mio 來處理 IO。

接下來在Storage中實現了async_getasync_batch_get等非同步函式,這些函式中將對應的指令送到通道中,然後被排程器(scheduler)接收到並非同步執行。

Ok,瞭解完Storage 結構體是如何實現的之後,我們終於可以接觸到在Scheduler 被呼叫的 MVCC 層了。

當 storage 接收到從客戶端來的指令後會將其傳送到排程器中。然後排程器執行相應的過程或者呼叫相應的非同步函式。在排程器中有兩種操作型別,讀和寫。讀操作在 MvccReader 中實現,這一部分很容易理解,暫且不表。寫操作的部分是MVCC的核心。

MVCC

Ok,兩段提交(2-Phase Commit,2PC)是在 MVCC 中實現的,整個 TiKV 事務模型的核心。在一段事務中,由兩個階段組成。

Prewrite

選擇一個 row 作為 primary row, 餘下的作為 secondary row。 對primary row 上鎖. 在上鎖之前,會檢查是否有其他同步的鎖已經上到了這個 row 上 或者是是否經有在 startTS 之後的提交操作。這兩種情況都會導致衝突,一旦都衝突發生,就會回滾(rollback)。 對於 secondary row 重複以上操作。

Commit

RollbackPrewrite 過程中出現衝突的話就會被呼叫。

Garbage Collector

很容易發現,如果沒有垃圾收集器(Gabage Collector) 來移除無效的版本的話,資料庫中就會存有越來越多的 MVCC 版本。但是我們又不能僅僅移除某個 safe point 之前的所有版本。因為對於某個 key 來說,有可能只存在一個版本,那麼這個版本就必須被儲存下來。在TiKV中,如果在 safe point 前存在Put 或者Delete,那麼說明之後所有的 writes 都是可以被移除的,不然的話只有DeleteRollbackLock 會被刪除。

TiKV-Ctl for MVCC

在開發和 debug 的過程中,我們發現查詢 MVCC 的版本資訊是一件非常頻繁並且重要的操作。因此我們開發了新的工具來查詢 MVCC 資訊。TiKV 將 Key-Value,Locks 和Writes 分別儲存在CF_DEFAULTCF_LOCKCF_WRITE中。它們以這樣的格式進行編碼

default lock write
key z{encoded_key}{start_ts(desc)} z{encoded_key} z{encoded_key}{commit_ts(desc)}
value {value} {flag}{primary_key}{start_ts(varint)} {flag}{start_ts(varint)}

Details can be found here.

因為所有的 MVCC 資訊在 Rocksdb 中都是儲存在 CF Key-Value 中,所以想要查詢一個 Key 的版本資訊,我們只需要將這些資訊以不同的方式編碼,隨後在對應的 CF 中查詢即可。CF Key-Values 的表示形式

原文連結

相關文章