Redis 数据库及相关命令实现
1. 数据库管理命令
数据库管理的命令如下表格所示:redis keys命令详解
FLUSHDB清空当前数据库的所有key
FLUSHALL清空整个Redis服务器的所有key
DBSIZE返回当前数据库的key的个数
DEL key [key …]删除一个或多个键
EXISTS key检查给定key是否存在
SELECT id切换到指定的数据库
RANDOMKEY从当前数据库中随机返回(不删除)一个 key 。
KEYS pattern查找所有符合给定模式pattern的key
SCAN cursor [MATCH pattern] [COUNT count]增量式迭代当前数据库键
LASTSAVE返回最近一次成功将数据保存到磁盘上的时间,以 UNIX 时间戳格式表示。
TYPE key返回指定键的对象类型
SHUTDOWN停止所有客户端,关闭 redis 服务器(server)
RENAME key newkey重命名指定的key,newkey存在时覆盖
RENAMENX key newkey重命名指定的key,当且仅当newkey不存在时操作
MOVE key db移动key到指定数据库
EXPIREAT key timestamp为 key 设置生存时间,EXPIREAT 命令接受的时间参数是 UNIX 时间戳
EXPIRE key seconds以秒为单位设置 key 的生存时间
PEXPIRE key milliseconds以毫秒为单位设置 key 的生存时间
PEXPIREAT key milliseconds-timestamp以毫秒为单位设置 key 的过期 unix 时间戳
TTL key以秒为单位返回 key 的剩余生存时间
PTTL key以毫秒为单位返回 key 的剩余生存时间
2. 数据库的实现
2.1数据库的结构
typedef struct redisDb { // 键值对字典,保存数据库中所有的键值对 dict *dict; /* The keyspace for this DB */ // 过期字典,保存着设置过期的键和键的过期时间 dict *expires; /* Timeout of keys with a timeout set */ // 保存着 所有造成客户端阻塞的键和被阻塞的客户端 dict *blocking_keys; /*Keys with clients waiting for data (BLPOP) */ // 保存着 处于阻塞状态的键,value为NULL dict *ready_keys; /* Blocked keys that received a PUSH */ // 事物模块,用于保存被WATCH命令所监控的键 dict *watched_keys; /* WATCHED keys for MULTI/EXEC CAS */ // 当内存不足时,Redis会根据LRU算法回收一部分键所占的空间,而该eviction_pool是一个长为16数组,保存可能被回收的键 // eviction_pool中所有键按照idle空转时间,从小到大排序,每次回收空转时间最长的键 struct evictionPoolEntry *eviction_pool; /* Eviction pool of keys */ // 数据库ID int id; /* Database ID */ // 键的平均过期时间 long long avg_ttl; /* Average TTL, just for stats */} redisDb;
blocking_keys 和 ready_keys 使用于在列表类型的阻塞命令(BLPOP等),详细内容看:Redis 列表键命令实现
watched_keys 是用于事物模块。
eviction_pool 是Redis在内存不足情况下,要回收内存时所使用。
dict 和 expires 和 id是本篇主要讨论的。
Redis服务器和客户端也都保存有数据库的信息,下面截取出来:
typedef struct client { redisDb *db; /* Pointer to currently SELECTed DB. */} client;struct redisServer { redisDb *db; int dbnum; /* Total number of configured DBs */};
Redis服务器在初始化时,会创建一个长度为dbnum(默认为16)个 redisDb类型数组,客户端登录时,默认的数据库为0号数据库。当执行SELECT index命令后,就会切换数据库。我们用两个客户端,表示如下图:
SELECT index命令非常简单,源码如下:
// 切换数据库int selectDb(client *c, int id) { // id非法,返回错误 if (id < 0 || id >= server.dbnum) return C_ERR; // 设置当前client的数据库 c->db = &server.db[id]; return C_OK;}
2.2 数据库的键值对字典
Redis是一个key-value数据库服务器,它将所有的键值对都保存在 redisDb 结构中的 dict 字典成员中(Redis 字典结构源码剖析)。
键值对字典的键,就是数据库的key,每一个key都是字符串的对象。
键值对字典的值,就是数据库的value,每一个value可以是字符串的对象,列表对象,哈希表对象,集合对象和有序集合对象中的任意一种。
Redis 对象系统源码剖析
数据库对键对象的删除操作,会连带值对象也一并删除,因此再有一些操作中,例如RENAME等命令,中间步骤会使用删除原有键,常常需要对值对象的引用计数加1,保护值对象不被删除,当新的键被设置后,则对值对象的引用计数减1。
我们向一个数据库中添加几个键,并且用图表示出来:
红色代表键对象,有 RAW编码的字符串对象,哈希对象。将结构简化表示,重点关注引用计数。
蓝色代表值对象,完成结构如图所示。
数据库每次根据键名找到值对象时,是分为以读操作 lookupKeyRead() 或写操作 lookupKeyWrite() 的方式取出的,而这两种有一定的区别,下面展示源码:
lookupKey()函数
读操作 lookupKeyRead() 或写操作 lookupKeyWrite()都会调用这个底层的函数,这个函数非常简单,就是从键值对字典中先找到键名对应的键对象,然后取出值对象
// 该函数被lookupKeyRead()和lookupKeyWrite()和lookupKeyReadWithFlags()调用// 从数据库db中取出key的值对象,如果存在返回该对象,否则返回NULL// 返回key对象的值对象robj *lookupKey(redisDb *db, robj *key, int flags) { // 在数据库中查找key对象,返回保存该key的节点地址 dictEntry *de = dictFind(db->dict,key->ptr); if (de) { //如果找到 robj *val = dictGetVal(de); //取出键对应的值对象 /* Update the access time for the ageing algorithm. * Don't do it if we have a saving child, as this will trigger * a copy on write madness. */ // 更新键的使用时间 if (server.rdb_child_pid == -1 && server.aof_child_pid == -1 && !(flags & LOOKUP_NOTOUCH)) { val->lru = LRU_CLOCK(); } return val; //返回值对象 } else { return NULL; }
lookupKeyRead()函数
lookupKeyRead()函数调用了lookupKeyReadWithFlags()函数,后者其实就判断了一下当前键是否过期,如果没有过期,更新 misses 和 hits 信息,然后就返回值对象。
还有就是两个宏:
define LOOKUP_NONE 0 //zero,没有特殊意义
define LOOKUP_NOTOUCH (1<<0) //不修改键的使用时间,如果只是想判断key的值对象的编码类型(TYPE命令)我们不希望改变键的使用时间。
// 以读操作取出key的值对象,会更新是否命中的信息robj *lookupKeyRead(redisDb *db, robj *key) { return lookupKeyReadWithFlags(db,key,LOOKUP_NONE);}// 以读操作取出key的值对象,没找到返回NULL// 调用该函数的副作用如下:// 1.如果一个键的到达过期时间TTL,该键被设置为过期的// 2.键的使用时间信息被更新// 3.全局键 hits/misses 状态被更新// 注意:如果键在逻辑上已经过期但是仍然存在,函数返回NULLrobj *lookupKeyReadWithFlags(redisDb *db, robj *key, int flags) { robj *val; // 如果键已经过期且被删除 if (expireIfNeeded(db,key) == 1) { /* Key expired. If we are in the context of a master, expireIfNeeded() * returns 0 only when the key does not exist at all, so it's save * to return NULL ASAP. */ // 键已过期,如果是主节点环境,表示key已经绝对被删除,如果是从节点, if (server.masterhost == NULL) return NULL; // 如果我们在从节点环境, expireIfNeeded()函数不会删除过期的键,它返回的仅仅是键是否被删除的逻辑值 // 过期的键由主节点负责,为了保证主从节点数据的一致 if (server.current_client && server.current_client != server.master && server.current_client->cmd && server.current_client->cmd->flags & CMD_READONLY) { return NULL; } } // 键没有过期,则返回键的值对象 val = lookupKey(db,key,flags); // 更新 是否命中 的信息 if (val == NULL) server.stat_keyspace_misses++; else server.stat_keyspace_hits++; return val;}
- lookupKeyWrite()函数
lookupKeyWrite() 函数则先判断键是否过期,然后直接调用最底层的 lookupKey() 函数,和 lookupKeyRead()函数 相比,少了一步更新 misses 和 hits 信息的过程。
// 以写操作取出key的值对象,不更新是否命中的信息robj *lookupKeyWrite(redisDb *db, robj *key) { expireIfNeeded(db,key); return lookupKey(db,key,LOOKUP_NONE);}
2.3 键的过期时间
redisBb结构中的 expires 字典保存这设置了过期时间的键和过期的时间。通过 EXPIRE 、 PEXPIRE、 EXPIREAT 和 PEXPIREAT四个命令,客户端可以给某个存在的键设置过期时间,当键的过期时间到达时,键就不再可用。
我们先用图展示一下数据库中的过期字典,用刚才的键值对字典中的对象。
很明显,键值对字典和过期字典中的相同对象只占一份空间,只是增加引用计数。
我们重点讨论过期键的删除策略:
惰性删除:当客户度读出带有超时属性的键时,如果已经超过键设置的过期时间,会执行删除并返回空。
定时删除:Redis内部维护一个定时任务,默认每秒运行10次。
我们给出惰性删除的代码,这个函数 expireIfNeeded(),所有读写数据库的Redis命令在执行前都会调用,删除过期键。
// 检查键是否过期,如果过期,从数据库中删除// 返回0表示没有过期或没有过期时间,返回1 表示键被删除int expireIfNeeded(redisDb *db, robj *key) { //得到过期时间,单位毫秒 mstime_t when = getExpire(db,key); mstime_t now; // 没有过期时间,直接返回 if (when < 0) return 0; /* No expire for this key */ /* Don't expire anything while loading. It will be done later. */ // 服务器正在载入,那么不进行过期检查 if (server.loading) return 0; /* If we are in the context of a Lua script, we claim that time is * blocked to when the Lua script started. This way a key can expire * only the first time it is accessed and not in the middle of the * script execution, making propagation to slaves / AOF consistent. * See issue #1525 on Github for more information. */ // 返回一个Unix时间,单位毫秒 now = server.lua_caller ? server.lua_time_start : mstime(); /* If we are running in the context of a slave, return ASAP: * the slave key expiration is controlled by the master that will * send us synthesized DEL operations for expired keys. * * Still we try to return the right information to the caller, * that is, 0 if we think the key should be still valid, 1 if * we think the key is expired at this time. */ // 如果服务器正在进行主从节点的复制,从节点的过期键应该被 主节点发送同步删除的操作 删除,而自己不主动删除 // 从节点只返回正确的逻辑信息,0表示key仍然没有过期,1表示key过期。 if (server.masterhost != NULL) return now > when; /* Return when this key has not expired */ // 当键还没有过期时,直接返回0 if (now <= when) return 0; /* Delete the key */ // 键已经过期,删除键 server.stat_expiredkeys++; //过期键的数量加1 propagateExpire(db,key); //将过期键key传播给AOF文件和从节点 notifyKeyspaceEvent(NOTIFY_EXPIRED, //发送"expired"事件通知 "expired