redis和nginx一直是我认为很优秀的开源项目,也是我看的较多的开源实现。下面从几个方面简单说说redis实现。
- redis介绍
- 常用数据类型
- io模型
- 通信协议
- 疑问解答
redis
redis是一个基于内存的数据库,现有的数据类型满足大部分公司的开发需求,性能也满足一般公司使用。
redis虽然是一个基于内存的数据库,但他提供了两种可以持久化的方式。这种持久化的机制避免的redis服务
器重启丢失数据的问题,但也不能完全避免数据不会丢失(这里讨论的是非集群模式)。
redis提供的内存持久有rdb
快照和基于aof
的文件方式,默认是rdb快照,可以通过配置修改快照更新的方式,
这两种方式都是fork
进程执行backup
的。下面我们讨论redis里面的数据类型
数据类型
我们先来看下redis源码说明。redis里的值有5种基础的类型,任何一个值都是以redisObject
存在,并以下面不
同的编码方式编码成一个redisObject
。
server.h
|
|
String
字符串类型是redis里最基础的值。字符串是二进制安全的,也就是说这样的字符串可以包含任意的数据,比如一张JPEG
的图片或一个序列化的Ruby
对象。字符串值最大的长度是512MB
。
在redis里面,你可以用字符串做一些有趣的事情。比如:
- 字符串可以使用类似
INCR
的命令(INCR
DECR
INCRBY
)作为原子计数 - 用
APPEND
命令向字符串的值里追加一些字符串 - 可以用
GETRANGE
和SETRANGE
像随机访问数组一样获取或改变字符串里面的内容 - 少量的空间可以编码大量的数据或者可以使用
GETBIT
和SETBIT
来创建一个Redis backed Bloom Filter
常用的命令如下:
INCR
GET
SET
STRLEN
等命令
Lists
列表是一个简单的字符串列表,按插入的顺序排序。往列表增加一个元素可能从列表的头部或者从列表的尾部添加。
LPUSH
命令在列表的头部插入一个新的元素,RPUSH
则是往列表尾部插入一个新的元素。当在一个不存的在key上
执行以上操作后会生成一个新的列表。同样的,执行对应的操作key所对应的空间会被删除。
这些命令都是非常方便的语义,因此,如果命令带有一个不存的key时,所有的列表相关的命令在空列表里面会执行和他们名称相同的操作,
一些列表相关的命令使用例子以及结果如下:
|
|
开表最大的长度是232-1(4294967295,超过40亿).
从时间复杂角度来看列表的主要特征是他在列表的首尾进行插入和删除的时间常数。即使列表里面有数亿个元素,访问里边的元素是非常快的,
但访问一个非常巨大的列表来说,越访问后面的速度会变得越慢,它的时间复杂度为O(N)
你可以用列表做一些有趣的事情,比如:
在社交网络中建立一个时间线,基于用户的时间线使用
LPUSH
增加新的元素,使用LRANGE
来获取最近插入的元素。LPUSH
结合LTRIM
可以创建一个不会超过指定元素的列表,只记录最近N个元素列表可以作为一个消息传递原语,例如著名的
Resque Ruby
图书馆创建后台作业你可以做更多关于列表相关的事,这种数据类型支持许多命令,包含阻塞的命令,如:
BLPOP
Sets
Set是一个没有排序的字符串的一个集合。新增、删除以及判断是成员是否成在的时间复杂度可以是O(1)
(这个时间常量不管集合里面的无数个数)。
集合拥有不允许重复元素的特性。同一个元素增加多次的结果只会有一个元素存在集合里面,从实践的角度来说往集合里面增加一个元素的时候不需要主动判断元素是否已经存在。
一个关于集合非常有趣的事情是,它支持一些服务器用来计算已经存在的集合的命令,这样你可以在短的时间内完成集合的合并、交集,差异。
集合中最大的元素个数是232-1(4294967295,超过40亿).
你可以用集合做许多有趣的事情,例如:
可以用集合记录唯一的东西。想要知道访问一篇博客的所有唯一的IP地址? 每次一个页面查看的时候简单的使用
SADD
加入IP。你确定重复的IP将不会被插入。集合对代表关系有好处。你可以用集合代表每个一标签创建一个标签系统。
然后你可以用SADD
命令给所有拥有一个标签的对象的所有ID加到一个集合里面代表这特殊的标签。如果你想所有对象的所有ID在同一时间拥有三个不同的标签,那就使用SINTER
你可以使用
SPOP
或SRANDMEMBER
命令随机获取集合里面的元素。
Hashes
Hash是字符串字段和字符串的值的一个映射,因此Hash用来代表对象类型是完美的数据类型(举例:一个拥有多个字段的用户,如名字、姓氏、年龄以及更多)
|
|
拥有一些字段的hash以花费非常少的空间方式存储,所以你可以在一个小的Redis实例里面存储数以百万计的对象。
然而hash主要被用来当作对象存储,它们拥有存储多个元素的能力,所以你同样可以为其它更多任务使用hash
每一个hash
最多可以存储232-1个field-value组(超过40亿).
Sorted sets
有序集合和没有重复的字符串集合非常相似。区别是有序集合的每个一个成员都关联一个分值,这个分值被用来使有序集合保持从小到大的顺序。因此成员是唯一的,但分值可能重复。
通过有序集合,你可以非常快的方式新增、删除或更新一个元素(在时间上与集合里面的元素个数成对比).
因为元素在集合里面是有序的,并非后续才有序,你同样可以根据分数或排名很快速的获取范围。访问有序集合中间元素也非常快,所以你可以使用有序集合作为一个没有重复无素的智能列表,在这里你可以快速访问你想要访问的任何东西: 有序的元素、快速存在检测,快速访问中间元素!
简而言之,使用有序集合你可以完成许多任务,并拥有在其它种类的数据库里面不能达到的好的性能。
你可以用有序集合做如下事情:
在大型在线游戏中做一个排行榜,在这里每一次一个新的分数提交时,你用
ZADD
更新它。使用ZRANGE
可以容易的获取排名靠前的玩家,你同样可以使用ZRANK
获取一个玩家自己的排名。结合ZRANK
和ZRANGE
你可以显示玩家以及对应的分数。所有操作都非常快。有序集合经常被用来查找存在
redis
里面的数据。比如你有许多hash
代表玩家,可以使用以玩家年龄作为分数,玩家id
作为值的元素的一个集合。这样使用ZRANGEBYSCORE
将会变得平常并可以根据给的年龄段很快的获取所有的玩家
io模型
服务器是基于epoll的io模型,redis
是单进程的事件模型驱动的架构设计。处理两类事件
- 时间事件
- 网络IO事件
对于
redis
里面IO事件它支持select
、eploo
、kqueue
、evport
多种io监听机制。并封装了一套IO的接口
ae.h
|
|
redis
服务器处理事件的主入口在这里,每次进入事件循环之前先执行了一个函数(持入化的相关的流程,以及回复需要回复的client等),然后再去执行定时事件和客户端发过来的命令。
|
|
|
|
aeProcessEvents
函数主要是处理定时任务的事件和网络IO事件,先找时间最近的一个定时任务,如果时间到了aePoll
的时间参数为0,表示立即返回,说明优先执行定时任务然后再等下次循环进入。
通信协议
redis的协议很简单,就基本的raw字符串,然后有自己的固定格式,按这格式读写就可以和redis
服务器完成通信和后续的任务。
只要和redis
服务器建立tcp连接后,知道他消息格式的话,直接写入raw字符串就可以执行对应的命令。我们也可以通过解析这
样的消息格式就可以实现和服务器通信的目的。
redis里面用的协议名称叫RESP(REdis Serialization Protocol)
,它支持以下几种数据类型:
- Strings 协议里的第一字符为 “+”
- Errors 协议里的第一字符为 “-“
- Integers 协议里的第一字符为 “:”
- Bulk Strings 协议里的第一字符为 “$”
- Arrays 协议里的第一字符为 “*”
客户端发给服务器的命令是一个
Bulk String
的数组,服务器回的包含以上所例举的。
Strings
字符串类型被编码成下面的方式:”+”后面跟具体的字符串内容,中间不包含CR
或者LF
字符(没有新的行才是允许的),最后台CRLF
(\r\n)结尾.
字符串类型用最小的开销来传输非二进安全的字符串。比如许多redis
命令执行成功时仅回复”OK”,这样的字符串类型被编码成以下5个字节:
|
|
为了发送二进制安全的字符串,Bulk Strings
用来替代Strings
的功能。
当redis
服务器回复一个字符串类型时,一个redis
客户端的库应该返回+
后边的字符串,不包括CRLF
。
Errors
RESP对于错误有一个特定的数据类型。实际上errors
非常像字符串类型,只是用-
替代了+
.两者的区别在于errors
被客户端用作异常处理,错误的数据类型里面的字符串就是它的错误消息.
错误消息的格式如下:
|
|
只有在发生一些错误的时候才会回复错误消息,比方说,如果执行的操作与你对应的数据类型不同时,或者你执行的命令不存在以及其它情况。当收到一个错误回复的时候redis
客户端应该抛出一个异常。
下面是一个错误回包的一个例子:
|
|
从第一个字符-
到第一个空格或新的行,代表了返回的错误类型。这是redis
常用的惯例,但并不是所有协议里面的错误格式。
举例说明,ERR
是常用的错误类型,而WRONGTYPE
是更具体的错误,这种错误应用在当客户端执行一个操作对于错误的数据类型时候。这种错误类型叫错误前缀,它是一种允许客户端理解服务器回复的错误类型的方式,而不依赖服务器回复的具体内容,这可能会随着时间改变。
对于不同的错误类型,一个客户端的实现可能会返回不同的异常,也可能会通过直接返回错误的字符串描述内容给调用者提供一个常用的方式来定位错误
然而,像这样一个特征不应该考虑至关重要因为它很少有用,一个有限的客户端实现可能简单的返回常用的错误条件,比如false.
Integers
这种类型是以CRLF
结尾的字符串代表一个interger
,以一个:
字节做为前缀。比如”:0\r\n”,或者”:1000\r\n”都是回复的整型类型.
许多redis
命令都回复Intergers
,比如INCR
,LLEN
以及LASTSAVE
.
返回整形没有什么特殊的意义,对于INCR
是一个增量值,对LASTSAVE
是一个Unix time
等等。然而,返回的整数确定是在有符号的64位整型的范围.
回复整形在回得true
或false
的时候使用也广泛。像EXISTS
或SISMEMBER
命令将会返回1代表true,0代表false.
像其它SADD
,SREM
以及SETNX
,如果命令实际执行成功会返回1,否则返回0.
下面命令会回复一个整型:
Bulk Strings
Bulk Strings
被用来代表一个独立的二进制安全的最大长度为512M的字符串
Bulk Strings
被编码成下面的方式:
$
后面跟随组成字符串内容的字节个数,以CRLF
结尾- 实际的字符串内容
- 最后一个
CRLF
所以字符串”foobar”被编码成:
|
|
空字符串如下:
|
|
Bulk Strings
可以用来特殊的格式来表示一个不存在的值,这样的值代表了一个Null
值。这种特殊的格式的长度为-1,后边没有数据,所以一个Null
值如下所示:
|
|
这叫做Null Bulk String
.
当服务器回复一个Null Bulk String
,客户端的API不应该返回一个空字符串,而是一个空对象。
比如一个Ruby的客户端应该返回一个nil
值,而C实现的客户端应该返回NULL
(或者在回复的对象里设置一个特殊的标志),以及等等
Arrays
客户端使用的是Arrays
协议向服务器发送命令。相似的Redis
命令使用Arrays
协议返回元素的一个合集给客户端。一个LRANGE
命令返回列表的例子.
Clients send commands to the Redis server using RESP Arrays. Similarly certain Redis commands returning collections of elements to the client use RESP Arrays are reply type. An example is the LRANGE command that returns elements of a list.
使用下面的格式返回一个Arrays
:
- 前面一个
*
,后面跟随数组里面元素个数的十进制数值,再接着一个CRLF
- 数组里面的元素是其它数据类型
|
|
当一个包含”foo”和”bar”两个元素的数组被编码成:
|
|
你可以看到数组里面在*<count>CRLF
的部分是由其它的数据类型连续组成的。比如一个包含三个integer
的数组被编码成如下:
|
|
数组类型同样可以包含不同的其它类型,没有要求数组里面的元素都是相同的类型。比如,一个包含四个integer
的列表和一个bulk string
被编码成如下:
|
|
换成多行是为了方便阅读
第一行内容*5\r\n
说明后面接着有5个元素。每一个回复构成了Multi Bulk
用作传播。
Null Array
的概念同样存在,指定一个空值是可选的(通常使用一个Null Bulk String
,由于历史原因我们有两种格式).
比如执行BLPOP
命令超时后,服务器返回一个count为-1的空数据,格式如下:
|
|
redis服务器回服一个空数组里,redis客户端应该返回一个空对象而不是一个空数组。很有必要来区分空列表和不同的条件(比如BLPOP
命令超时)。
在RESP
协议里面数组是可以嵌套使用的。比如一个包含两个数组的数组被编码成如下:
|
|
这个说明,一个数组里面包含一个有三个整型的数组以及一个两个字条串类型的数组。
Null elements in Arrays
数组里面的单个元素可能为空。在redis回复的包里用来说明这个元素被丢失且不为空字符串。这个可能会发生在sort
对get
模糊匹配时指定的key不存在时进行排序时。回复一个包含空元素数组的例子:
|
|
数组中第二个元素是空的。redis客户端应该返回类似下面的东西:
|
|
思考
- 一次client最大能发送多大的数据
- 服务器最大回复的长度
|
|
一个redis client向服务器发送命令最大的长度为PROTO_MAX_QUERYBUF_LEN
。
服务器一次回复最大的消息长度为PROTO_REPLY_CHUNK_BYTES
,比方说hgetall
比较大的时候就分多次发送