首页 > 数据库 >Redis详解——内存数据库

Redis详解——内存数据库

时间:2023-07-02 22:32:43浏览次数:40  
标签:订阅 过期 数据库 Redis 详解 内存 key 客户端

前言

Redis详解——存储 中介绍了Redis的基础数据结构,本文我们来看看Redis是如何组织这些数据类型,来构建一个内存数据库的。

一、内存数据库

以下是Redis数据库的结构: image.png

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同时会做一些维护工作: image.png

三、如何存储键的过期时间

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类型的整数,保存数据库键的过期时间(毫秒时间戳)。

image.png

四、如何删除过期键

在一般程序设计中,我们也会用三种策略来实现数据的过期删除: image.png

定时删除策略是一种方案,但是如果设置的不合理,就会即浪费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;  // 保存所有模式订阅关系
    // ...
};

具体结构如下图所示: image.png

在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结构中会存储一个客户端的链表,该链表节点用来记录与客户端通信的各种信息: image.png

我们重点来关注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、命令执行流程

一个命令执行的完整流程如下图所示: image.png

可以发现,Redis执行命令的整个过程,相关的中间信息都存储在redisClient中。

 

参考: https://www.itzhai.com/articles/redis-technology-insider-cache-data-structure-concurrency-clustering-and-algorithm.html

标签:订阅,过期,数据库,Redis,详解,内存,key,客户端
From: https://blog.51cto.com/u_14014612/6607392

相关文章

  • springboot+token+redis,模拟登录
    登录测试的controller:loginTest.javapackagecom.example.demo.controller;importcom.example.demo.po.ResponseBean;importorg.springframework.data.redis.core.RedisTemplate;importorg.springframework.web.bind.annotation.*;importjavax.annotation.Resource;i......
  • Python | with关键字详解
    with使用背景对于系统资源如文件、数据库连接、socket而言,应用程序打开这些资源并执行完业务逻辑之后,必须做的一件事就是要关闭(释放)该资源。比如Python程序打开一个文件,往文件中写内容,写完之后,就要关闭该文件,如果不关闭会出现什么情况呢?极端情况下会出现Toomanyopenfiles......
  • Redis数据结构——链表
    前言Redis链表为双向无环链表!Redis使用了简单动态字符串,链表、字典(散列表)、跳跃表、整数集合、压缩列表这些数据结构来操作内存。本文继续来分析链表。 链表是一种非常常见的数据结构,在Redis中使用非常广泛,列表对象的底层实现之一就是链表。其它如慢查询,发布订阅,监视器等功......
  • Redis数据结构——字典
    前言字典在Redis中的应用非常广泛,数据库与哈希对象的底层实现就是字典。一、复习散列表1.1散列表散列表(哈希表),其思想主要是基于数组支持按照下标随机访问数据时间复杂度为O(1)的特性。可以说是数组的一种扩展。假设,我们为了方便记录某高校数学专业的所有学生的信息。要求可......
  • Redis数据结构——简单动态字符串SDS
    前言相信用过Redis的人都知道,Redis提供了一个逻辑上的对象系统构建了一个键值对数据库以供客户端用户使用。这个对象系统包括字符串对象、哈希对象、列表对象、集合对象、有序集合对象等。但是Redis面向内存并没有直接使用这些对象。而是使用了简单动态字符串,链表、字典(散列表)、......
  • sftp命令详解
    Lookslikesftpdoesn't distinguishBinaryfilesandASCIIfilesatall.Thatmeansitdoesntsupportthecommandslike'bin'or'ascii'thatftpsupports.sftp>helpAvailablecommands:byeQui......
  • 【WALT】WALT入口 update_task_ravg() 代码详解
    目录【WALT】WALT入口update_task_ravg()代码详解代码展示代码逻辑⑴ 判断是否进入WALT算法⑵ 获取WALT算法中上一个窗口的开始时间⑶如果任务刚初始化结束⑷ 更新任务及CPU的cycles⑸ 更新任务及CPU的demand及pred_demand⑹ 更新CPU的busytime⑺ 更新任务的p......
  • 【WALT】update_window_start() 代码详解
    目录【WALT】update_window_start()代码详解代码展示代码逻辑【WALT】update_window_start()代码详解代码版本:Linux4.9android-msm-crosshatch-4.9-android12代码展示staticu64update_window_start(structrq*rq,u64wallclock,intevent){ s64delta; intnr_window......
  • OpenWrt+R2S 主路由、旁路由配置详解
    1、R2S用作主路由R2S作为主路由的好处,所有流量都经过软路由,客户端不需要额外配置坏处:所有流量都经过软路由1.1网络拓扑1.2R2S的WAN口配置家庭一般都是光猫拨号,光猫的网口直接连上R2S的WAN口,R2S一般保持默认配置就好,即配置DHCP客户端自动获取IP地址就好了。比如你是校园......
  • 详解Java中跳跃表的原理和实现
    原文链接及讲解:详解Java中跳跃表的原理和实现java跳表实现importjava.util.Collections;importjava.util.List;importjava.util.stream.Collectors;importjava.util.stream.Stream;/***java跳表实现**@authorlyn*@date2023/6/3018:50*/publicclass......