前言
在 Redis详解——存储 中介绍了Redis的基础数据结构,本文我们来看看Redis是如何组织这些数据类型,来构建一个内存数据库的。
一、内存数据库
以下是Redis数据库的结构:
Redis服务器程序所有的数据库都保存在redisService结构体中,其中有个db数组,为redisDb类型,每个元素为一个数据库。
db数组可配置,默认为16个,redisDb中保存了一个字典,该字典保存了数据库中所有的键值对,我们也称该字典为键空间(key space)。
字典的key为String类型,字典的value就是我们上一节提到的各种数据类型了,这些数据类型让Redis可以存储多样化的数据,利用特定的数据结构实现一些业务场景。
客户端连接哪个数据库? 默认情况下,Redis客户端会连接0号数据库,可以通过SELECT命令切换数据库。
127.0.0.1:6379> SET name arthinking
OK
127.0.0.1:6379> GET name
"arthinking"
127.0.0.1:6379> DEL name
(integer) 1
127.0.0.1:6379> GET name
(nil)
二、读写键的时候做了啥
当通过命令对数据库进行了读写之后,Redis同时会做一些维护工作:
三、如何存储键的过期时间
3.1、过期相关命令
- EXPIRE key seconds,设置key的生存秒数;
- PEXPIRE key milliseconds,设置key的生存毫秒数;
- EXPIREAT key timestamp,设置key的过期时间戳(秒);
- PEXPIREAT key timestamp,设置key的过期时间戳(毫秒)
- SETEX,设置一个字符串的过期时间;
- TTL与PTTL,接收一个带有生存时间的键,返回键的剩余生成时间。
这些设置过期时间命令,本质上都会转成PEXPIREAT命令来执行,数据库中存储的是键的过期时间点。
3.2、应该使用哪个过期命令比较靠谱?
注意:建议直接使用EXPIREAT命令来设置过期时间,避免主从同步延迟,导致从库实际的EXPIREAT时间比主库的晚,最终客户端在从库上读取到了过期的数据(主库已过期,从库未过期)。
3.3、过期字典
我们注意到,上面的数据库结构图中,包含了一个expires过期字典,该字典的键是一个纸指向键空间中某个数据库键的指针,值是一个long long类型的整数,保存数据库键的过期时间(毫秒时间戳)。
四、如何删除过期键
在一般程序设计中,我们也会用三种策略来实现数据的过期删除:
定时删除策略是一种方案,但是如果设置的不合理,就会即浪费CPU,或者内存及时删除。为此,Redis采用了惰性删除和定期删除配合工作的方式。
- Redis中的惰性删除:接收读写数据库命令,判断是否已过期,如果过期则删除并返回空回复,否则执行实际的命令流程;
- Redis中的定期删除:每次运行,从一定量数据库中取出一定量随机键进行检查,然后把过期的键删除掉,通过一个全局表示current_db记录处理进度,确保所有数据库都可以被处理。
4.1、从库的KEY过期了可以被清理掉吗?
当主库键key过期时时,会同步一个DEL操作到从库,从库不会自己删除过期key,只会应用从主库同步过来的DEL操作,这样就避免了缓存一致性的错误。
这样就会有一个问题,如果从库在同步DEL操作之前,就有客户端请求从库获取key,那么就有可能读取到主库已经删除,但是从库还未删除的key。
好在从Redis 3.2开始,对从库读取key做了优化:在从库发起读请求的时候,会先判断这个key是否过期,如果过期了,就直接返回nil,从而避免了在从库中读取到了过期的key的问题。
另外:建议直接使用EXPIREAT命令来设置过期时间,避免主从同步延迟,导致从库实际的EXPIREAT时间比主库的晚,最终客户端在从库上读取到了过期的数据(主库已过期,从库未过期)。
五、Redis中的发布订阅机制
Redis的发布订阅功能有以下命令组成:
-
SUBSCRIBE channel [channel ...]
- 订阅给定的一个或多个频道的信息;
-
-
SUBSCRIBE channel
-
-
UNSUBSCRIBE [channel [channel ...]]
- 退订给定的频道
-
-
UNSUBSCRIBE channel
-
-
PUBLISH channel message
- 用于将信息发送到指定的频道;
-
-
PUBLISH channel itzhai.com
-
-
PUBSUB subcommand [argument [argument ...\]]
- 查看订阅与发布系统状态
-
-
PUBSUB CHANNELS
-
-
PSUBSCRIBE pattern [pattern ...]
- 订阅一个或多个符合给定模式的频道
-
-
PSUBSCRIBE site.*
-
-
PUNSUBSCRIBE [pattern [pattern ...\]]
- 退订给定模式的频道
-
-
UNSUBSCRIBE site.*
-
如下,通过给定的模式频道进行订阅和发布消息:
# 客户端A订阅模式频道
127.0.0.1:6379> PSUBSCRIBE site.*
psubscribe
site.*
1
# 客户端B向模式频道发布消息
127.0.0.1:6379> PUBLISH site.itzhai "hello world!!!"
1
# 客户端A输出
pmessage
site.*
site.itzhai
hello world!!!
Redis在服务端通过链表的形式维护了每个频道的客户端的订阅记录,每次发布消息的时候,都从链表中找到所有相关的客户端的socket连接,并发送订阅消息给各个客户端。Redis中存放客户端订阅关系的相关数据结构:
struct redisServer {
// ...
dict *pubsub_channels; // 保存所有频道订阅关系
list *pubsub_patterns; // 保存所有模式订阅关系
// ...
};
具体结构如下图所示:
在dict字典中,每个键值对存储一个频道的订阅关系,key为频道名称,value为链表结构,存储该频道所有订阅的客户端。
每当执行SUBSCRIBE命令的时候,执行把客户端追加到字典中对应频道的key的values链表中即可。
每当执行PUBLISH命令的时候,从字典中找到对应的频道键值对,依次遍历values中所有的客户端进行发送消息即可。
在list链表中,保存了所有的模式频道订阅关系。
每当执行PUBLISH命令的时候,除了在pubsub_channels寻找频道订阅关系,发给具体的频道的所有客户端之外,同时,Redis会遍历pubsub_patterns中的所有订阅模式频道,找到与当前发布消息频道匹配的模式频道,将消息发送给该模式频道的客户端。
5.1、发布订阅的优缺点
通过使用Redis的发布订阅机制,很容易实现消息的订阅与通知,可以快速实现一个消息队列。
但是,该消息队列的缺点也比较明显,请大家慎用:
- 发布的消息不支持持久化,如果Redis挂了,那么发布的消息也就丢失了;或者消息发送给了一半的订阅者,Redis就挂了,那么剩下的一般订阅者也就不会收到消息了;或者准备发送消息给其中一个订阅者的时候,该订阅者失去连接了,消息也会丢失;
- 消息发送缺少ACK机制,不能保证消息一定会被消费...
针对可靠性要求低的业务,为了简单快速实现,可以使用Redis的发布订阅机制来实现消息通知。而对于可靠性要求比较高的,则可以尝试Redis 5.0的新数据结构Stream,具体用法和原理。
六、实现数据库通知
基于Redis的发布订阅机制,我们就可以实现数据库通知功能了。该功能常常用于作为对数据或者命令的监控。
因为开启数据库通知需要消耗一定的CPU,所以默认配置下,是关闭状态的。为了开启这个功能更,我们可以修改redis.conf文件:
notify-keyspace-events KElg
如上,我们开启了:
- K:键空间通知,所有通知以__keyspace@<db>__为前缀;
- E:键事件通知,所有通知以__keyevent@<db>__为前缀;
- l:列表命令通知;
- g:DEL 、 EXPIRE 、 RENAME 等类型无关的通用命令的通知。
更多关于notify-keyspace-events的配置,请参考官方文档:Redis Keyspace Notifications
现在我们启动Redis服务器,就支持数据库通知了。
现在我们在一个客户端1订阅一个键空间通知,监听我的钱包my_money:
127.0.0.1:6379> SUBSCRIBE __keyspace@0__:my_money
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "__keyspace@0__:my_money"
3) (integer) 1
在另一个客户端2,给我的钱包打100块钱看看:
127.0.0.1:6379> SET my_money 100
OK
结果我们在客户端1可以收到以下到账通知:
1) "message"
2) "__keyspace@0__:my_money"
3) "set"
另外,我们也可以监听某一个命令:
127.0.0.1:6379> SUBSCRIBE __keyevent@0__:del
七、Redis客户服务程序设计
Redis在传输层,使用的是TCP协议,每当有客户端连接到服务器的时候,都会创建一个Socket连接,对应一个套接字文件描述符fd。
而在Redis服务器中,是如何维护这些网络连接的呢,接下来我们就来看看。
7.1、客户端信息
当客户端与Redis服务器连接之后,redisServer结构中会存储一个客户端的链表,该链表节点用来记录与客户端通信的各种信息:
我们重点来关注redisClient以下信息:
-
int fd
:文件描述符,-1代表伪客户端; -
robj * name
:客户端名字; -
int flags
:客户端标志,flags可以是单个标志,或者多个标志的二进制或,常见的标志:- REDIS_LUA_CLIENT:表示客户端是用于处理Lua脚本的伪户端;
- REDIS_MONITOR:客户端正在执行MONITOR命令;
- REDIS_UNIX_SOCKET:服务器使用UNIX套接字来连接客户端;
- REDIS_BLOCKED:客户端正在被BRPOP、BLPOP等命令阻塞;
- REDIS_UNBOKCKED:表示客户端已从阻塞状态中脱离出来,该标志只能在REDIS_BLOCKED标志已经打开的情况下使用;
- REDIS_MULTI:客户端正在执行事务
- REDIS_FORCE_AOF:强制将执行的命令写入到AOF文件里面,一般情况,Redis只会对数据库进行了修改的命令写入到AOF文件中,而对于PUBSUB和SCRIPT LOAD命令,则需要通过该标志,强制将这个命令写入到AOF文件中,另外,为了让主从服务器可以正确载入SCRIPT LOAD命令指定的脚本,需要使用REDIS_FORCE_REPL标志,强制将SCRIPT LOAD命令复制给所有从服务器;
-
sds querybuf
:客户端输入缓冲区,命令请求字符串,最大大小不能超过1G,否则客户端将被服务器关闭; -
robj **argv
:要执行的命令,以及所有参赛构成的数组; -
int argc
:argv数组的长度; -
struct redisCommand *cmd
:客户端请求对应的命令,从字典结构的命令表中查找得到; -
char buf[REDIS_REPLY_CHUNK_BYTES]
:固定大小缓冲区,REDIS_REPLY_CHUNK_BYTES值默认为16KB; -
int bufpos
:固定大小缓冲区易用字节数量; -
list *reply
:可变大小缓冲区由链表组成,每个节点为一个字符串对象; -
int authenticated
:客户端身份验证状态,1表示验证通过,如果Redis启用了身份验证功能,则需要用到该字段; -
time_t ctime
:客户端连接创建时间; -
time_t lastinteraction
:客户端与服务器最后一次通信时间; -
time_t obuf_soft_limit_reached_time
:输出缓冲区第一次达到软性限制的时间;
7.2、命令执行流程
一个命令执行的完整流程如下图所示:
可以发现,Redis执行命令的整个过程,相关的中间信息都存储在redisClient中。
标签:订阅,过期,数据库,Redis,详解,内存,key,客户端 From: https://blog.51cto.com/u_14014612/6607392