使用 TiKV 構(gòu)建分布式類 Redis 服務(wù)

什么是 Redis

Redis 是一個開源的,高性能的,支持多種數(shù)據(jù)結(jié)構(gòu)的內(nèi)存數(shù)據(jù)庫,已經(jīng)被廣泛用于數(shù)據(jù)庫,緩存,消息隊列等領(lǐng)域。它有著豐富的數(shù)據(jù)結(jié)構(gòu)支持,譬如 String,Hash,Set 和 Sorted Set,用戶通過它們能構(gòu)建自己的高性能應(yīng)用。

Redis 非??欤瑳]準是世界上最快的數(shù)據(jù)庫了,它雖然使用內(nèi)存,但也提供了一些持久化機制以及異步復(fù)制機制來保證數(shù)據(jù)的安全。

Redis 的不足

Redis 非??幔灿幸恍﹩栴}:

  1. 內(nèi)存很貴,而且并不是無限容量的,所以我們不可能將大量的數(shù)據(jù)存放到一臺機器。
  2. 異步復(fù)制并不能保證 Redis 的數(shù)據(jù)安全。
  3. Redis 提供了 transaction mode,但其實并不滿足 ACID 特性。
  4. Redis 提供了集群支持,但也不能支持跨多個節(jié)點的分布式事務(wù)。

所以有時候,我們需要一個更強大的數(shù)據(jù)庫,雖然在延遲上面可能趕不上 Redis,但也有足夠多的特性,譬如:

  1. 豐富的數(shù)據(jù)結(jié)構(gòu)
  2. 高吞吐,能接受的延遲
  3. 強數(shù)據(jù)一致
  4. 水平擴展
  5. 分布式事務(wù)

為什么選擇 TiKV

大約 4 年前,我開始解決上面提到的 Redis 遇到的一些問題。為了讓數(shù)據(jù)持久化,最直觀的做法就是將數(shù)據(jù)保存到硬盤上面,而不是在內(nèi)存里面。所以我開發(fā)了 LedisDB,一個使用 Redis 協(xié)議,提供豐富數(shù)據(jù)結(jié)構(gòu),但將數(shù)據(jù)放在 RocksDB 的數(shù)據(jù)庫。LedisDB 并不是完全兼容 Redis,所以后來,我和其他同事繼續(xù)創(chuàng)建了 RebornDB,一個完全兼容 Redis 的數(shù)據(jù)庫。

無論是 LedisDB 還是 RebornDB,因為他們都是將數(shù)據(jù)放在硬盤,所以能存儲更大量的數(shù)據(jù)。但它們?nèi)匀徊荒芴峁?ACID 的支持,另外,雖然我們可以通過codis 去提供集群的支持,我們也不能很好的支持全局的分布式事務(wù)。

所以我們需要另一種方式,幸運的是,我們有TiKV。

TiKV 是一個高性能,支持分布式事務(wù)的 key-value 數(shù)據(jù)庫。雖然它僅僅提供了簡單的 key-value API,但基于 key-value,我們可以構(gòu)造自己的邏輯去創(chuàng)建更強大的應(yīng)用。譬如,我們就構(gòu)建了 TiDB ,一個基于 TiKV 的,兼容 MySQL 的分布式關(guān)系型數(shù)據(jù)庫。TiDB 通過將 database 的 schema 映射到 key-value 來支持了相關(guān) SQL 特性。所以對于 Redis,我們也可以采用同樣的辦法 - 構(gòu)建一個支持 Redis 協(xié)議的服務(wù),將 Redis 的數(shù)據(jù)結(jié)構(gòu)映射到 key-value 上面。

如何開始

整個架構(gòu)非常簡單,我們僅僅需要做的就是構(gòu)建一個 Redis 的 Proxy,這個 Proxy 會解析 Redis 協(xié)議,然后將 Redis 的數(shù)據(jù)結(jié)構(gòu)映射到 key-value 上面。

Redis Protocol

Redis 協(xié)議被叫做 RESP(Redis Serialization Protocol),它是文本類型的,可讀性比較好,并且易于解析。它使用 “rn” 作為每行的分隔符并且用不同的前綴來代表不同的類型。例如,對于簡單的 String,第一個字節(jié)是 “+”,所以一個 “OK” 行就是 “+OKrn”。

大多數(shù)時候,客戶端會使用最通用的 Request-Response 模型用于跟 Redis 進行交互??蛻舳藭紫劝l(fā)送一個請求,然后等待 Redis返回結(jié)果。請求是一個 Array,Array 里面元素都是 bulk strings,而返回值則可能是任意的 RESP 類型。Redis 同樣支持其他通訊方式:

  1. Pipeline - 這種模式下面客戶端會持續(xù)的給 Redis 發(fā)送多個請求,然后等待 Redis 返回一個結(jié)果。
  2. Push - 客戶端會在 Redis 上面訂閱一個 channel,然后客戶端就會從這個 channel 上面持續(xù)受到 Redis push 的數(shù)據(jù)。

下面是一個簡單的客戶端發(fā)送 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

客戶端會發(fā)送一個帶有兩個 bulk string 的 array,第一個 bulk string 的長度是 4,而第二個則是 6。Redis 會返回一個 48293 整數(shù)。正如你所見,RESP 非常簡單,自然而然的,寫一個 RESP 的解析器也是非常容易的。

作者創(chuàng)建了一個 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()

函數(shù) ParseRequest 返回一個解析好的 request,它是一個 [][]byte 類型,第一個字段是函數(shù)名字,譬如 “LLEN”,然后后面的字段則是這個命令的參數(shù)。

TiKV 事務(wù) API

在我們開始之前,作者將會給一個簡單實用 TiKV 事務(wù) API 的例子,我們調(diào)用 Begin 開始一個事務(wù):

txn, err := db.Begin()

函數(shù) Begin 創(chuàng)建一個事務(wù),如果出錯了,我們需要判斷 err,不過后面作者都會忽略 err 的處理。

當(dāng)我們開始了一個事務(wù)之后,我們就可以干很多操作了:

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 使用樂觀事務(wù)模型,它會將所有的改動都先緩存到本地,然后在一起提交給 Server。

// Commit the transaction
txn.Commit(context.TODO())

跟其他事務(wù)處理一樣,我們也可以回滾這個事務(wù):

txn.Rollback()

如果兩個事務(wù)操作了相同的 key,它們就會沖突。一個事務(wù)會提交成功,而另一個事務(wù)會出錯并且回滾。

映射 Data structure 到 TiKV

現(xiàn)在我們知道了如何解析 Redis 協(xié)議,如何在一個事務(wù)里面做操作,下一步就是支持 Redis 的數(shù)據(jù)結(jié)構(gòu)了。Redis 主要有 4 中數(shù)據(jù)結(jié)構(gòu):String,Hash,Set 和 Sorted Set,但是對于 TiKV 來說,它只支持 key-value,所以我們需要將這些數(shù)據(jù)結(jié)構(gòu)映射到 key-value。

首先,我們需要區(qū)分不同的數(shù)據(jù)結(jié)構(gòu),一個非常容易的方式就是在 key 的后面加上 Type flag。例如,我們可以將 ’s’ 添加到 String,所以一個 String key “abc” 在 TiKV 里面其實就是 “abcs”。

對于其他類型,我們可能需要考慮更多,譬如對于 Hash 類型,我們需要支持如下操作:

HSET key field1 value1
HSET key field2 value2
HLEN key

一個 Hash 會有很多 fields,我有時候想知道整個 Hash 的個數(shù),所以對于 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,任何時候,當(dāng)我們加入一個新的 field,我們都需要去更新這個 length 的值,這也是一個開銷。對于我來說,我傾向于使用另一個 key 來保存 length,因為 HLEN 是一個高頻的操作。

例子

作者構(gòu)建了一個非常簡單的例子 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,然后執(zhí)行:

./redis-tikv-example

這個例子會監(jiān)聽端口 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"

尾聲

現(xiàn)在已經(jīng)有一些公司基于 TiKV 來構(gòu)建了他們自己的 Redis Server,并且也有一個開源的項目tidis 做了相同的事情。tidis 已經(jīng)比較完善,如果你想替換自己的 Redis,可以嘗試一下。

正如同你所見,TiKV 其實算是一個基礎(chǔ)的組件,我們可以在它的上面構(gòu)建很多其他的應(yīng)用。如果你對我們現(xiàn)在做的事情感興趣,歡迎聯(lián)系我:tl@pingcap.com。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容