浅谈Redis通信协议

Redis客户端和服务器端使用的通信协议叫做RESP(Redis Serialization Protocol)。它是特意为Redis设计的,同时也可以用于其他软件工程。

RESP在以下事项之间进行妥协:

  • 实现简单
  • 快速解析
  • 可读性强

RESP可以序列化多种不同的数据类型,比如:整型、字符串、数组。错误是一种特定的类型。Redis客户端把参数用数组来表示。回复的是一种特殊的数据格式。

RESP是二进制安全的,它不需要处理从一个进程到另一个进程的批量数据,因为采用的是前缀长度来传输批量数据。

注意:这里的协议只适用用与客户端-服务器通信。Redis集群使用的是不同的协议

一般情况下,RESP是一种简单的请求-响应式协议。二般情况是:

  • Redis支持管道,所以有可能一次发送多个命令,然后一起响应
  • 如果Redis客户端订阅了Pub/Sub频道,那么协议就会变成一种推送协议,当服务器接收到新的数据时会自动推送给客户端

RESP协议支持的数据类型有:Simple Strings,Errors,Integers,Bulk Strings和Arrays。它的使用方法有:

  • 客户端以Bulk Strings数组的形式发送命令
  • 服务器端返回的结果是协议支持的类型之一

RESP协议中,上述类型是通过首个字节区分的:

  • +代表简单字符串(Simple Strings)
  • -代表错误类型(Errors)
  • :代表整型(Integers)
  • $代表多行字符串(Bulk Strings)
  • *代表数组(Arrays)

此外,每一部分结束时,Redis统一使用“rn”表示结束。

看到这里你是否有疑问呢?为什么没有表示null的方法呢?别着急我们一会就会解释。

RESP简单字符串

简单字符串中不允许出现rn,只能有一行。它用于以最小开销传输非二进制安全字符串,例如回复的OK

1"+OKrn"

如果要发送二进制安全的字符串,应该使用多行字符串。

RESP错误

RESP有特定的错误类型,它与简单字符串类似,只不过是把开头的+换成了-,而两者之间真正的区别是客户端将错误视为异常,而错误中的字符串只是表示错误信息。

1"-Error messagern"

当客户端收到错误信息时,通常会抛出一个异常。我们来看一些例子:

1-ERR unknown command 'foobar'
2-WRONGTYPE Operation against a key holding the wrong kind of value

从第一个字符“-”之后,到第一个空格或新的一行,这之间的字符串表示错误类型。这只是Redis的一种约定,并不是RESP的错误格式。

例如ERR是普通错误,而WRONGTYPE表示客户端试图对错误的数据类型执行操作。

RESP整型

整型只是以rn结尾,以:开头的纯整数的字符串。

1:1000rn

很多Redis命令都会返回整型,例如INCR、LLEN和LASTSAVE。

返回的整数需要在64位有符号整数范围内,同时也可以用于表示真或假。

RESP多行字符串

多行字符串是二进制安全的,最大长度是512MB。

多行字符串的编码方式如下:

  • $+数字开头,以rn结束
  • 数据都是字符串
  • 结尾是rn

所以“foobar”应该编码为

1"$6rnfoobarrn"

空字符串表示为:

1"$0rnrn"

多行字符串也可以用来null

1"$-1rn"

当服务器返回Null多行字符串时,正常客户端是不应该返回空字符串的,而是应该返回nil对象。

RESP数组

客户端向服务器端发送命令时使用的就是RESP数组。类似的,某些命令返回的元素集合也是RESP数组的类型。

RESP数组遵循以下规则:

  • 第一个字符是*,后面跟的十进制数字是数组元素的数量,然后跟着rn
  • 每个元素都是RESP类型的

空数组表示为:

1"*0rn"

数组中的元素可以是不同类型的:

1*5rn
2:1rn
3:2rn
4:3rn
5:4rn
6$6rn
7foobarrn

第一行的*5rn表示数组有5个元素,后面每行是一个元素。

RESP也有NULL数组的表示方法,这是NULL的另一种表示方法,通常用多行字符串的NULL来表示,不过由于历史原因,就保留了两种形式。

当BLPOP命令超时时,就会返回NULL数组

1"*-1rn"

当服务器返回NULL数组时,客户端应该返回null对象而不是空数组。

数组中的NULL

数组中的元素可以是NULL,通常表示数组中某个元素缺失,而不是空字符串:

1*3rn
2$3rn
3foorn
4$-1rn
5$3rn
6barrn

其中第二个元素时NULL,客户端的返回结果应该是:

1["foo",nil,"bar"]
小结

到此我们已经了解了RESP协议,RESP中虽然有大量的冗余rn,但是仍然有很多开源项目使用。

Redis 选择hash还是string 存储数据?

在stackoverflow 看到一个问题,Redis strings vs Redis hashes to represent JSON: efficiency?内容如下:

I want to store a JSON payload into redis. There’s really 2 ways I can do this:

  1. One using a simple string keys and values.

    key:user, value:payload (the entire JSON blob which can be 100-200 KB)

    SET user:1 payload

  2. Using hashes

    HSET user:1 username “someone”HSET user:1 location “NY”HSET user:1 bio “STRING WITH OVER 100 lines”

Keep in mind that if I use a hash, the value length isn’t predictable. They’re not all short such as the bio example above.Which is more memory efficient? Using string keys and values, or using a hash?

string 和 hash 直观测试

首先我们先测试用数据测试一下,测试数据结构如下:

values = {
    "name": "gs",
    "age": 1
}

使用for 生成10w个key,key的生成规则为:

for i in range(100000):
    key = "object:%d" % i

把数据分别以hash 和 string(values 使用 json encode 为string )的形式存入redis。

结果如下:

hash 占用 10.16M

string 占用 10.15M

这看起来和我们印象中hash 占空间比较大的观念不太一致,这是为什么呢?

这里是因为Redis 的hash 对象有两种编码方式:

  1. ziplist(2.6之前是zipmap)
  2. hashtable

当哈希对象可以同时满足以下两个条件时, 哈希对象使用 ziplist 编码:

  1. 哈希对象保存的所有键值对的键和值的字符串长度都小于 64 字节;
  2. 哈希对象保存的键值对数量小于 512 个;

不能满足这两个条件的哈希对象需要使用 hashtable 编码。上述测试数据满足这两个条件,所以这里使用的是ziplist来存储的数据,而不是hashtable。

注意这两个条件的上限值是可以修改的, 具体请看配置文件中关于 hash-max-ziplist-value 选项和 hash-max-ziplist-entries 选项的说明。

hash-max-ziplist-entries for Redis >= 2.6hash-max-ziplist-value for Redis >= 2.6

ziplist

ziplist 编码的数据底层是使用压缩列表作为底层数据结构,结构如下:

Redis 选择hash还是string 存储数据?

Redis 选择hash还是string 存储数据?

hash 对象使用ziplist 保存时,程序会将保存了键的ziplist节点推入到列表的表尾,然后再将保存了值的ziplist节点推入列表的表尾。

使用这种方式保存时,并不需要申请多余的内存空间,而且每个Key都要存储一些关联的系统信息(如过期时间、LRU等),因此和String类型的Key/Value相比,Hash类型极大的减少了Key的数量(大部分的Key都以Hash字段的形式表示并存储了),从而进一步优化了存储空间的使用效率。

在这篇redis memory optimization官方文章中,作者强烈推荐使用hash存储数据

Use hashes when possible

Small hashes are encoded in a very small space, so you should try representing your data using hashes every time it is possible. For instance if you have objects representing users in a web application, instead of using different keys for name, surname, email, password, use a single hash with all the required fields.

But many times hashes contain just a few fields. When hashes are small we can instead just encode them in an O(N) data structure, like a linear array with length-prefixed key value pairs. Since we do this only when N is small, the amortized time for HGET and HSET commands is still O(1): the hash will be converted into a real hash table as soon as the number of elements it contains will grow too much (you can configure the limit in redis.conf).

This does not work well just from the point of view of time complexity, but also from the point of view of constant times, since a linear array of key value pairs happens to play very well with the CPU cache (it has a better cache locality than a hash table).

hashtable

hashtable 编码的哈希对象使用字典作为底层实现, 哈希对象中的每个键值对都使用一个字典键值对来保存:

  • 字典的每个键都是一个字符串对象, 对象中保存了键值对的键;
  • 字典的每个值都是一个字符串对象, 对象中保存了键值对的值。

hashtable 编码的对象如下所示:

Redis 选择hash还是string 存储数据?

第二次测试

values = {
    "name": "gs",
    "age": 1,
    "intro": "long..long..long..string"
}

第二次测试方式和第一次一样,只是把测试数据中加了一个大的字符串,以保证hash 使用hashtable 的方式存储数据

结果如下:

hashtable: 1.13G

string: 1.13G

基本一样,这里应该主要是Hash类型极大的减少了Key的数量(大部分的Key都以Hash字段的形式表示并存储了),从而进一步优化了存储空间的使用效率。

NOTE: 读取和写入的速度基本一致,差别不大

回到这个问题,对于string 和 hash 该如何选择呢?

我比较赞同下面这个答案:

Redis 选择hash还是string 存储数据?

具体使用哪种数据结构,其实是需要看你要存储的数据以及使用场景。

如果存储的都是比较结构化的数据,比如用户数据缓存,或者经常需要操作数据的一个或者几个,特别是如果一个数据中如果filed比较多,但是每次只需要使用其中的一个或者少数的几个,使用hash是一个好的选择,因为它提供了hget 和 hmget,而无需取出所有数据再在代码中处理。

反之,如果数据差异较大,操作时常常需要把所有数据都读取出来再处理,使用string 是一个好的选择。

当然,最简单的就是听从官方的建议,放心的使用hash 吧。

还有一种场景:如果一个hash中有大量的field(成千上万个),需要考虑是不是使用string来分开存储是不是更好的选择。

参考链接

[1] Redis strings vs Redis hashes to represent JSON: efficiency?: https://stackoverflow.com/questions/16375188/redis-strings-vs-redis-hashes-to-represent-json-efficiency[2] redis memory optimization: https://redis.io/topics/memory-optimization[3] Redis 设计与实现: http://redisbook.com/preview/object/hash.html