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