Redis设计与实现 学习笔记 第九章 数据库

ops/2024/10/21 6:18:44/

第9章到第14章属于本书第二部分:单机数据库的实现。

9.1 服务器中的数据库

Redis服务器将所有数据库都保存在服务器状态结构redis.h/redisServer的db数组中,db数组的每一项都是一个redis.h/redisDb结构,每个redisDb结构代表一个数据库

struct redisServer {// ...// 一个数组,保存着服务器中的所有数据库redisDb *db;// ...
};

在初始化服务器时,程序会根据redisServer结构的dbnum属性来决定应该创建多少个数据库

struct redisServer {// ...// 服务器的数据库数量int dbnum;// ...
};

dbnum属性的值由服务器配置的database选项决定,默认该选项值为16,所以Redis默认会创建16个数据库
在这里插入图片描述
9.2 切换数据库

每个Redis客户端都有自己的目标数据库,当客户端执行命令时,目标数据库会成为这些命令的操作对象。

默认,Redis客户端的目标数据库为0号数据库,但客户端可通过SELECT命令来切换目标数据库

以下代码示例演示了客户端切换数据库的操作:
在这里插入图片描述
上图有一个错误,第二个SET命令中,msg后面应该有一个空格。

在服务器内部,表示客户端状态的redisClient结构的db属性记录了客户端当前的目标数据库,这个属性是一个指向redisDb结构的指针:

typedef struct redisClient {// ...// 记录客户端当前正在使用的数据库redisDb *db;// ...
} redisClient;

redisClient.db指针指向redisServer.db数组中的一个元素,而被指向的元素就是客户端的目标数据库

比如,如果某客户端的目标数据库是1号数据库redisServer和redisClient的关系如图9-2所示:
在这里插入图片描述
如果这时客户端执行命令SELECT 2:
在这里插入图片描述
通过redisClient.db指针,让它指向服务器中不同的数据库,从而实现切换目标数据库的功能,这就是SELECT命令的实现原理。

谨慎处理多数据库程序,到目前为止,Redis没有返回当前目标数据库的命令。虽然redis-cli客户端会在输入符旁提示当前所使用的目标数据库
在这里插入图片描述
但如果你在其他语言的客户端中执行Redis命令,且该客户端没有像redis-cli那样显示目标数据库的号码,那么在数次切换数据库后,你可能会忘记自己当前使用的是哪个数据库。出现这种情况时,为了避免对数据库误操作,在执行Redis命令,特别是像FLUSHDB(清空数据库中所有key)这样的危险命令前,最好先执行一个SELECT命令,显式切换到指定的数据库,然后再执行。

9.3 数据库键空间

Redis是一个键值对(key-value pair)数据库服务器,其中的每个数据库都由一个redis.h/redisDb结构表示,其中,redisDb结构的dict字典保存了数据库中的所有键值对,我们称这个字典为键空间(key space):

typedef struct redisDb {// ...// 数据库键空间,保存着数据库中的所有键值对dict *dict;// ...
} redisDb;

键空间和用户所见的数据库是直接对应的:
1.键空间的键就是数据库的键,每个键都是一个字符串对象。

2.键空间的值就是数据库的值,每个值可以是一个字符串对象、列表对象、哈希表对象、集合对象、有序集合对象其中一种。

例如,我们在空白数据库中执行以下命令:
在这里插入图片描述
那么执行这些命令后,数据库的键空间会是图9-4所示的样子:
在这里插入图片描述
因为数据库的键空间是一个字典,所以所有针对键值对的操作,实际都是通过键空间字典来实现的。

9.3.1 添加新键

添加一个新键值对到数据库,实际就是将一个新键值对添加到键空间字典里。

例如,如果当前键空间状态如图9-4所示,那么执行以下命令后:
在这里插入图片描述
键空间将添加一个新键值对:
在这里插入图片描述
9.3.2 删除键

删除数据库中的一个键,实际就是删除键空间里对应的键值对对象。

例如,如果键空间当前状态如图9-4所示,那么执行以下命令后:
在这里插入图片描述
键book将从键空间中删除:
在这里插入图片描述
9.3.3 更新键

更新一个键,实际就是对键空间里对应的值对象进行更新,根据值对象的类型不同,更新的具体方法也会不同。

例如,如果键空间当前状态如图9-4所示,那么执行以下命令后:
在这里插入图片描述
键空间中,message的值对象将被改变:
在这里插入图片描述
如果我们继续执行以下命令:
在这里插入图片描述
那么键空间中的book键的值对象将被更新:
在这里插入图片描述
9.3.4 对键取值

对一个数据库键进行取值,实际就是从键空间中取出对应的值对象,根据值对象的类型不同,具体的取值方法也有所不同。

例如,如果当前键空间的状态如图9-4所示,执行以下命令时:
在这里插入图片描述
GET命令的取值过程如下图所示:
在这里插入图片描述
再举一个例子,当执行以下命令时(从alphabet列表中从左到右获取所有元素):
在这里插入图片描述
LRANGE取值过程如下图所示:
在这里插入图片描述
9.3.5 其他键空间操作

除了上面列出的添加、删除、更新、取值操作外,还有一些针对数据库本身的Redis命令,也是通过对键空间的处理来完成的。

比如,用于清空整个数据库的FLUSHDB命令,就是通过删除键空间中所有键值对来实现的。又比如,用于随机返回数据库中某个键的RANDOMKEY命令,就是通过在键空间中随机返回一个键来实现的。

另外,用于返回数据库键数量的DBSIZE命令,就是通过返回键空间中包含的键值对来实现的。类似的命令还有EXISTS、RENAME、KEYS(返回符合模式的所有key)等。

9.3.6 读写键空间时的维护操作

当对数据库进行读写时,服务器除了执行指定操作外,还会执行一些额外的维护操作:
1.读写一个键时,服务器会根据键是否存在来更新服务器的键命中次数(键存在)、键不命中次数(键不存在),这两个值可以在INFO stat命令的keyspace_hits和keyspace_misses中查看。

2.读取一个键后,服务器会更新键的LRU时间(最后一次使用时间),该值用于计算键的闲置时间,使用OBJECT idletime <key>命令可查看键key的闲置时间。

3.如果服务器在读取一个键时发现该键已过期,那么服务器会先删除这个过期键。

4.如果有客户端使用WATCH命令(用于事务,只有被监视的键没有被修改,事务才能执行成功)监视了某个键,那么被监视的键被修改后,会将这个键标记为脏(dirty),从而让事务程序注意到这个键已被修改。

5.服务器每次修改一个键后,都会对键计数器的值增1,这个计数器会触发服务器的持久化和复制操作。

6.如果服务器开启了数据库通知功能,那么在对键进行修改后,服务器将按配置发送相应的通知。

9.4 设置键的生存时间或过期时间

通过EXPIRE命令或PEXPIRE命令,客户能以秒或毫秒为精度给某个键设置生存时间(Time To Live,TTL),在经过指定的时间后,服务器会自动删除该键:
在这里插入图片描述
SETEX命令可以在创建一个字符串键的同时为它设置过期时间,这个命令是类型限定的命令(只能用于字符串键)。SETEX命令设置过期时间的原理和EXPIRE命令设置过期时间的原理是完全一样的。

客户端还可通过EXPIREAT或PEXPIREAT命令,以秒或毫秒为精度给某个键设置过期时间(expire time)。

过期时间是一个UNIX时间戳,当键到了过期时间点,服务器就会自动从数据库中删除这个键:
在这里插入图片描述
在这里插入图片描述
上图中,TIME命令的输出有两行,第一行是秒数,第二行是当前秒内过去的微秒数。

TTL命令和PTTL命令可获取键的剩余生存时间:
在这里插入图片描述
TTL和PTTL命令用于永久键时返回-1,键不存在时返回-2。

9.4.1 设置过期时间

Redis有四个命令可用于设置键的生存时间(键还可以存在多久)或过期时间(键什么时间点会被删除):
1.EXPIRE <key> <ttl>用于将键key的生存时间设为ttl秒。

2.PEXPIRE <key> <ttl>用于将键key的生存时间设为ttl毫秒。

3.EXPIREAT <key> <timestamp>用于将键key的过期时间设为timestamp指定的秒级时间戳。

4.PEXPIREAT <key> <timestamp>用于将键key的过期时间设为timestamp指定的毫秒级时间戳。

虽然有四种不同单位和形式的设置命令,但EXPIRE、PEXPIRE、EXPIREAT命令都是使用PEXPITEAT命令来实现的,服务器会将其他三个命令经过转换,最终执行PEXPITEAT命令。

首先,EXPIRE命令可以转换为PEXPITE命令:

def EXPIRE(key, ttl_in_sec):# 将TTL从秒转换成毫秒ttl_in_ms = sec_to_ms(ttl_in_sec)PEXPIRE(key, ttl_in_ms)

接着,PEXPIRE命令又可以转换成PEXPIREAT命令:

def PEXPIRE(key, ttl_in_ms):# 获取以毫秒计算的当前UNIX时间戳now_ms = get_current_unix_timestamp_in_ms()# 当前时间加上TTL,得出毫秒格式的键过期时间PEXPIREAT(key, now_ms+ttl_in_ms)

并且,EXPIREAT命令也可被转换成PEXPIREAT命令:

def EXPIREAT(key, expire_time_in_sec):# 将过期时间从秒转换为毫秒expire_time_in_ms = sec_to_ms(expire_time_in_sec)PEXPIREAT(key, expire_time_in_ms)

在这里插入图片描述
9.4.2 保存过期时间

redisDb结构的expires字典保存了数据库中所有键的过期时间,我们称其为过期字典:
1.过期字典的键是一个指针,这个指针指向键空间的中某个键。

2.过期字典的值是一个long long类型的整数,这个整数保存了键指向的数据库键的过期时间——一个毫秒精度的UNIX时间戳。

typedef struct redisDb {// ...// 过期字典,保存着键的过期时间dict *expires;// ...
} redisDb;

下图展示了一个带有过期字典的数据库,其中,键空间保存了数据库中所有的键值对,而过期字典则保存了数据库键的过期时间:
在这里插入图片描述
为了展示方便,上图中键空间和过期字典中重复出现了两次alphabet和book键对象,实际上,键空间的键和过期字典的键都指向同一个键对象,所以不会浪费空间出现重复对象。

当客户端设置对象过期时间时,服务器会在数据库的过期字典中关联给定的数据库键和过期时间。

例如,如果数据库当前状态如图9-12所示,那么在服务器执行完以下命令后:
在这里插入图片描述
过期字典将新增一个键值对:
在这里插入图片描述
以下是PEXPIREAT命令的伪代码:

def PEXPIREAT(key, expire_time_in_ms):# 如果给定的键不存在于键空间,则不能设置过期时间if key not in redisDb.dict:return 0# 在过期字典中关联键和过期时间redisDb.expires[key] = expire_time_in_ms# 过期时间设置成功return 1

9.4.3 移除过期时间

PERSIST命令可以移除一个键的过期时间:
在这里插入图片描述
PERSIST命令就是PEXPITEAT命令的反操作,PERSIST命令会将过期字典中的对应键值对删去。

例如,如果数据库当前状态如图9-12所示,那么当服务器执行以下命令后:
在这里插入图片描述
数据库状态将变为:
在这里插入图片描述
可见,过期字典中原来的book键值对消失了。

以下是PERSIST命令的伪代码:

def PERSIST(key):# 如果键不存在,或键没有设置过期时间,那么直接返回if key not in redisDb.expires:return 0# 移除过期字典中给定键的键值对关联redisDb.expires.remove(key)# 键的过期时间移除成功return 1

9.4.4 计算并返回剩余生存时间

TTL命令以秒为单位返回键的剩余生存时间,PTTL命令以毫秒为单位返回键的剩余生存时间:
在这里插入图片描述
TTL和PTTL命令都是通过计算键的过期时间和当前时间的差来实现的,以下是这两个命令的伪代码:

def PTTL(key):# 键不存在于数据库if key not in redisDb.dict:return -2# 尝试获取键的过期时间# 如果键没有设置过期时间,那么expire_time_in_ms将为Noneexpire_time_in_ms = redisDb.expires.get(key)# 键没有设置过期时间if expire_time_in_ms is None:return -1# 获取当前时间now_ms = get_current_unix_timestamp_in_ms()# 过期时间减去当前时间,差为键的剩余生存时间return (expire_time_in_ms - now_ms)def TTL(key):# 获取以毫秒为单位的剩余生存时间ttl_in_ms = PTTL(key)if ttl_in_ms < 0:# 处理返回值为-2和-1的情况return ttl_in_mselse:# 将毫秒转换为秒return ms_to_sec(ttl_in_ms)

9.3.5 过期键的判定

通过过期字典,程序用以下步骤检查键是否过期:
1.检查给定键是否存在于过期字典,如果存在,取得键的过期时间。

2.检查当前UNIX时间戳是否大于键的过期时间,如果是,那么键已过期;否则,键未过期。

可用伪代码描述这一过程:

def is_expired(key):# 取得键的过期时间expire_time_in_ms = redisDb.expires.get(key)# 键没有设置过期时间if expire_time_in_ms is None:return False# 取得当前时间的UNIX时间戳now_ms = get_current_unix_timestamp_in_ms()# 检查当前时间是否大于键的过期时间if now_ms > expire_time_in_ms:# 是,键已过期return Trueelse:# 否,键未过期return False

实现过期键判定的另一种方法是使用TTL或PTTL命令,如果对某个键执行这两个命令,返回值大于0说明键未过期。实际上,Redis使用is_expired伪代码描述的方式来检查键是否过期,因为直接访问字典比执行一个命令稍快。

9.5 过期键的删除策略

有三种删除策略:
1.定时删除:在设置键的过期时间的同时,创建一个定时器(timer),让定时器在键过期时执行对键的删除操作。

2.惰性删除:放任过期键不管,但每次从键空间中获取键时,都检查取得的键是否过期,如果过期,就删除该键;如果没过期,就返回该键。

3.定期删除:每隔一段时间,程序就对数据库进行一次检查,删除里面的过期键。至于要删除多少过期键、检查多少数据库,则由算法决定。

这三种策略中,第一种和第三种为主动删除策略,第二种为被动删除策略。

9.5.1 定时删除

定时删除策略对内存是最友好的,通过定时器可以保证过期键能尽可能快地被删除,从而立即释放过期键占用的内存。

但定时策略对CPU时间最不友好,在过期键比较多的情况下,删除过期键的行为可能会占用相当一部分CPU时间,在内存不紧张但CPU时间非常紧张的情况下,将CPU时间用在删除和当前任务无关的过期键上,无疑会对服务器的响应时间和吞吐量造成影响。

此外,创建定时器需要用到Redis服务器的时间事件,而当前时间事件的实现方式是无序链表,查找一个事件的时间复杂度为O(N),不能高效地处理大量时间事件。

因此,要让服务器大量创建定时器,从而实现定时删除策略,在现阶段来说不现实。

9.5.2 惰性删除

惰性删除策略对CPU时间来说是最友好的,程序只会在取出键时才进行过期检查,这保证了删除过期键的操作只会在非做不可的情况下进行,且删除的目标仅限于当前处理的键。

但惰性删除策略对内存是最不友好的,如果一个键已过期,又没有被访问,它所占用的内存就不会释放。

使用惰性删除策略时,如果数据库中有非常多过期键,而这些过期键又没有被访问到的话,它们也许永远不会被删除(除非用户手动执行FLUSHDB),我们甚至可以将这种情况看作一种内存泄漏,无用的垃圾占用了大量内存,而服务器却不会自己去释放它们,这对于运行状态非常依赖于内存的Redis服务器来说,不是一个好消息。

例如,对于一些和时间有关的数据,比如日志(log),在某个时间点后,对它们的访问会大大减少,甚至不再访问,如果这类过期数据大量积压在数据库中,用户以为服务器已经自动将它们删除了,但实际上这些键仍存在,且键占用的内存也没有释放,那造成的后果是非常严重的。

9.5.3 定期删除

定期删除策略是前两种策略的一种整合和折中:
1.定期删除策略每隔一段时间执行一次删除过期键的操作,并通过限制删除操作执行的时长和频率来减少对CPU时间的影响。

2.通过定期删除过期键,有效减少了因过期键带来的内存浪费。

定期删除策略的难点是确定删除操作执行的时长和频率:
1.如果删除操作执行得太频繁,或执行的时间太长,定期删除策略就会将CPU时间过多地消耗在删除过期键上。

2.如果删除操作执行的太少,或执行的时间太短,定期删除策略又会出现浪费内存的情况。

9.6 Redis的过期键删除策略

Redis服务器使用的是惰性删除和定期删除两种策略,可以很好地在合理使用CPU时间和避免内存浪费之间取得平衡。

9.6.1 惰性删除策略的实现

过期键的惰性删除策略由db.c/expireIfNeeded函数实现,所有读写命令执行前都会调用expireIfNeeded函数对键进行检查:
1.如果输入键已过期,那么expireIfNeeded函数会将键删除。

2.如果输入键未过期,那么expireIfNeeded函数不做动作。

命令调用expireIfNeeded函数的过程:
在这里插入图片描述
expireIfNeeded函数就像一个过滤器,它可以在真正执行命令前,过滤掉过期的输入键,从而避免命令接触到该键。

例如,下图展示了GET命令的执行过程:
在这里插入图片描述
9.6.2 定期删除策略的实现

过期键的定期删除策略由redis.c/activeExpireCycle函数实现,每当Redis的服务器周期性执行redis.c/serverCron函数时,activeExpireCycle函数就会被调用,它在规定时间内,遍历服务器中的各个数据库,从数据库的expires字典中随机检查一部分键的过期时间,并删除其中的过期键。

定期删除策略的伪代码描述:

# 默认每次检查的数据库数量
DEFAULT_DB_NUMBERS = 16# 默认每个数据库检查的键数量
DEFAULT_KEY_NUMBERS = 20# 全局变量,记录检查进度
current_db = 0def activeExpireCycle():# 初始化要检查的数据库数量# 如果服务器的数据库数量比DEFAULT_DB_NUMBERS要小# 那么以服务器的数据库数量为准if server.dbnum < DEFAULT_DB_NUMBERS:db_numbers = server.dbnumelse:db_numbers = DEFAULT_DB_NUMBERS# 遍历各个数据库for i in range(db_numbers):# 如果current_db的值等于数据库总数# 表示检查程序已经遍历了所有服务器# 将current_db重置为0,开始新一轮遍历if current_db == server.dbnum:current_db = 0# 获取当前要处理的数据库redisDb = server.db[current_db]# 将数据库索引增1,指向下一个要处理的数据库current_db += 1# 检查数据库for j in range(DEFAULT_KEY_NUMBERS):# 如果数据库中没有一个键带有过期时间,那么跳过这个数据库if redisDb.expires.size() == 0: break# 随机获取一个带有过期时间的键key_with_ttl = redisDb.expires.get_random_key()# 检查键是否过期,如果过期就删除它if is_expired(key_with_ttl):delete_key(key_with_ttl)# 已达到删除操作的时间上限,停止处理if reach_time_limit(): return        

activeExpireCycle函数的工作模式可总结如下:
1.函数每次运行时,都从一定数量的数据库中取出一定数量的随机键进行检查,并删除其中的过期键。

2.全局变量current_db会记录当前检查的进度,并在下一次activeExpireCycle函数调用时,接着上一次的进度进行处理。

3.随着activeExpireCycle函数的不断执行,所有数据库都会被检查一遍,这时current_db会重置为0,然后再次开始新一轮检查。

9.7 AOF、RDB和复制功能对过期键的处理

9.7.1 生成RDB文件(Redis DataBase,RDB是Redis用于持久化数据库的一种文件格式)

在执行SAVE命令(保存当前数据库的快照到磁盘,期间会阻塞所有客户端请求)或BGSAVE命令(创建一个子进程,异步地保存快照)创建一个新的RDB文件时,程序会对数据库中的键进行检查,已过期的键不会保存到新创建的RDB文件中。

9.7.2 载入RDB文件

启动Redis服务器时,如果服务器开启了RDB功能,则会载入RDB文件:
1.如果服务器以主服务器模式运行,那么在载入RDB文件时,程序会对文件中保存的键进行检查,未过期的键会被载入到数据库,而过期键会被忽略。

2.如果服务器以从服务器模式运行,那么在载入RDB文件时,文件中保存的所有键,无论是否过期,都会被载入到数据库中。但主从服务器进行数据同步前,从服务器的数据库就会被清空,所以将RDB中的过期键载入从服务器也不会造成影响。

9.7.3 AOF文件(Append Only File,Redis持久化数据的一种方法,通过记录数据库操作日志来实现)写入

当服务器以AOF持久化模式运行时,如果数据库中某个键已过期,但还没被删除,那么AOF文件中还是会有该过期键。

当过期键被惰性删除或定期删除后,程序会向AOF文件追加(append)一条DEL命令,来显式地记录该键已被删除。

例如,客户端使用GET message命令,试图访问过期的message键,那么服务器将执行以下三个动作:
1.删除message键。

2.追加一条DEL message命令到AOF文件。

3.向执行GET命令的客户端返回空回复。

9.7.4 AOF重写(优化AOF文件大小和效率的过程,将冗余操作优化掉,可以使重放的效率更高,空间占用更小)

在执行AOF重写过程中,程序会对键进行检查,过期的键不会被保存到重写后AOF文件中。

9.7.5 复制

当服务器运行在复制模式下,从服务器的过期键删除动作由主服务器控制:
1.主服务器在删除一个过期键后,会显式向所有从服务器发送一个DEL命令,告知从服务器删除这个过期键。

2.从服务器在执行客户端发送的读命令时,即使碰到过期键也不会将过期键删除,而是将它当成未过期键来处理。

3.从服务器只有接到主服务器发来的DEL命令后,才删除该键。

例如,有一对主从服务器,它们都保存着同样的三个键:
在这里插入图片描述
如果这时有客户端向从服务器发送命令GET message,那么从服务器将发现message键已过期,但从服务器不会删除message键 ,而是将message键的值返回给客户端,就像message键没有过期一样:
在这里插入图片描述
假设在此之后,有客户端向主服务器发送命令GET message,那么主服务器将发现键message已过期,主服务器会删除message键,向客户端返回空回复,并向从服务器发送DEL message命令:
在这里插入图片描述
从服务器接收到主服务器发来的DEL message命令后,会将从服务器里的message键删除:
在这里插入图片描述
9.8 数据库通知

数据库通知是Redis 2.8版本新加的功能,这个功能可以让客户端通过订阅给定的频道或模式,来获知数据库中键的变化,以及数据库中命令的执行情况。

例如,以下代码展示了客户端如何获取0号数据库中针对message键执行的所有命令:
在这里插入图片描述
在这里插入图片描述
上图中,数据库标识为__keyspace@0__,两个下划线应该是连续的。

根据发回的通知显示,先后共有SET、EXPIRE、DEL三个命令对键message进行了操作。

这一类关注“某个键执行了什么命令”的通知称为键空间通知(key-space notification),此外,还有一类称为键事件通知(key-event notification)的通知,它们关注的是“某个命令被什么键执行了”。

以下是一个键事件通知的例子,代码展示了客户端如何获取0号数据库中所有执行DEL命令的键:
在这里插入图片描述
根据发回的通知显示,key、number、message三个键先后执行了DEL命令。

服务器配置的notify-keyspace-events选项决定了服务器能发哪些通知:
1.值为AKE时,能发送所有键空间通知和键事件通知。

2.值为AK时,只能发送所有键空间通知。

3.值为AE时,只能发送所有键事件通知。

4.值为K$时,只能发送和字符串键有关的键空间通知。

5.值为El时,只能发送和列表键有关的键事件通知。

有关数据库通知功能的详细用法,以及notify-keyspace-events选项的更多设置见Redis官方文档(总结一下,E表示开键事件通知,K表示开键空间通知,其他字符表示键类型,如$表示字符串命令,g表示非类型特定的通用命令)。

9.8.1 发送通知

发送数据库通知的功能是由notify.c/notifyKeyspaceEvent函数实现的:

void notifyKeyspaceEvent(int type, char *event, robj *key, int dbid);

type参数是想要发送的通知类型,程序会根据此参数来判断通知是否是notify-keyspace-events选项所允许的类型,从而决定是否发送通知。

event、key、dbid参数分别是事件名(一个字符串,如set、sadd等)、产生事件的键、产生事件的数据库号。之后函数就会根据参数构建通知内容、接收通知的频道名。

Redis命令会调用notifyKeyspaceEvent函数,例如,以下是SADD命令的实现函数saddCommand的一部分代码:

void saddCommand(redisClient *c) {// ...// 如果至少有一个元素被成功添加if (added) {// ...// 发送事件通知,第一个type参数中的SET表示这是字符串操作notifyKeyspaceEvent(REDIS_NOTIFY_SET, "sadd", c->argv[1], c->db->id);}
}

以下是另一个例子,展示了DEL命令的实现函数delCommand的一部分代码:

void delCommand(redisClient *c) {int deleted = 0, j;// 遍历所有输入键for (j = 1; j < c->argc; j++) {// 尝试删除键if (dbDelete(c->db, c->argv[i])) {// ...// 删除成功,发送通知,第一个type参数中的GENERIC表示这是非类型特定的通用操作notifyKeyspaceEvent(REDIS_NOTIFY_GENERIC, "del", c->argv[j], c->db->id);// ...}}// ...
}

以上函数中,遍历了所有输入键,并在删除成功时发送通知,通知类型为REDIS_NOTIFY_GENERIC(一个通用类型的通知,通用类型指的是DEL、EXPIRE、RENAME等非类型特定的操作),通知名为del。

9.8.2 发送通知的实现

以下是notifyKeyspaceEvent函数的伪代码实现:

def notifyKeyspaceEvent(type, event, key, dbid):# 如果给定的通知不允许发送,那么直接返回# 这里相当于判断了notify-keyspace-events字符串命令、通用命令、列表命令等是否允许发通知if not(server.notify_keyspace_events & type):return# 发送键空间通知,这里相当于判断了notify-keyspace-events选项是否有Kif server.notify_keyspace_events & REDIS_NOTIFY_KEYSPACE:# 将通知发送给频道__keyspace@<dbid>__:<key># 内容为键发生的事件<event># 构建频道名chan = "__keyspace@{dbid}__:{key}".format(dbid=dbid, key=key)# 发送通知pubsubPublishMessage(chan, event)# 发送键事件通知,这里相当于判断了notify-keyspace-events选项是否有Eif server.notify_keyspace_events & REDIS_NOTIFY_KEYEVENT:# 将通知发送给频道__keyevent@<dbid>__:<event># 内容为发生事件的键<key># 构建频道名chan = "__keyevent@{dbid}__:{event}".format(dbid=dbid, event=event)# 发送通知pubsubPublishMessage(chan, key)

另外,pubsubPublishMessage函数时PUBLISH命令的实现函数,执行这个函数等同于执行PUBLISH命令,该函数的具体实现参考第18章。

9.9 重点回顾

1.Redis的所有数据库都保存在redisServer.db数组中,而数据库的数量由redisServer.dbnum属性保存。

2.客户端通过修改目标数据库指针,让它指向redisServer.db数组中的不同元素来切换不同数据库

3.数据库主要由dict和expires两个字典构成,其中dict字典负责保存键值对,expires字典负责保存键的过期时间。

4.因为数据库由字典构成,所以对数据库的操作都是建立在字典操作上的。

5.数据库的键总是一个字符串对象,而值可以是任意一种Redis对象类型,包括字符串对象、哈希表对象、集合对象、列表对象、有序集合对象。

6.expires字典的键指向数据库中的某个键,而值记录了数据库键的过期时间,过期时间是一个以毫秒为单位的UNIX时间戳。

7.Redis使用惰性删除和定期删除两种策略来删除过期的键:惰性删除策略只在碰到过期键时才进行删除操作,定期删除策略则每隔一段时间主动查找并删除过期键。

8.执行SAVE命令或BGSAVE命令所产生的RDB文件不会包含过期键。

9.执行BGWRITEAOF命令后产生的重写AOF文件不会包含过期键。

10.当一个过期键被删除后,服务器会追加一条DEL命令到现有AOF文件的末尾,显式地删除过期键。

11.当主服务器删除一个过期键后,它会向所有从服务器发送一条DEL命令,显式地删除过期键。

12.从服务器即使发现过期键也不会主动删除它,而是将它当作未过期键处理,等待主节点发来的DEL命令,这种统一、中心化的过期键删除策略可以保证主从服务器数据的一致性。

13.当Redis命令对数据库进行修改后,服务器会根据配置向客户端发送数据库通知。


http://www.ppmy.cn/ops/127199.html

相关文章

Rancher—多集群Kubernetes管理平台

目录 一、Rancher 简介1.1 Rancher 和 k8s 的区别 二、Rancher 安装及配置2.1 安装 rancher2.2 登录 Rancher 平台2.3 Rancher 管理已存在的 k8s 集群2.4 创建名称空间 namespace2.5 创建 Deployment 资源2.6 创建 service2.7 Rancher 部署监控系统 一、Rancher 简介 Rancher …

PicoQuant GmbH公司Dr. Christian Oelsner到访东隆科技

昨日&#xff0c;德国PicoQuant公司的光谱和显微应用和市场专家Dr.Christian Oelsner莅临武汉东隆科技有限公司。会议上Dr. Christian Oelsner就荧光寿命光谱和显微技术的最新研究和应用进行了深入的交流与探讨。此次访问不仅加强了两家公司在高科技领域的合作关系&#xff0c;…

D43【python 接口自动化学习】- python基础之函数

day43 装饰器&#xff08;上&#xff09; 学习日期&#xff1a;20241020 学习目标&#xff1a;函数&#xfe63;- 56 装饰器&#xff1a;函数嵌套的定义与调用的区别 学习笔记&#xff1a; 变量作用域 变量读取顺序&#xff1a;local-》enclosed-》global-》builtin # 变量…

10-1.idea中的项目结构,辅助快捷键,模块的操作

idea中的项目结构和辅助快捷键 IDEA中项目结构 首先是创建项目&#xff0c;新建的项目中有子项目&#xff0c;我们可以创建模块 然后在模块中我们可以创建包&#xff0c;在包中的SRC中写我们的源代码&#xff0c;也就是类。 VScode写Java项目 如何你电脑比较卡的话&#…

isis 不同区域的配置实验

一、同区域下&#xff0c;Level-1、Level-2、Level-1-2 之间的拓扑计算&#xff1a; 1、拓扑1&#xff1a; 2、配置&#xff1a; [R2]: isis 1is-level level-2cost-style widenetwork-entity 49.0001.0000.0000.0002.00is-name R2 # interface GigabitEthernet0/0/1ip addres…

SVN小乌龟 create patch 和 apply patch 功能

在SVN&#xff08;Subversion&#xff09;版本控制系统中&#xff0c;使用“小乌龟”&#xff08;TortoiseSVN&#xff09;这个图形界面工具可以极大地简化SVN操作。TortoiseSVN中的“create patch”和“apply patch”是两个非常有用的功能&#xff0c;它们与版本控制中的补丁&…

JVM篇(Java内存区域与内存溢出异常(深入理解JVM第三版))(持续更新迭代)

目录 学习前言 一、内存管理控制 1. C/C程序员 2. Java程序员 二、运行时数据区&#xff08;深入理解JVM第三版&#xff09; 1. 程序计数器 2. Java虚拟机栈 3. 本地方法栈 4. Java堆 5. 方法区 运行时常量池 学习前言 JVM学习预热后&#xff0c;我们涉及到了JVM的…

小柯:缓解生活压力 戏剧改变人生“免费戏剧训练营”正式启动

10月13日&#xff0c;著名音乐人小柯在社交平台发布了一条视频&#xff0c;称自己要做一件“大事”&#xff0c;而引发广泛关注与热议。在小柯剧场迎来其十二周年之际&#xff0c;为回馈广大观众一直以来的支持与厚爱&#xff0c;特别策划了一场全免费的戏剧训练营活动。凡是年…