{
    "componentChunkName": "component---src-templates-blog-blog-detail-tsx",
    "path": "/blog/mvcc-in-tikv",
    "result": {"pageContext":{"blog":{"id":"Blogs_17","title":"TiKV 的 MVCC（Multi-Version Concurrency Control）机制","tags":["TiKV","可串行化","多版本并发控制","2PC","隔离级别"],"category":{"name":"产品技术解读"},"summary":"事务隔离在数据库系统中有着非常重要的作用，因为对于用户来说数据库必须提供这样一个“假象”：当前只有这么一个用户连接到了数据库中，这样可以减轻应用层的开发难度。但是，对于数据库系统来说，因为同一时间可能会存在很多用户连接，那么许多并发问题，比如数据竞争（data race），就必须解决。在这样的背景下，数据库管理系统（简称 DBMS）就必须保证并发操作产生的结果是安全的，通过可串行化（serializability）来保证。","body":"### 并发控制简介\n事务隔离在数据库系统中有着非常重要的作用，因为对于用户来说数据库必须提供这样一个“假象”：当前只有这么一个用户连接到了数据库中，这样可以减轻应用层的开发难度。但是，对于数据库系统来说，因为同一时间可能会存在很多用户连接，那么许多并发问题，比如数据竞争（data race），就必须解决。在这样的背景下，数据库管理系统（简称 DBMS）就必须保证并发操作产生的结果是安全的，通过可串行化（serializability）来保证。\n\n\n虽然 Serilizability 是一个非常棒的概念，但是很难能够有效的实现。一个经典的方法就是使用一种[两段锁（2PL）][1]。通过 2PL，DBMS 可以维护读写锁来保证可能产生冲突的事务按照一个良好的次序（well-defined) 执行，这样就可以保证 Serializability。但是，这种通过锁的方式也有一些缺点：\n\n1. 读锁和写锁会相互阻滞（block）。\n2. 大部分事务都是只读（read-only）的，所以从事务序列（transaction-ordering）的角度来看是无害的。如果使用基于锁的隔离机制，而且如果有一段很长的读事务的话，在这段时间内这个对象就无法被改写，后面的事务就会被阻塞直到这个事务完成。这种机制对于并发性能来说影响很大。\n\n**多版本并发控制（Multi-Version Concurrency Control，以下简称 MVCC）** 以一种优雅的方式来解决这个问题。在 MVCC 中，每当想要更改或者删除某个数据对象时，DBMS 不会在原地去删除或这修改这个已有的数据对象本身，而是创建一个该数据对象的新的版本，这样的话同时并发的读取操作仍旧可以读取老版本的数据，而写操作就可以同时进行。这个模式的好处在于，可以让读取操作不再阻塞，事实上根本就不需要锁。这是一种非常诱人的特型，以至于在很多主流的数据库中都采用了 MVCC 的实现，比如说 PostgreSQL，Oracle，Microsoft SQL Server 等。\n\n\n### TiKV 中的 MVCC\n让我们深入到 TiKV 中的 MVCC，了解 MVCC 在 TiKV 中是如何 [实现](https://github.com/pingcap/tikv/tree/master/src/storage) 的。\n\n\n#### 1. Timestamp Oracle(TSO)\n因为`TiKV` 是一个分布式的储存系统，它需要一个全球性的授时服务，下文都称作 TSO（Timestamp Oracle），来分配一个单调递增的时间戳。 这样的功能在 TiKV 中是由 PD 提供的，在 Google 的 [Spanner](http://static.googleusercontent.com/media/research.google.com/en//archive/spanner-osdi2012.pdf) 中是由多个原子钟和 GPS 来提供的。\n\n\n#### 2. Storage\n从源码结构上来看，想要深入理解 TiKV 中的 MVCC 部分，[src/storage](https://github.com/tikv/tikv/blob/1050931de5d9b47423f997d6fc456bd05bd234a7/src/storage/mod.rs) 是一个非常好的入手点。 `Storage` 是实际上接受外部命令的结构体。\n\n```rust\npub struct Storage {\n    engine: Box<Engine>,\n    sendch: SendCh<Msg>,\n    handle: Arc<Mutex<StorageHandle>>,\n}\n\n\n\n\nimpl Storage {\n    pub fn start(&mut self, config: &Config) -> Result<()> {\n        let mut handle = self.handle.lock().unwrap();\n        if handle.handle.is_some() {\n            return Err(box_err!(\"scheduler is already running\"));\n        }\n\n\n\n\n        let engine = self.engine.clone();\n        let builder = thread::Builder::new().name(thd_name!(\"storage-scheduler\"));\n        let mut el = handle.event_loop.take().unwrap();\n        let sched_concurrency = config.sched_concurrency;\n        let sched_worker_pool_size = config.sched_worker_pool_size;\n        let sched_too_busy_threshold = config.sched_too_busy_threshold;\n        let ch = self.sendch.clone();\n        let h = try!(builder.spawn(move || {\n            let mut sched = Scheduler::new(engine,\n                                           ch,\n                                           sched_concurrency,\n                                           sched_worker_pool_size,\n                                           sched_too_busy_threshold);\n            if let Err(e) = el.run(&mut sched) {\n                panic!(\"scheduler run err:{:?}\", e);\n            }\n            info!(\"scheduler stopped\");\n        }));\n        handle.handle = Some(h);\n\n\n\n\n        Ok(())\n    }\n}\n\n```\n\n`start` 这个函数很好的解释了一个 storage 是怎么跑起来的。\n\n\n#### 3. Engine\n首先是 [Engine](https://github.com/tikv/tikv/blob/1050931de5d9b47423f997d6fc456bd05bd234a7/src/storage/engine/mod.rs#L44)。 `Engine` 是一个描述了在储存系统中接入的的实际上的数据库的接口，[raftkv](https://github.com/tikv/tikv/blob/1050931de5d9b47423f997d6fc456bd05bd234a7/src/storage/engine/raftkv.rs#L91) 和 [Enginerocksdb](https://github.com/tikv/tikv/blob/1050931de5d9b47423f997d6fc456bd05bd234a7/src/storage/engine/rocksdb.rs#L66) 分别实现了这个接口。\n\n\n#### 4. StorageHandle\n`StorageHanle` 是处理从`sendch` 接受到指令，通过 [mio](https://github.com/carllerche/mio) 来处理 IO。\n\n\n接下来在`Storage`中实现了`async_get` 和`async_batch_get`等异步函数，这些函数中将对应的指令送到通道中，然后被调度器（scheduler）接收到并异步执行。\n\n\nOk，了解完`Storage` 结构体是如何实现的之后，我们终于可以接触到在`Scheduler` [被调用的 MVCC 层](https://github.com/tikv/tikv/blob/1050931de5d9b47423f997d6fc456bd05bd234a7/src/storage/txn/scheduler.rs#L763)了。\n\n\n当 storage 接收到从客户端来的指令后会将其传送到调度器中。然后调度器执行相应的过程或者调用相应的[异步函数](https://github.com/tikv/tikv/blob/1050931de5d9b47423f997d6fc456bd05bd234a7/src/storage/txn/scheduler.rs#L643)。在调度器中有两种操作类型，读和写。读操作在 [MvccReader](https://github.com/tikv/tikv/blob/1050931de5d9b47423f997d6fc456bd05bd234a7/src/storage/mvcc/reader.rs#L20) 中实现，这一部分很容易理解，暂且不表。写操作的部分是MVCC的核心。\n\n\n#### 5. MVCC\nOk，两段提交（2-Phase Commit，2PC）是在 MVCC 中实现的，整个 TiKV 事务模型的核心。在一段事务中，由两个阶段组成。\n\n\n##### Prewrite\n选择一个 row 作为 primary row， 余下的作为 secondary row。\n对primary row [上锁](https://github.com/tikv/tikv/blob/1050931de5d9b47423f997d6fc456bd05bd234a7/src/storage/mvcc/txn.rs#L80). 在上锁之前，会检查[是否有其他同步的锁已经上到了这个 row 上](https://github.com/tikv/tikv/blob/1050931de5d9b47423f997d6fc456bd05bd234a7/src/storage/mvcc/txn.rs#L71) 或者是是否经有在 startTS 之后的提交操作。这两种情况都会导致冲突，一旦都冲突发生，就会[回滚（rollback）](https://github.com/tikv/tikv/blob/1050931de5d9b47423f997d6fc456bd05bd234a7/src/storage/mvcc/txn.rs#L115)。\n对于 secondary row 重复以上操作。\n\n\n##### Commit\n[Rollback](https://github.com/tikv/tikv/blob/1050931de5d9b47423f997d6fc456bd05bd234a7/src/storage/mvcc/txn.rs#L115) 在`Prewrite` 过程中出现冲突的话就会被调用。\n\n\n##### Garbage Collector\n很容易发现，如果没有[垃圾收集器（Gabage Collector）](https://github.com/tikv/tikv/blob/1050931de5d9b47423f997d6fc456bd05bd234a7/src/storage/mvcc/txn.rs#L143) 来移除无效的版本的话，数据库中就会存有越来越多的 MVCC 版本。但是我们又不能仅仅移除某个 safe point 之前的所有版本。因为对于某个 key 来说，有可能只存在一个版本，那么这个版本就必须被保存下来。在`TiKV`中，如果在 safe point 前存在 `Put` 或者 `Delete` 记录，那么比这条记录更旧的写入记录都是可以被移除的，不然的话只有`Delete`，`Rollback`和`Lock` 会被删除。\n\n\n\n## TiKV-Ctl for MVCC\n在开发和 debug 的过程中，我们发现查询 MVCC 的版本信息是一件非常频繁并且重要的操作。因此我们开发了新的工具来查询 MVCC 信息。`TiKV` 将 Key-Value，Locks 和 Writes 分别储存在`CF_DEFAULT`，`CF_LOCK`，`CF_WRITE`中。它们以这样的格式进行编码\n\n\n|           | default                        | lock                                  | write                           |\n|:----------|:-------------------------------|:--------------------------------------|:--------------------------------|\n| **key**   | z{encoded_key}{start_ts(desc)} | z{encoded_key}                        | z{encoded_key}{commit_ts(desc)} |\n| **value** | {value}                        | {flag}{primary_key}{start_ts(varint)} | {flag}{start_ts(varint)}        |\n\nDetails can be found [here](https://github.com/pingcap/tikv/issues/1077).\n\n\n因为所有的 MVCC 信息在 Rocksdb 中都是储存在 CF Key-Value 中，所以想要查询一个 Key 的版本信息，我们只需要将这些信息以不同的方式编码，随后在对应的 CF 中查询即可。CF Key-Values 的 [表示形式](https://github.com/tikv/tikv/blob/1050931de5d9b47423f997d6fc456bd05bd234a7/src/bin/tikv-ctl.rs#L210)。\n\n[1]: https://en.wikipedia.org/wiki/Two-phase_locking","date":"2016-11-22","author":"ivan yang","fillInMethod":"writeDirectly","customUrl":"mvcc-in-tikv","file":null,"relatedBlogs":[]}}},
    "staticQueryHashes": ["1327623483","1820662718","3081853212","3430003955","3649515864","4265596160","63159454"]}