Redis 是一個開源的,高性能的,支持多種數據結構的內存數據庫,已經被普遍用於數據庫,緩存,消息隊列等領域。它有着豐富的數據結構支持,譬如 String,Hash,Set 和 Sorted Set,用戶經過它們能構建本身的高性能應用。git
Redis 很是快,沒準是世界上最快的數據庫了,它雖然使用內存,但也提供了一些持久化機制以及異步複製機制來保證數據的安全。github
Redis 很是酷,但它也有一些問題:redis
因此有時候,咱們須要一個更強大的數據庫,雖然在延遲上面可能趕不上 Redis,但也有足夠多的特性,譬如:數據庫
大約 4 年前,我開始解決上面提到的 Redis 遇到的一些問題。爲了讓數據持久化,最直觀的作法就是將數據保存到硬盤上面,而不是在內存裏面。因此我開發了 LedisDB,一個使用 Redis 協議,提供豐富數據結構,但將數據放在 RocksDB 的數據庫。LedisDB 並非徹底兼容 Redis,因此後來,我和其餘同事繼續建立了 RebornDB,一個徹底兼容 Redis 的數據庫。
不管是 LedisDB 仍是 RebornDB,由於他們都是將數據放在硬盤,因此能存儲更大量的數據。但它們仍然不能提供 ACID 的支持,另外,雖然咱們能夠經過 codis 去提供集羣的支持,咱們也不能很好的支持全局的分佈式事務。緩存
因此咱們須要另外一種方式,幸運的是,咱們有 TiKV。安全
TiKV 是一個高性能,支持分佈式事務的 key-value 數據庫。雖然它僅僅提供了簡單的 key-value API,但基於 key-value,咱們能夠構造本身的邏輯去建立更強大的應用。譬如,咱們就構建了 TiDB ,一個基於 TiKV 的,兼容 MySQL 的分佈式關係型數據庫。TiDB 經過將 database 的 schema 映射到 key-value 來支持了相關 SQL 特性。因此對於 Redis,咱們也能夠採用一樣的辦法 - 構建一個支持 Redis 協議的服務,將 Redis 的數據結構映射到 key-value 上面。數據結構
整個架構很是簡單,咱們僅僅須要作的就是構建一個 Redis 的 Proxy,這個 Proxy 會解析 Redis 協議,而後將 Redis 的數據結構映射到 key-value 上面。架構
Redis 協議被叫作 RESP(Redis Serialization Protocol),它是文本類型的,可讀性比較好,而且易於解析。它使用 「rn」 做爲每行的分隔符而且用不一樣的前綴來表明不一樣的類型。例如,對於簡單的 String,第一個字節是 「+」,因此一個 「OK」 行就是 「+OKrn」。
大多數時候,客戶端會使用最通用的 Request-Response 模型用於跟 Redis 進行交互。客戶端會首先發送一個請求,而後等待 Redis返回結果。請求是一個 Array,Array 裏面元素都是 bulk strings,而返回值則多是任意的 RESP 類型。Redis 一樣支持其餘通信方式:異步
Pipeline - 這種模式下面客戶端會持續的給 Redis 發送多個請求,而後等待 Redis 返回一個結果。
Push - 客戶端會在 Redis 上面訂閱一個 channel,而後客戶端就會從這個 channel 上面持續受到 Redis push 的數據。分佈式
下面是一個簡單的客戶端發送 LLEN mylist
命令到 Redis 的例子:
C: *2\r\n C: $4\r\n C: LLEN\r\n C: $6\r\n C: mylist\r\n S: :48293\r\n
客戶端會發送一個帶有兩個 bulk string 的 array,第一個 bulk string 的長度是 4,而第二個則是 6。Redis 會返回一個 48293 整數。正如你所見,RESP 很是簡單,天然而然的,寫一個 RESP 的解析器也是很是容易的。
做者建立了一個 Go 的庫 goredis,基於這個庫,咱們能很是容易的從鏈接上面解析出 RESP,一個簡單的例子:
// Create a buffer IO from the connection. br := bufio.NewReaderSize(conn, 4096) // Create a RESP reader. r := goredis.NewRespReader(br) // Parse the Request req := r.ParseRequest()
函數 ParseRequest
返回一個解析好的 request,它是一個 [][]byte
類型,第一個字段是函數名字,譬如 「LLEN」,而後後面的字段則是這個命令的參數。
在咱們開始以前,做者將會給一個簡單實用 TiKV 事務 API 的例子,咱們調用 Begin 開始一個事務:
txn, err := db.Begin()
函數 Begin
建立一個事務,若是出錯了,咱們須要判斷 err,不事後面做者都會忽略 err 的處理。
當咱們開始了一個事務以後,咱們就能夠幹不少操做了:
value, err := txn.Get([]byte(「key」)) // Do something with value and then update the newValue to the key. txn.Put([]byte(「key」), newValue)
上面咱們獲得了一個 key 的值,而且將其更新爲新的值。TiKV 使用樂觀事務模型,它會將全部的改動都先緩存到本地,而後在一塊兒提交給 Server。
// Commit the transaction txn.Commit(context.TODO())
跟其餘事務處理同樣,咱們也能夠回滾這個事務:
txn.Rollback()
若是兩個事務操做了相同的 key,它們就會衝突。一個事務會提交成功,而另外一個事務會出錯而且回滾。
如今咱們知道了如何解析 Redis 協議,如何在一個事務裏面作操做,下一步就是支持 Redis 的數據結構了。Redis 主要有 4 中數據結構:String,Hash,Set 和 Sorted Set,可是對於 TiKV 來講,它只支持 key-value,因此咱們須要將這些數據結構映射到 key-value。
首先,咱們須要區分不一樣的數據結構,一個很是容易的方式就是在 key 的後面加上 Type flag。例如,咱們能夠將 ’s’ 添加到 String,因此一個 String key 「abc」 在 TiKV 裏面其實就是 「abcs」。
對於其餘類型,咱們可能須要考慮更多,譬如對於 Hash 類型,咱們須要支持以下操做:
HSET key field1 value1 HSET key field2 value2 HLEN key
一個 Hash 會有不少 fields,我有時候想知道整個 Hash 的個數,因此對於 TiKV,咱們不光須要將 Hash 的 key 和 field 合在一塊兒變成 TiKV 的一個 key,也同時須要用另外一個 key 來保存整個 Hash 的長度,因此整個 Hash 的佈局相似:
key + ‘h’ -> length key + ‘f’ + field1 -> value key + ‘f’ + field2 -> value
若是咱們不保存 length,那麼若是咱們想知道 Hash 的 length,每次都須要去掃整個 Hash 獲得全部的 fields,這個其實並不高效。但若是咱們用另外一個 key 來保存 length,任什麼時候候,當咱們加入一個新的 field,咱們都須要去更新這個 length 的值,這也是一個開銷。對於我來講,我傾向於使用另外一個 key 來保存 length,由於 HLEN
是一個高頻的操做。
做者構建了一個很是簡單的例子 example ,裏面只支持 String 和 Hash 的一些操做,咱們能夠 clone 下來並編譯:
git clone https://github.com/siddontang/redis-tikv-example.git $GOPATH/src/github.com/siddontang/redis-tikv-example cd $GOPATH/src/github.com/siddontang/redis-tikv-example go build
在運行以前,咱們須要啓動 TiKV,能夠參考 instruction,而後執行:
./redis-tikv-example
這個例子會監聽端口 6380,而後咱們能夠用任意的 Redis 客戶端,譬如 redis-cli
去鏈接:
redis-cli -p 6380 127.0.0.1:6380> set k1 a OK 127.0.0.1:6380> get k1 "a" 127.0.0.1:6380> hset k2 f1 a (integer) 1 127.0.0.1:6380> hget k2 f1 "a"
如今已經有一些公司基於 TiKV 來構建了他們本身的 Redis Server,而且也有一個開源的項目 tidis 作了相同的事情。tidis
已經比較完善,若是你想替換本身的 Redis,能夠嘗試一下。
正如同你所見,TiKV 其實算是一個基礎的組件,咱們能夠在它的上面構建不少其餘的應用。若是你對咱們如今作的事情感興趣,歡迎聯繫我:tl@pingcap.com。
做者:唐劉
原文連接: https://www.jianshu.com/p/b4dee8372d8d