本章节主要的内容在于对Redis 数据库的介绍,数据库保存键值对的方式,数据库增删改查数据的方式,保存键的过期时间的方式以及自动删除过期键的策略。
数据库结构
Redis 所有的数据库都保存在服务器 redisServer 结构的 db数组中,每一项都是 redisDb 结构,每一个redisDb 结构都代表着一个数据库。
struct redisServer {
......
// 数据库
redisDb *db;
// 服务器数据库数量
int dbnum;
......
}
typedef struct redisDb {
// 数据库键空间,保存着数据库中的所有键值对
dict *dict; /* The keyspace for this DB */
// 键的过期时间,字典的键为键,字典的值为过期事件 UNIX 时间戳
dict *expires; /* Timeout of keys with a timeout set */
// 正处于阻塞状态的键
dict *blocking_keys; /* Keys with clients waiting for data (BLPOP) */
// 可以解除阻塞的键
dict *ready_keys; /* Blocked keys that received a PUSH */
// 正在被 WATCH 命令监视的键
dict *watched_keys; /* WATCHED keys for MULTI/EXEC CAS */
struct evictionPoolEntry *eviction_pool; /* Eviction pool of keys */
// 数据库号码
int id; /* Database ID */
// 数据库的键的平均 TTL ,统计信息
long long avg_ttl; /* Average TTL, just for stats */
} redisDb;
一般 dbnum 值默认的是 16 ,因为 Redis 服务器一般默认创建 16 个数据库。
而且从上面的 redisDb 结构可以看到,每个数据库都是通过 dict(字典) 这个属性来保存所有的键值对,我们可以称它为键空间,它是一个字典结构:
-
键空间的键也就是数据库的键,每个键都是字符串对象。
-
键空间的值也就是数据库的值,每个值都是 Redis 对象也就是包含字符串对象,列表对象,哈希表对象等等之前说过的数据结构对象。
数据库的一些基本操作
切换数据库
每个 Redis 客户端都有一个自己的目标数据库,每次执行对应的数据库命令的是就会对对应的目标数据库进行操作。默认情况下 Redis 客户端目标数据库是 0 号数据库,当然我们也可以通过 select 命令来切换目标数据库。
原理是在 Redis 服务器内部是通过 redisClient 结构来记录客户端状态,这个结构中有一个指向 redisDb 的指针,这个指针指向我们刚说的 redisServer 结构的 db 数组的其中一个元素,这个被指向的元素就是客户端的目标数据库。通过修改 redisClient.db 指针就可以让它指向服务器的不同的数据库。
增删改查等操作
其实所有的增删改查操作都是对这个键空间也就是字典进行操作的。以书上的为例说明:
- 先通过以下命令初始化一个数据库键空间
SET message "hello world"
RPUSH alphabet "a" "b" "c"
HSET book name "Redis in Action"
HSET book author "Josiah L. Carlson"
HSET book publisher "Manning"
2. 添加新建也就是将一个键值对添加到这个字典当中。
SET date "2013.12.1"
3. 删除键实际就是在这个字典中删除对应的键值对
DEL book
4. 更新键实际就是在字典中找到对应的键值对,根据键对应的值对象进行更新。
SET message "blah blah"
- 对键取值实际就是在字典中找到对应的键值对,取出键对应的值对象
设置过期时间及过期键删除策略
设置过期时间
Redis 提供了四种不同的命令用于设置键的生存时间,在经过指定的时间之后服务器就会自动删除生存时间为 0 的键。
-
EXPIRE [key] [ttl] 命令用于将键 key 的生存时间设置 ttl 秒。
-
PEXPIRE [key] [ttl] 命令用于将键 key 的生存时间设置 ttl 毫秒。
-
EXPIREAT [key] [timestamp] 命令用于将键 key 的生存时间设置 timestamp 所指定的秒数时间戳。
-
PEXPIREAT [key] [timestamp] 命令用于将键 key 的过期时间设置为 timestamp 所指定的毫秒数时间戳。
虽然有多种不同单位和不同形式的设置命令,但实际上 EXPIRE、PEXPIRE、EXPIREAT 三个命令都是使用PEXPIREAT 命令来实现的。无论客户端执行的是以上四个命令中的哪一个,经过转换之后,最终的执行都是通过执行 PEXPIREAT 命令来实现。转化图如下
保存过期时间
在数据库结构中,我们也会看到我们除了用字典来保存键空间之后,我们还用了一个 expires 这个字典保存着数据库所有键的过期时间。
-
过期字典的键是一个指针,指向键空间的某一个键对象。
-
过期字典的值是一个 long long 类型的证书,保存着键所指向的数据库的过期时间(一个毫秒精度的 UNIX 的时间戳)。 这里键空间的键和过期字典的键都指向同一个对象的目的就是不想出现重复的对象,浪费空间。
过期键的判断
在 Redis 中通过以下的步骤检查一个给定键是否过期:
-
检查这个键是否在过期字典中。
-
检查当前的 UNIX 时间戳是否大于过期时间。
过期键的删除策略
一般来说有三种不同的删除策略
-
定时删除:在设置键的过期时间的同时,创建一个定时器 timer,让定时器在键的过期时间来临时,立即执行对键的删除操作。
-
惰性删除:放任键过期不管,但是每次从键空间中获取键时,都检查取得的键是否过期,如果过期的话,就删除该键;如果没有过期,就返回该键。
-
定期删除:每隔一段时间,程序就对数据库进行一-次检查,删除里面的过期键。至于要删除多少过期键,以及要检查多少个数据库,则由算法决定。
在 Redis 中是通过惰性删除和定时删除两种策略搭配着来:
- 在所有的读写数据库的 Redis 命令执行之前,调用 expiredIfNeed 函数对输入键做检查,如果是过期键就删除,不然那就不做操作。
- Redis 周期性的执行 activeExpireCycle 函数,在规定的时间内,多次遍历服务器的数据库,从数据库的 expired 字典中随机检查一部分键的过期时间,并删除其中的过期键。
RDB、AOF和复制功能对过期键的处理
RDB处理方式
在生成一个 RDB 文件时,就已经会对数据库中的键进行检查,已过期的键是不会被保存到新创建的 RDB 文件中的。
在载入一个 RDB 文件时,如果是主服务器,在载入的时候就会对保存的键进行检查,过期的键就不会放到数据库中;如果是从服务器的话不管是否过期都会全部载入,但是因为主服务器会在数据同步的时候清理从服务的数据所以也不会产生影响。
AOF处理方式
在生成一个 AOF 文件时,如果某个键过期没有被删除也是会被写进到 AOF 文件中,但是当这个键被删除的时候,程序会向 AOF 文件中追加一条 DEL 命令来表示这个键已经被删除了。
在载入一个 AOF 文件时,不管是主服务器还是从服务器都会对数据库中的键进行检查,过期的键就不会放到数据库中。
复制功能的处理
从服务器的过期键删除动作由主服务器控制:
-
主服务器在删除一个过期键之后,会显式地向所有从服务器发送一个 DEL 命令,告知从服务器删除这个过期键。
-
从服务器在执行客户端发送的读命令时,即使碰到过期键也不会将过期键删除,而是继续像处理未过期的键一样来处理过期键。
-
从服务器只有在接到主服务器发来的 DEL 命令之后,才会删除过期键。
数据库通知
数据库通知是Redis2.8版本新增加的功能,这个功能可以让客户端通过订阅给定的频道或者模式,来获知数据库中键的变化,以及数据库中命令的执行情况。
举个例子,以下代码展示了客户端如何获取0号数据库中针对message键执行的所有命令:
127.0.0.1:6379>SUBSCRIBE _ _keyspace@0_ _:message
Reading messages . . . (press Ctrl-C to quit)
1) "subscribe" //订阅信息
2) "_ _keyspace@0_:message"
3) (integer) 1
1) "message" //执行SET 命令
2) "_ _keyspace@0_: message"
3) "set"
1) "message" //执行EXPIE命令
2) " keyspace@0_:message"
3) "expire"
1) "message" //执行DEL 命令
2) "_ _keyspace@0_: message "
3) "de1"
根据发回的通知显示,先后共有SET、EXPlRE 、DEL 三个命令对键message进行了操作。
这一类关注"某个键执行了什么命令"的通知称为键空间通知(key-space-notification)。
除此之外,还有另一类称为键事件通知(key-event-notification)的通知,它们关注的是"某个命令被什么键执行了" 。
以下是一个键事件通知的例子,代码展示了客户端如何获取0 号数据库中所有执行DEL 命令的键:
127.0.0.1:6379> SUBSCRIBE _ _keyspace@0_ _:del
Reading messages. . . (press Ctrl-C to quit)
1) "subcribe" //订阅信息
2) "_ _keyevent@0_ _:del"
3) (integer) 1
1) "message" //键key执行了DEL命令
2) "keyevent@0_ _:del"
3) "key"
1) "message" //键number执行了DEL命令
2) "_ _keyevent@0_ _:del"
3) "number"
1) "message" //键message执行了DEL命令
2) "keyevent@0_ _:del"
3) "message"
根据发回的通知显示,key、number、message三个键先后执行了DEL 命令。 服务器配置的notify-keyspace-events选项决定了服务器所发送通知的类型:
- 想让服务器发送所有类型的键空间通知和键事件通知,可以将选项的值设置为AKE。
- 想让服务器发送所有类型的键空间通知,可以将选项的值设置为AK。
- 想让服务器发送所有类型的键事件通知,可以将选项的值设置为AE。
- 想让服务器只发送和字符串键有关的键空间通知,可以将选项的值设置为K$。
- 想让服务器只发送和列表键有关的键事件通知,可以将选项的值设置为E1。
发送通知
发送数据库通知的功能是由notify.c/notifyKeyspaceEvent函数实现的:
void notifyKeyspaceEvent(int type, char *event, robj *key, int dbid);
函数的type参数是当前想要发送的通知类型,程序会根据这个值来判断通知是否就是服务器配置notify-keyspace-events选项所定的同志类型,从而决定是否发送通知。
event、keys、dbid分别是事件的名称、产生事件的键、以及产生事件的数据库号码,函数会根据这几个参数构建时间通知的内容,以及接受通知的频道名。
当redis命令需要发送数据库通知时,该命令的实现函数就会调用notifyKeyspaceEvent函数并传递相关参数。
例如,以下是SADD命令的实现函数saddCommand其中一部分代码:
void saddCommand(redisClient*c){
//...
//如果至少一个元素被成功添加,那么执行以下程序
if(added){
//..
//发送事件通知
notifyKeyspaceEvent(REDIS_NOTIFY_SET,"sadd",c-->argy[1],c->db->id);
//...
}
}
当SADD命令至少添加一个集合元素之后,命令就会发送通知,该通知的类型为REDIS_NOTIFY_SET(表示这是一个集合通知),名为sadd(表示这是执行SADD命令所产生的通知)
发送通知的实现
以下是notifyKeyspaceEvent函数的伪代码实现:
def notifyKeyspaceEvent(type,event,key,dbid):
#如果给定的通知不是服务器允许发送的通知,那么直接返回
if not(server.notify_keyspace_events&type) :
return
#发送键空间通知
if server.notify_keyspace_events&REDIS_NOTIFY_KEYSPACE:
#将通知发送给频道__ keyspace@<dbid>__ :<key>
#内容为键所发生的事件<event>
#构建频道名字
chan="keyspace@{dbid}:{key}".format(dbid=dbid,key=key)
#发送通知
pubsubPublishMessage(chan,event)
#发送键事件通知
if server.notify_keyspace_events&REDIS_NOTIFY_KEYEVENT:
#将通知发送给频道_keyevent@<dbid>_:<event>
#内容为发生事件的键<key>
#构建频道名字
chan="keyevent@{dbid}:{event}".format(dbid=dbid, event=event)
#发送通知
pubsubPublishMessage(chan, key)
notifyKeyspaceEvent函数执行以下操作:
server.notify_keyspace_events属性就是服务器配置notify-keyspace-events选项所设置的值,如果给定的通知类型type不是服务器允许发迭的通知类型,那么函数会直接返回,不做任何动作。
如果给定的通知是服务器允许发送的通知,那么下一步函数会检测服务器是否允许发送键空间通知,如果允许的话,程序就会构建并发送事件通知。
最后,函数检测服务器是否允许发送键事件通知,如果允许的话,程序就会构建并发送事件通知。
另外,pubsubPublishMessage函数是PUBLISH命令的实现函数,执行这个函数等同于执行PUBLISH命令,订阅数据库通知的客户端收到的信息就是由这个函数发出的.
总结
dsa ⒈Redis 服务器的所有数据库都保存在redisServer.db 数组中,而数据库的数量则由redisServer.dbnum 属性保存。
dsa ⒉客户端通过修改目标数据库指针,让它指向redisServer .db 数组中的不同元素来切换不同的数据库。
dsa ⒊数据库主要由diet 和expires 两个字典构成,其中diet 字典负责保存键值对,而expires 字典则负责保存键的过期时间。
dsa ⒋因为数据库由字典构成,所以对数据库的操作都是建立在字典操作之上的。
dsa ⒌数据库的键总是一个字符串对象,而值则可以是任意一种Redis 对象类型,包括字符串对象、哈希表对象、集合对象、列表对象和有序集合对象,分别对应字符串键哈希表键、集合键、列表键和有序集合键。
dsa ⒍expires 字典的键指向数据库中的某个键,而值则记录了数据库键的过期时间,过期时间是一个以毫秒为单位的 UNIX 时间戳。
dsa ⒎Redis 使用惰性删除、定期删除两种策略来删除过期的键:惰性删除策略只在碰到过期键时才进行删除操作,定期删除策略则每隔一段时间主动查找并删除过期键。
dsa ⒏执行SAVE 命令或者BGSAVE 命令所产生的新RDB 文件不会包含已经过期的键。
dsa ⒐执行BGREWRITEAOF 命令所产生的重写AOF 文件不会包含已经过期的键。
dsa ⒑ 当一个过期键被删除之后,服务器会追加一条DEL 命令到现有AOF文件的末尾,显式地删除过期键。
dsa ⒒ 当主服务器删除一个过期键之后,它会向所有从服务器发送一条DEL 命令,显式地删除过期键。
dsa ⒓ 从服务器即使发现过期键也不会自作主张地删除它,而是等待主节点发来DEL 命令,这种统一、中心化的过期键删除策略可以保证主从服务器数据的一致性。 dsa ⒔ 当Redis 命令对数据库进行修改之后,服务器会根据配置向客户端发送数据库通知。