后端工程师必备技能之Redis的数据类型系列文章

Redis 是后端工程师必备的技能。每次采访都会询问一位粉丝。有位粉丝特地将公众号前面发布的Redis系列文章整理成一篇,自学,帮助大家一起学习。

文章比较长,建议先收藏再看。

Redis的数据类型有哪些?

Redis中有五种数据类型,每种数据类型都有相关的命令。类型如下:

String(字符串) List(列表) Hash(字典) Set(集合) Sorted Set(有序集合)

Redis 中有五种常见的数据类型,每一种都有自己的使用场景。一般字符串类型应用最广,常见的Key/Value就是这种类型;列表类型使用的场景通常有一个粉丝列表。观察名单的场景;字典类型为哈希表结构,在各种系统中也被广泛使用,可用于存储用户或设备信息,类似于HashMap的结构;Redis set 提供的 set 的功能类似于 list 类型的,也是 list 的功能,不同的是 Set 是去重;有序集合的功能与Set的功能相同,但它是有序的。

Redis的内存回收和Key的过期策略Redis内存过期策略过期策略配置

随着Redis使用的时间越来越长,占用的内存也会越来越大。当 Redis 内存不够时,我们需要知道 Redis 使用什么策略来消除数据。在配置文件中,我们使用 maxmemory-policy 来配置策略,如下图

我们可以看到该策略的价值包括以下内容:

volatile-lru:使用LRU算法消除所有key中具有过期时间的数据;alkeys-lru:使用最近最少使用的LRU算法,剔除所有key中的数据,保证新添加的数据正常;volatile-random:在所有有过期时间的key中随机剔除数据;allkeys-random:随机剔除所有key中的数据;volatile-ttl:剔除所有有过期时间的key中最早过期的数据;noeviction:不回收,当达到最大内存时,添加新数据时会返回错误,并且不会清除旧数据,这是Redis的默认策略;

volatile-lru、volatile-random 和 volatile-ttl 与 Redis 没有过期 key 时的 noeviction 策略相同。淘汰策略可动态调整,调整时无需重启。原文是这么说的,我们可以根据自己的Redis模式动态调整策略。”根据应用程序的访问模式选择正确的驱逐策略很重要,但是您可以在应用程序运行时在运行时重新配置策略,并使用 Redis INFO 输出监控缓存未命中和命中的数量以调整你的设置。”

策略执行过程中,客户端运行命令添加数据申请内存;Redis 会检查内存使用情况。如果超过了最大限制,则会根据配置的内存淘汰策略淘汰对应的key,以保证新数据的正常添加;继续执行订单。近似 LRU 算法

Redis 中的 LRU 算法并不是一个精确的 LRU 算法,而是一个采样的 LRU。我们可以通过在配置文件中设置 maxmemory-samples 5 来设置样本大小。默认值为5,我们可以自行调整。官方的采用率对比如下,我们可以看到当采用数设置为 10 时,非常接近真实的 LRU 算法。

在 Redis 3.x 以上的版本中进行了优化。目前的近似LRU算法大大提高了效率。Redis 之所以不对实际的 LRU 算法进行采样,是因为它会消耗大量的内存。原文是这样说的

Redis 之所以不使用真正的 LRU 实现,是因为它需要更多的内存。

Key的过期策略设置key有过期时间

Redis的内存回收策略前面已经介绍过了。下面我们来看看Key的过期策略。说到Key的过期策略,我们说的是有过期时间的key,如下

通过redis> set name ziyouu ex 100命令,我们将Redis中的key设置为name,data设置为ziyouu。从上面的截图我们可以看到右下角有一个TTL,每次刷新都在递减。这意味着我们成功设置了带有过期时间的密钥。

Redis如何清除过期的key

至于如何清除过期的key,我们自然会想到可以给每个key添加一个定时器,这样当时间到了过期时间,key就自动删除了。这种策略称为时序策略。这种方式对内存友好,因为可以及时清理过期的,但是由于每个有过期时间的key都需要一个定时器ttl传输中过期是什么意思,所以这种方式对CPU不友好,而且会占用大量的CPU,在另外,这种方法是一种主动行为。

有主动的和被动的,我们不能使用定时器,而是每次访问一个key时判断key是否已经到了过期时间,过期就删除。这种方法称为惰性策略。这种方法对CPU很友好,但也有一个相应的问题,就是如果我们从不访问这些过期的key,就永远不会被删除。

Redis服务器实际实现的时候,会用到上面两种方法,这样就可以得到一个折中的方法。另外,在计时策略中,从官网我们可以看到如下说明

具体来说,这是 Redis 每秒执行 10 次的操作:

从具有关联过期时间的密钥集中测试 20 个随机密钥。

删除所有发现过期的密钥。

如果超过 25% 的密钥过期,请从步骤 1. 重新开始

意思是Redis会从有过期时间的key集合中随机选择20个key,删除已经过期的key,如果比例超过25%则重新执行操作。每秒执行 10 次这些操作。

你知道 Redis 的发布订阅功能吗?

发布-订阅系统在我们的日常工作中经常用到。在大多数情况下,我们使用消息队列。常用的消息队列有Kafka、RocketMQ、RabbitMQ。每个消息队列都有自己的特点。其实很多时候,我们可能不需要独立部署对应的消息队列,直接使用就可以了,数据量也不会太大。在这种情况下,我们可以考虑使用 Redis 的 Pub/Sub 模型。

如何使用发布和订阅

Redis的发布和订阅功能主要由PUBLISH、SUBSCRIBE、PSUBSCRIBE命令组成。一个或多个客户订阅一个或多个频道。当其他客户端向该频道发送消息时,订阅该频道的客户端将收到相应的消息。

上图中有四个客户端,Client 02、Client 03、Client 04订阅了同一个Sport频道(Channel)。双方同时收到了这条信息。

整个流程的执行命令如下:

先打开四个Redis客户端,然后在Client 02、Client 03、Client 04中输入subscribe sport命令订阅sport频道

然后在Client 01的客户端输入publish sport Basketball,将消息“basketball”发送到sport频道

这时候我们在看Client 02-04的客户端,可以看到消息已经收到了,每个订阅这个频道的客户端都是一样的。

这里,三个客户端Client 02-Client 04订阅了Sport频道,我们称之为订阅者,Client 01发布消息,我们称之为发布者,发送的消息就是消息。

模式订阅

我们之前看到的是一个客户端订阅了一个 Channel。实际上,单个客户端也可以同时订阅多个 Channel。使用模式匹配,客户端可以同时订阅多个频道。

如上图所示,Client 05 通过命令 subscribe run 订阅了 run 频道,Client 06 通过命令 psubscribe run* 订阅了匹配 run* 的频道。当客户端 07 向运行通道发送消息 666 时,客户端 05 和 06 都收到该消息;那么当Client 07 向run1 和run_sport 通道发送消息时,Client 06 仍然可以接收到该消息,而Client 05 将不会收到该消息。

客户端 05 订阅运行通道并接收消息:

客户端 06 订阅 run* 频道并接收消息:

image-20191222141458065

客户端 07 向多个通道发送消息:

image-20191222141514914

通过上述案例,我们了解到客户端可以订阅单个或多个频道。通过 subscribe 和 psubscribe 命令,客户端可以通过 publish 发送相应的消息。

在命令行中,我们可以使用Ctrl+C取消相关订阅,对应的命令是unsubscribe channelName。

Pub/Sub 底层存储结构订阅 Channel

在 Redis 的底层结构中,客户端与频道的订阅关系是通过字典和链表结构保存的,形式如下:

在 Redis 的底层结构中,在 Redis 服务器结构中定义了一个 pubsub_channels 字典

struct redisServer {
 //用于保存所有频道的订阅关系
 dict *pubsub_channels;
}

在这个字典中,key 代表频道名称,value 是一个链表,里面存储了所有订阅频道的客户端。

所以当客户端执行订阅频道的动作时,服务器会将客户端与 pubsub_channels 字典中的订阅频道相关联。

这个时候有两种情况:

比如有一个新的客户端Client 08订阅run channel,上图就会变成

如果 Client 08 想订阅一个新频道 new_sportttl传输中过期是什么意思,它将变成

image-20191222161558999

整个订阅过程可以用下面的伪代码来实现

Map> pubsub_channels = new HashMap<>();
    public void subscribe(String[] subscribeList, Object client) {
        //遍历所有订阅的 channel,检查是否在 pubsub_channels 中,不在则创建新的 key 和空链表
        for (int i = 0; i < subscribeList.length; i++) {
            if (!pubsub_channels.containsKey(subscribeList[i])) {
                pubsub_channels.put(subscribeList[i], new ArrayList<>());
            }
            pubsub_channels.get(subscribeList[i]).add(client);
        }
    }

退订

上面描述了单个Channel的订阅。相反,如果一个客户端想要退订一个相关的Channel,无非是找到对应Channel的链表,并从中删除对应的客户端。如果client已经是最后一个了,那么对应的Channel也会被删除。

public void unSubscribe(String[] subscribeList, Object client) {
        //遍历所有订阅的 channel,依次删除
        for (int i = 0; i < subscribeList.length; i++) {
            pubsub_channels.get(subscribeList[i]).remove(client);
            //如果长度为 0 则清楚 channel
            if (pubsub_channels.get(subscribeList[i]).size() == 0) {
                remove(subscribeList[i]);
            }
        }
    }

模式订阅结构

模式频道订阅类似于单频道订阅,只是服务器将所有模式订阅关系存储在服务器状态的 pubsub_patterns 属性中。

struct redisServer{
 //保存所有模式订阅关系
 list *pubsub_patterns;
}

与订阅单个 Channel 不同,pubsub_patterns 属性是一个链表,而不是字典。节点结构如下:

struct pubsubPattern{
 //订阅模式的客户端
 redisClient *client;
 //被订阅的模式
 robj *pattern;
} pubsubPattern;

其实client属性是用来存放对应的client信息的,pattern是用来存放client对应的匹配模式的。

所以上面Client-06模式匹配对应的结构体存储如下

image-20191222174528367

pubsub_patterns 链表中有一个节点,对应的客户端是Client-06,对应的匹配模式是run*。

订阅模式

当客户端通过命令psubscribe订阅对应模式的频道时,服务端会创建一个节点,将Client属性设置为对应的客户端,将模式属性设置为对应的模式规则,然后添加到链表。

对应的伪代码如下:

List pubsub_patterns = new ArrayList<>();
    public void psubscribe(String[] subscribeList, Object client) {
        //遍历所有订阅的 channel,创建节点
        for (int i = 0; i < subscribeList.length; i++) {
            PubSubPattern pubSubPattern = new PubSubPattern();
            pubSubPattern.client = client;
            pubSubPattern.pattern = subscribeList[i];
            pubsub_patterns.add(pubSubPattern);
        }
    }

创建一个新节点;为节点的属性赋值;在链表的末尾添加一个节点;退订模式

取消订阅模式的命令是 punsubscribe,客户端使用它来取消订阅一个或多个模式频道。服务端收到命令后,会遍历pubsub_patterns链表,删除匹配到的具有client和pattern属性的节点。这里需要判断client属性和pattern属性是否有效,然后再删除。

伪代码如下:

public void punsubscribe(String[] subscribeList, Object client) {
        //遍历所有订阅的 channel 相同 client 和 pattern 属性的节点会删除
        for (int i = 0; i < subscribeList.length; i++) {
            for (int j = 0; j < pubsub_patterns.size(); j++) {
                if (pubsub_patterns.get(j).client == client
                && pubsub_patterns.get(j).pattern == subscribeList[i]) {
                    remove(pubsub_patterns);
                }
            }
        }
    }

遍历所有节点,匹配相同客户端属性和模式属性时删除节点。

发布消息

发布消息更容易理解。当客户端执行发布 channelName 消息命令时,服务端会从 pubsub_channels 和 pubsub_patterns 两个结构中找到所有匹配 channelName 的 Channel,并发送消息。在pubsub_channels中,只要找到对应Channel的key,在对应的value列表中向客户端发送消息即可。

你了解 Redis 的持久化吗?

持久性是一种在持久状态和瞬态状态之间转换程序数据的机制。通俗的说,就是将瞬态数据(比如内存中的数据,不能永久保存)作为持久化数据进行持久化(比如持久化到数据库,可以长期保存)。另外,我们使用的Redis之所以快,是因为数据是存储在内存中的。为了保证服务器异常后可以恢复数据,有Redis的持久化。Redis的持久化方式有两种,一种是快照形式的RDB,另一种是增量文件AOF。

关系数据库

RDB 持久化方法是在特定时间间隔内的某个时间点保存数据快照。拿到这个数据快照后,我们就可以根据这个快照完整的复制数据了。这样,我们就可以备份数据,备份快照文件,然后传输到其他服务器直接恢复数据。但这只是某个时间点的所有数据。如果我们想要最新的数据,我们只能定期生成快照文件。

RDB的实现主要通过创建子进程实现RDB文件的快照生成,通过子进程实现备份功能,不影响主进程的性能。同时上面也提到,RDB的快照文件保存了一定时间间隔的数据,这会导致如果时间间隔过长,服务器会丢失之前这个间隔的所有数据有时间生成快照;同学们会说,我们可以把时间间隔设置的更短一些,适当的缩短也是可以的,但是如果时间间隔设置的更短,并且频繁生成快照,还是会对系统产生影响,尤其是在数据量很大的情况下。在高性能环境下,这种情况是不允许的。

我们可以在redis.conf中配置RDB,配置生成快照的策略,以及日志文件的路径和名称。还有定期备份规则,如下图所示。里面的评论很清楚。简单来说就是在一定时间内key的数量发生变化时触发快照。例如 save 300 10 表示如果 10 个 key 在 5 分钟内发生变化,则会触发生产快照,其他情况也是如此。

除了在配置文件中配置自动生成快照文件外,Redis本身也提供了相关的命令可以让我们手动生成快照文件,即SAVE和BGSAVE。这两个命令功能相同,但方法和效果不同。SAVE 命令执行后,服务器进程被阻塞。阻塞后,服务器无法处理任何请求,因此无法在生产中使用。与 SAVE 命令直接阻塞服务器进程不同,BGSAVE 命令生成一个子进程,并通过该子进程创建一个 RDB 文件。主进程仍然可以处理收到的命令,这样就不会阻塞服务器,可以在生产中使用。

一位粉丝在这里测试快照的自动生成。我们修改快照生成策略保存10 2,然后在本地启动Redis服务,使用redis-cli链接进入,步骤如下

修改配置,启动Redis服务如下,我们从启动日志可以看出,默认是读取RDB文件进行恢复

链接Redis服务,10s内设置3个key。这时,我们会在 Redis 日志中看到如下输出。因为触发了规则,所以启动子流程进行数据备份。同时,在对应的文件路径下,我们也看到了rdb文件。

从上面可以看出,我们配置的规则生效了,成功生成了RDB文件。如果服务器出现异常,只要重启服务器,就会读取对应的RDB文件进行数据备份。

AOF

AOF 是附加执行命令的一种形式。它与RDB的区别在于AOF不保存数据,而是保存执行的动作。启用 AOF 功能后,客户端连接后执行的每一条命令都会被记录下来。这其实让阿芬想起了MySQL的binlog log,也是一个记录操作的命令,后面可以根据文件恢复数据。

AOF 是附加命令格式的文件。同样,我们可以定义同步数据的频率。Redis 本身提供了三种策略来实现命令同步,分别是不同步、每秒同步一次和有查询时。时间同步。默认的策略也是最常用的策略,就是每秒同步一次,这样我们就可以知道丢失的数据最多只有一秒的数据。有了这个机制,AOF 会比 RDB 可靠很多,但是因为文件中有执行的命令,所以 AOF 文件一般比 RDB 文件大。

Redis 的 AOF 功能默认是不开启的。我们可以通过在配置文件中配置appendonly yes来开启该功能,并配置同步策略appendfsync everysec开启每秒同步。我们拿到AOF文件后,就可以根据这个文件进行恢复了。数据。

同样的,我们在redis.conf中可以看到默认没有开启AOF功能,我们也可以指定对应的文件名和路径。

接下来我们测试一下AOF功能,修改配置,重启Redis服务器。我们会发现RDB文件的日志没有被读取,在日志文件路径下生成了一个aof文件。需要注意的是,因为我们重启了服务并开启了AOF,所以Redis服务器中并没有我们之前添加的数据(这是什么问题?)。

接下来我们使用客户端连接进入,设置如下值,然后我们就可以查看aof文件的内容了

我们可以看到,aof文件中的内容是执行的命令,但是它是以固定的格式存储的。如果我们在备份过程中不需要任何数据,我们可以手动删除相应的命令重新备份数据。

Redis有几种集群模式

虽然单机 Redis 理论上可以达到 10 万并发和持久化,但是当实际用于生产环境时,相信没有公司敢这么用。当数据量达到一定规模时,必须安装。Redis 集群。

Redis的模式包括主从复制模式、哨兵模式和集群模式。这三种模式涉及更多的空间和内容。后面有粉丝会单独写文章介绍。有兴趣的小伙伴可以自行学习。.

© 版权声明
THE END
喜欢就支持一下吧
点赞0
分享
评论 抢沙发

请登录后发表评论