从此Redis是路人
从此Redis是路人
序言:Redis(Remote DIctionary Server)作为一个开源/C实现/高性能/基于内存的key-value存储系统,相信做Java的小伙伴都不会陌生。Redis常用于缓存、分布式锁、队列(或有序集合)等场景,追求技术的小伙伴们肯定不只满足于Redis的使用上,肯定也想了解Redis背后的设计思想及对应的开发实践,话不多少,上车吧~
ps:文章较长,小伙伴可以搬个小板凳慢慢阅读,也可以结合自身情况进行选择阅读 : )
由于文章内容较多,下面就按照Redis对象、事件处理机制、持久化机制、事务机制、主备模型、集群机制等内容进行分析讨论,中间可能穿插着相关内容的扩展和思考。Let’s go….
Redis对象
Redis主要有5种不同类型的对象,分别是字符串、列表、哈希表、集合、有序集合。这些对象都是基于Redis基础数据结构来构建的,并且每种对象都用到了至少一种基础数据结构。Redis对象还实现了引用计数的内存回收技术,当不再使用某个对象时,可以及时释放其内存;通过引用计数实现了对象共享机制,节约内存(Redis只对包含整数值的字符串对象进行共享);Redis的对象带有访问时间戳,可用于计算该对象空转时间,启用maxmemroy功能时,空转时间较长的键优先被删除。
Redis用到的底层数据结构有:简单动态字符串、双端链表、字典、压缩列表、整数集合、跳跃表等,Redis并没有直接使用这些数据结构来实现键值对数据库,而是基于这些基础数据结构创建了一个对象系统,这写对象包括字符串对象、列表对象、哈希对象、集合对象和有序集合对象等。
Redis中使用对象表示键和值,当新建一个键值对时,Redis至少创建2个对象,一个是键对象,另一个是值对象。
typedef struct redisObject {
unsigned type:4;
unsigned encoding:4;
unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */
int refcount;
void *ptr;
} robj;
type表示对象类型(有REDIS_STRING/REDIS_LIST/REDIS_HASH/REDIS_SET/REDIS_ZSET
几种),对于Redis键值对来说,键永远都是字符串,值可以是字符串、列表、哈希表、集合、有序集合中的一种。encoding表示对象编码,也就是该对象使用什么底层数据结构实现。ptr指向对象的底层数据结构。
Redis对象:
字符串:字符串对象底层可以是int和raw编码方式,如set num 1后执行append num hello,则会导致编码方式由int到raw转换。
127.0.0.1:6379> set num 1
OK
127.0.0.1:6379> object encoding num
"int"
127.0.0.1:6379> append num hello
(integer) 6
127.0.0.1:6379> object encoding num
"raw"列表:列表对象的编码可以是ziplist、linkedlist和quicklist(新版本Redis使用quicklist作为列表底层数据结构了)。ziplist使用功能压缩列表作为底层实现,每个压缩列表节点保存一个列表元素。
127.0.0.1:6379> rpush nums 1 "tow" 3
127.0.0.1:6379> object encoding nums
"quicklist"集合:set容器,集合对象的编码可以是intset和hashtable。intset编码的集合对象使用整数集合作为底层实现,所有元素都保存在整数集合中。另一方面,使用hashtable的集合对象使用字典作为底层实现,字典中每个键都是一个字符串对象,即一个集合元素,而字典的值都是NULL。
127.0.0.1:6379> sadd mans 1
127.0.0.1:6379> sadd mans 2
127.0.0.1:6379> object encoding mans
"intset"
127.0.0.1:6379> sadd mans luoxn28
127.0.0.1:6379> object encoding mans
"hashtable"有序集合:有序集合对象的编码可以是ziplist和skiplist。ziplist编码的压缩列表对象使用压缩列表作为底层实现,每个集合元素使用两个紧挨着的压缩列表节点保存,第一个保存集合元素,第二个保存集合元素对应的分值。压缩列表内集合元素按照分值大小进行排序,分值较小的在前,分值大的在后;skiplist编码的有序集合对象使用zset结构作为底层实现,一个zset结构同时包含一个字典和一个跳跃表,通过字典提高获取单个元素效率,通过skiplist提高获取范围查询能力,二者各取所长。
哈希表:哈希对象的编码可以是ziplist和hashtable。hashtable编码的哈希对象使用字典作为底层实现,则哈希对象中的每个键值对都是字典键值对来保存,hashtable为数组+链表的分离连接法实现。
事件处理机制
Redis的事件类型分为时间事件和文件事件,文件事件也就是IO事件。时间事件的处理是在epoll_wait返回处理文件事件后处理的,每次epoll_wait的超时时间都是Redis最近的一个定时器时间。Redis对epoll进行了简单封装,不像memcached直接使用libevent作为网络通信组件。
Redis在进行事件处理前,首先会进行初始化,初始化的主要逻辑在main/initServer
函数中。初始化流程主要做的工作如下:
设置回调函数;
创建事件循环机制,即调用epoll_create;
创建服务监听端口,创建定时事件,并将这些事件添加到事件机制中。
void initServer(void) {
// 设置信号对应的处理函数
signal(SIGHUP, SIG_IGN);
signal(SIGPIPE, SIG_IGN);
setupSignalHandlers();
...
createSharedObjects();
adjustOpenFilesLimit();
// 创建事件循环机制,及调用epoll_create创建epollfd用于事件监听
server.el = aeCreateEventLoop(server.maxclients+CONFIG_FDSET_INCR);
server.db = zmalloc(sizeof(redisDb)*server.dbnum);
/* Open the TCP listening socket for the user commands. */
// 创建监听服务端口,socket/bind/listen
if (server.port != 0 &&
listenToPort(server.port,server.ipfd,&server.ipfd_count) == C_ERR)
exit(1);
...
/* Create the Redis databases, and initialize other internal state. */
for (j = 0; j < server.dbnum; j++) {
server.db[j].dict = dictCreate(&dbDictType,NULL);
server.db[j].expires = dictCreate(&keyptrDictType,NULL);
server.db[j].blocking_keys = dictCreate(&keylistDictType,NULL);
server.db[j].ready_keys = dictCreate(&setDictType,NULL);
server.db[j].watched_keys = dictCreate(&keylistDictType,NULL);
server.db[j].eviction_pool = evictionPoolAlloc();
server.db[j].id = j;
server.db[j].avg_ttl = 0;
}
...
/* Create the serverCron() time event, that's our main way to process
* background operations. 创建定时事件 */
if(aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL) == AE_ERR) {
serverPanic("Can't create the serverCron time event.");
exit(1);
}
/* Create an event handler for accepting new connections in TCP and Unix
* domain sockets. */
for (j = 0; j < server.ipfd_count; j++) {
if (aeCreateFileEvent(server.el, server.ipfd[j], AE_READABLE,
acceptTcpHandler,NULL) == AE_ERR)
{
serverPanic("Unrecoverable error creating server.ipfd file event.");
}
}
// 将事件加入到事件机制中,调用链为 aeCreateFileEvent/aeApiAddEvent/epoll_ctl
if (server.sofd > 0 && aeCreateFileEvent(server.el,server.sofd,AE_READABLE,
acceptUnixHandler,NULL) == AE_ERR)
/* Open the AOF file if needed. */
if (server.aof_state == AOF_ON) {
server.aof_fd = open(server.aof_filename,
O_WRONLY|O_APPEND