Redis服务器将所有数据库都保存在服务器状态redis.h/redisServer结构的db数组中,db数组的每个项都是一个redis.h/redisDb结构,每个redisDb结构代表一个数据库:
struct redisServer{
// ...
// 一个数组,保存着服务器中的所有数据库
redisDb *db;
// ...
// 服务器的数据库数量
int dbnum;
};
在初始化服务器的时候,程序会根据服务器状态的dbnum属性来决定应该创建多少个数据库。而dbnum属性的值由服务器配置的database选项决定,默认情况下,改选项值为16,所有redis默认会创建16个数据库。
每个redis客户端都有自己的目标数据库(默认是0号数据库,可以通过SELECT
命令来切换数据库),每当客户端执行数据库写命令或者数据库读命令的时候,目标数据库就会成为这些命令的操作对象。
客户端状态redisClient结构的db属性记录了客户端当前的目标数据库,这个属性是一个指向redisDbd结构的指针:
typedef struct redisClient{
// ...
// 记录客户端当前正在使用的数据库
redisDb *db;
// ...
}redisClient;
redisCilent.db指针指向RedisServer.db数组中的其中一个元素,而被指向的元素就是客户端的目标数据库,我们可以通过修改redisCilent.db指针,让它指向服务器中的不同数据库,从而实现切换目标数据库的功能(SELECT实现原理)。
Redis是一个键值对数据库服务器,服务器中的每个数据库都由一个redis.h/redisDb结构表示,其中redisDb中的dict字典保存了数据库中的所有键值对,我们将其称为键空间:
typedef struct redisDb{
// ...
// 数据库键空间,保存着数据库中的所有键值对
dict *dict;
}redisDb;
键空间和用户所见的数据库直接对应:
对键值对的CRUD其实就是,对键空间里面的键或者键所对应的值进行CRUD操作。
除了CRUD之外,还有很多针对数据库本身的Redis命令,都是通过对键空间进行处理来完成的
eg:
FLUSHDB
RANDOMKEY
DBSIZE
EXITSTS、RENAME、KEYS
等等啊,都是通过对键空间进行操作实现的当执行Redis对数据库进行读写操作的命令时,服务器在执行的时候,还会执行一些额外的维护操作:
INFO stats
命令的keyspace_hits
属性和keyspace_misses
属性中查看。OBJECT idletime
命令查看WATCH
命令监视某个键,服务器会在这个键被修改之后,将这个键标记为脏(dirty),从而让食物程序注意到这个键被修改过。通过EXPIRE
命令或者PEXPIRE
命令,客户端可以以秒或者毫秒精度为数据库中的某个键设置生存时间,再进过指定时间之后,服务器就会自动删除生存时间为0的键。
eg:
127.0.0.1:6379> SET key value
OK
127.0.0.1:6379> EXPIRE key 5
(integer) 1
127.0.0.1:6379> GET key // 5秒之内
"value"
127.0.0.1:6379> GET key // 5秒之后
(nil)
注:
SETEX
命令可以在设置一个字符串键的同时为键设置过期时间,但是这个命令只适用于字符串键。
用户可以通过EXPIREAT
或者PEXPIREAT
命令,设置过期时间(秒或毫秒为精度),用法和上面类似。过期时间是一个UNIX时间戳,当键的过期时间到了,服务器就会自动从数据库中删除这个键。
TTL
和PTTL
接受一个带有生存时间胡总恶化过期时间点额键,返回这个键的剩余生存时间,即返回这个键距离被删除还有多长时间。eg:TTL key
解释下命令吧:
带P
的精度都是毫秒,不带P
的精度是秒
redisDb结构中的expires字典保存了数据库中所有键的过期时间,我们称之为过期字典
typedef struct redisDb{
// . . .
// 过期字典,保存着键的过期时间
dict *expires;
// . . .
}redisDb;
当我们设置了过期时间,服务器会自动往过期字典中添加数据。
PERSIST
命令可以移除一个键的过期时间。它就是在过期字典中找到给定的键,然后解除键和值(过期时间)在过期字典中的关联。 命令格式:PERSIST key
定时删除
使用定时器,定时删除过期键,可以保证尽快删除过期键,释放过期键的内存,对内存友好,对CPU不友好
惰性删除
只在取出键时才对键进行过期检查,对CPU非常友好,但是如果过期键太对的话,非常浪费内存。甚至如果我们一直不访问的话,过期键永远不会被删除,占用大量内存。
定期删除(TRUE)
每隔一段时间执行一次删除过期键操作,并且限制删除操作执行的时长和频率来减少操作对CPU时间的影响。
过期键的惰性删除策略由db.0c/expireIfNeeded函数删除,所有读写数据库的命令在执行之前都会调用expireIfNeeded函数对输入键进行检查:
过期键的定期删除有redis.c/activeExpireCycle函数实现,当Redis周期性操作redis.c/serverCron函数执行时,activeExpireCycle函数就会被调用,在规定时间内,多次遍历服务器的各个数据库,从数据库的expires字典中岁间检查出一部分键的过期时间,并删除过期键。
执行SAVE命令或者BGSAVE明理创建一个新的RDB文件时,服务器会对数据库中的键进行检查,已过期的键不会被爆粗到新创建的RDB文件中。
在启动Redis服务器是,如果开启了RDB功能的话,那么服务器将对RDB文件进行载入:
当服务器以AOF持久化模式运行时,如果数据库中的某个键已经过期,但它还没有被惰性删除或者定期删除,那么AOF文件不会因为这个过期键而产生影响。
当过期键被惰性删除或者定期删除之后,程序会想AOF文件追加(append)一条DEL命令,来显示地记录该键已被删除。
和生成的RDB文件类似,在执行AOF重写的过程中,程序会对数据库中的键进行检查,已过期的键不会被保存到重写的AOF文件中。
当服务器运行在复制模式下,从服务器的过期删除动作由主服务器控制:
综上:由主服务器来控制从服务器的删除过期键的动作,这样有利于保证主从服务器的数据一致性。
数据库通知是Redis2.8版本新增的功能,这个功能可以让客户端通过订阅给定的频道或者模式,来货值数据库中键的变化,以及数据库中命令的执行情况。
键空间通知(key-space notification):
关注某个键执行了什么命令
格式:SUBSCRIBE _ _keyspace@0_ _:键名
键事件通知(key-event notification):
关注某个命令被什么键执行了
格式:SUBSCRIBE _ _keyevent@0_ _:命令
发送数据库的通知的功能是由notif.c/notifyKeyspaceEvent函数实现的。
函数定义: void notifyKeyspaceEvent(int type, char *event, robj *key, int dbid)
举个例子吧 (SADD命令的实现函数saddCommand)
void saddCommand(redisClient *c){
// ...
// 如果至少有一个元素被成功添加,那么执行以下程序
if(added){
// ...
// 发送事件通知
notifyKeyspaceEvent(REDIS_NOTIFY_SET, "sadd", c->argv[1], c->db->id);
}
// ...
}
当SADD命令成功向集合添加了一项或者多项集合元素,命令就会发送通知,改通知的类型为REDIS_NOTIFY_SET(表示是一个集合键通知),名称为sadd(这表示是执行SADD命令所产生的通知)。
notifyKeyspaceEvent函数:
void notifyKeyspaceEvent(int type, char *event, robj *key, int dbid) {
sds chan;
robj *chanobj, *eventobj;
int len = -1;
char buf[24];
/* 如果给定的通知不是服务器允许发送的通知,则返回*/
if (!(server.notify_keyspace_events & type)) return;
eventobj = createStringObject(event,strlen(event));
/* __keyspace@__: notifications. 发送键空间通知*/
if (server.notify_keyspace_events & NOTIFY_KEYSPACE) {
chan = sdsnewlen("__keyspace@",11);
len = ll2string(buf,sizeof(buf),dbid);
chan = sdscatlen(chan, buf, len);
chan = sdscatlen(chan, "__:", 3);
chan = sdscatsds(chan, key->ptr);
chanobj = createObject(OBJ_STRING, chan);
pubsubPublishMessage(chanobj, eventobj);//发送通知
decrRefCount(chanobj);// 对象引用计数+1
}
/* __keyevente@__: notifications. 发送键事件通知*/
if (server.notify_keyspace_events & NOTIFY_KEYEVENT) {
chan = sdsnewlen("__keyevent@",11);
if (len == -1) len = ll2string(buf,sizeof(buf),dbid);
chan = sdscatlen(chan, buf, len);
chan = sdscatlen(chan, "__:", 3);
chan = sdscatsds(chan, eventobj->ptr);
chanobj = createObject(OBJ_STRING, chan);
// 这个函数是PUBLISH命令的实现函数,调用这个函数就相当于执行PUBLISH命令
pubsubPublishMessage(chanobj, key);
decrRefCount(chanobj);//对象引用计数+1
}
decrRefCount(eventobj);
}
数据库状态:Redis键值对数据库服务器中包含任意多个非空数据库,每个非空数据库又包含任意多个键值对,我们将服务器中的非空数据库以及它们的键值对统称为数据库状态。
Redis是内存数据库,它将数据库状态存在内存里,一旦服务器退出,服务器的数据库状态就会消失,所以就需要将存储在内存中的数据库状态保存到磁盘中,而这就是我们需要的RDB持久化,用于把Redis在内存中的数据库状态保存到磁盘里面,避免数据意外丢失。
RDB持久化可以通过手动执行,也能根据服务器配置选项定期执行,将某个时间点上的数据库状态保存到一个RDB文件上。我们可以通过这个文件还原数据库状态。
两个命令:SAVE、BGSAVE
用于生存RDB文件。
SVAE
会阻塞Redis服务器进程,知道RDB文件创建完毕为止,在服务器进程阻塞期间,服务器不能处理任何命令
BGSAVE
派生出一个子进程,由子进程负责创建RDB文件,服务器进程(父进程)继续处理命令请求。
创建RDB文件由rdb.c/rdbSave函数实现,SAVE
和BGSAVE
就是以不同的方式去调用这个函数,看看他们的伪代码吧:
def SAVE():
# 创建RDB文件
rdbSave();
def BGSAVE():
# 创建子进程
pid = fork();
if(pid == 0):
# 子进程负责创建RDB文件
rdbSave();
# 完成之后向父进程发送信号
signal_parent();
else pid > 0:
# 傅进臣继续处理命令请求,并通过轮询等待子进程信号
else:
# 处理出错情况
handle_fork_error();
**关于RDB文件的载入:**RDB文件的载入时服务器启动的时候就自动执行的,Redis没有专门用于载入RDB文件的命令,当Redis在启动的时候检测到RDB文件存在,就会自动载入RDB文件。
注意:AOF 文件的更新频率通常比RDB文件高,所以
载入RDB文件由rdb.c/rdbLoad函数完成,
SAVE
会阻塞Redis服务器进程,知道RDB文件创建完毕为止,在服务器进程阻塞期间,服务器不能处理任何命令。只有在服务器执行完SAVE命令、重新开始接收命令请求之后,客户端发送的命令才会被处理。
BGSAVE
派生出一个子进程,由子进程负责创建RDB文件,服务器进程(父进程)继续处理命令请求。在命令执行期间,服务器处理SAVE
、BGSAVE
、BGREWRITEAOF
三个命令的处理方式和平时不同:
BGSAVE
执行期间,SAVE
命令会被服务器拒绝,防止父进程(服务器进程)和子进程同时执行两个rdbSave
调用,发生竞争。BGSAVE
执行期间,BGSAVE
也会被拒绝,原因和上述差不多BGSAVE
和BGREWRITEAOF
不能同时执行
BGSAVE
执行时,BGREWRITEAOF
会被延迟到BGSAVE
执行结束后执行BGREWRITEAOF
执行时,BGSAVE
会被拒绝服务器在载入RDB文件时,会一直处于阻塞状态,知道载入工作完成为止
Redis 可以根据save选项设置的保存条件,自动执行BGSAVE
命令
当Redis服务器启动时,用户可以通过指定配置文件或者传入启动参数的方式设置save选项,如果用户没有主动设置save选项,那么服务器会为save设置默认条件:
save 900 1
save 300 10
save 60 10000
# 这个表示当满足下列条件 BGSAVE就会被执行
# 服务器在900秒之内,对数据库进行了至少一次修改
# 服务器在300秒内,对数据库进行了至少10次修改
# 服务器在60秒内,对数据库进行了至少10000次修改
接着,服务器程序会根据save选项所设置的保存条件,设置服务器状态redisServer
结构的sasveparams
属性:
struct redisServer{
// ...
// 记录了保存条件的数组
struct saveparam *saveparams;
// ...
};
sasveparams
属性是一个数组,数组中的每一个元素都是一个sasveparam
结构,,每个sasveparam
结构都保存了一个save选项设置的保存条件:
struct saveparam{
// 秒数
time_t seconds;
// 修改数
int change;
};
dirty 计数器
记录距离上一次成功执行SAVE
命令或者BGSAVE
命令之后,服务器对数据库状态(服务器中的所有数据库)进行了多少次修改(包括写入、删除、更新等操作)
lastsave
一个UNIX时间戳,记录了服务器上一次成功执行SAVE
命令或者BGSAVE
命令的时间
struct redisServer{
// ...
// 记录了保存条件的数组
struct saveparam *saveparams;
// 修改计数器
long long dirty;
// 上一次执行保存的时间
time_t lashsave;
// ...
};
当服务器成功执行了一个数据库修改命令之后,程就会对dirty计数器进行更新:命令修改了多少从数据库,dirty计数器的值就增加多少。
Redis 的服务器周期性操作函数serverCron默认每隔100毫秒就会执行一次,该函数用于对正在运行的服务器进行维护,她的其中一项工作就是检查save选项所设置的保存条件是否已经满足,如果满足,就执行BGSAVE
命令
瞅瞅伪代码吧:
def serverCron():
# ...
# 遍历所有保存条件
for saveparam in server.saveparams:
# 计算距离上次执行保存操作有多少秒
save_initerval = unixtime_now() - server.lastsave;
# 如果数据库状态的修改次数超过条件所设置的次数
# 并且距离上次保存的时间超过条件所设置的时间
# 那么执行保存操作
if(server.dirty >= saveparam.changes and save_interval > saveparam.seconds):
BGSAVE();
# ...
databases保存着任意多个非空数据库状态。databases其中的每个非空数据库都可以保存为SELECTDB、db——number、key_value_pairs三个部分。
RDB中的每个key_value_pairs 部分都保存着一个或以上数量的键值对,以及带有过期时间的键值对的过期时间。
TYPE
、key
、value
三部分组成TYPE
常量代表一种对象类型或者底层编码,当服务器读入RDB文件中的键值对数据是,程序或根据TYPE的值来决定如何读入和解释value的数据`
key
和value
分别存着键值对的键对象和值对象:
key
始终是一个字符串对象,编码方式和REDIS_RDB_TYPE_STRING
类型一样,长度由内容而改变
根据TYPE类型的不同,以及保存内容的长度的不同,保存value的结构和长度也会有所不同。
带过期时间的键值对在RDB文件中的结构
TYPE
、key
、value
和上面一样,不同的是EXPIRETIME_MS
和 ms
属性:
EXPIRETIME_MS
常量的长度为1字节,它告知读入程序,接下来读入的是一个以毫秒为单位的过期时间ms
是一个8字节长的带符号整数,记录着一个以毫秒为单位的UNIX时间戳,这个时间戳就是键值对的过期时间value
保存着一个值对象,每个值对象的类型都由与之对应的TYPE
记录,根据类型的不同,长度也在改变。
字符串对象
TYPE
的值为 REDIS_RDB_TYPE_STRING
,那么value保存的值对象就是一个字符串对象,字符串对象的编码可以是REDIS_ENCODING_INT
或者REDIS_ENCODING_RAW
.
如果字符串对象的编码为REDIS_ENCODING_INT
,那么说明对象中保存的是长度不超过32位的整数,由下面的结构保存。其中ENCODING
的值可以是REDIS_RDB_ENC_INT
、REDIS_RDB_ENC_INT16
或者REDIS_RDB_ENC_INT32
三个常量中其中一个,他们分别表示RDB文件使用8位、16位或者32位来保存整数值integer。
如果字符串对象的编码是REDIS_ENCODING_RAW
,那么说明对象所保存的是一个字符串值,根据字符串的长度的不同,有压缩和不压缩两种方法来保存这个字符串。
注:前提是服务器打开了RDB文件压缩功能的情况下才进行,如果没开,那就是全是无压缩。
没压缩的字符串的存放方式如下图:
string 保存的是字符串值本身,len保存字符串长度。
压缩了的字符串的存放方式如下图:
REDIS_RDB_ENC_LZF
常量标志着字符串已经被LZF算法压缩过了,读入程序碰到这个常量时,会根据之后的compressed_len
、origin_len
和compress_string
三部分吗,对字符串进行解压缩:其中compressed_len
记录的是字符串被压缩后的长度,origin_len
记录的是字符串原长度,compressed_string
记录的是被压缩之后的字符串。列表对象(TYPE = REDIS_RDB_TYPE_LIST 保存的是REDIS_ENCODING_LINKEDLIST编码)
集合对象(TYPE = REDIS_RDB_TYPE_SET 保存的是REDIS_ENCODING_HT编码)
哈希表对象(TYPE = REDIS_RDB_TYPE_HASH 保存的是REDIS_ENCODING_HT编码)
有序集合(TYPE = REDIS_RDB_TYPE_ZSET 保存的是REDIS_ENCODING_SKIPLIST编码)
如果 TYPE
的值为 REDIS_RDB_TYPE_ZSET
, 那么 value
保存的就是一个 REDIS_ENCODING_SKIPLIST
编码的有序集合对象, RDB 文件保存这种对象的结构如图 IMAGE_SKIPLIST_ZSET 所示。
sorted_set_size
记录了有序集合的大小, 也即是这个有序集合保存了多少元素, 读入程序需要根据这个值来决定应该读入多少有序集合元素。
以 element
开头的部分代表有序集合中的元素, 每个元素又分为成员(member)和分值(score)两部分, 成员是一个字符串对象, 分值则是一个 double
类型的浮点数, 程序在保存 RDB 文件时会先将分值转换成字符串对象, 然后再用保存字符串对象的方法将分值保存起来。
有序集合中的每个元素都以成员紧挨着分值的方式排列, 如图 IMAGE_MEMBER_AND_SCORE_OF_ZSET 所示。
因此, 从更详细的角度看, 图 IMAGE_SKIPLIST_ZSET 所展示的结构可以进一步修改为图 IMAGE_DETIAL_SKIPLIST_ZSET 。
作为示例, 图 IMAGE_EXAMPLE_OF_SKIPLIST_ZSET 展示了一个带有两个元素的有序集合。
在这个示例结构中, 第一个数字 2
记录了有序集合的元素数量, 之后跟着的是两个有序集合元素:
2
的字符串 "pi"
, 分值被转换成字符串之后变成了长度为 4
的字符串 "3.14"
。1
的字符串 "e"
, 分值被转换成字符串之后变成了长度为 3
的字符串 "2.7"
。INTSET编码的集合(TYPE = REDIS_RDB_TYPE_SET_INTSET 保存的是整数集合对象)
如果 TYPE
的值为 REDIS_RDB_TYPE_SET_INTSET
, 那么 value
保存的就是一个整数集合对象, RDB 文件保存这种对象的方法是, 先将整数集合转换为字符串对象, 然后将这个字符串对象保存到 RDB 文件里面。
如果程序在读入 RDB 文件的过程中, 碰到由整数集合对象转换成的字符串对象, 那么程序会根据 TYPE
值的指示, 先读入字符串对象, 再将这个字符串对象转换成原来的整数集合对象。
ZIPLIST编码的列表、哈希列表或者有序集合
如果 TYPE
的值为 REDIS_RDB_TYPE_LIST_ZIPLIST
、 REDIS_RDB_TYPE_HASH_ZIPLIST
或者 REDIS_RDB_TYPE_ZSET_ZIPLIST
, 那么 value
保存的就是一个压缩列表对象, RDB 文件保存这种对象的方法是:
如果程序在读入 RDB 文件的过程中, 碰到由压缩列表对象转换成的字符串对象, 那么程序会根据 TYPE
值的指示, 执行以下操作:
TYPE
的值,设置压缩列表对象的类型: 如果 TYPE
的值为 REDIS_RDB_TYPE_LIST_ZIPLIST
, 那么压缩列表对象的类型为列表; 如果 TYPE
的值为 REDIS_RDB_TYPE_HASH_ZIPLIST
, 那么压缩列表对象的类型为哈希表; 如果 TYPE
的值为 REDIS_RDB_TYPE_ZSET_ZIPLIST
, 那么压缩列表对象的类型为有序集合。从步骤 2 可以看出, 由于 TYPE
的存在, 即使列表、哈希表和有序集合三种类型都使用压缩列表来保存, RDB 读入程序也总可以将读入并转换之后得出的压缩列表设置成原来的类型。
介绍od
命令:
剩下的百度吧,人工分析RDB文件不是必须的,网上都有很多RDB处理文件 Redis也自带有RDB文件检查工具。