Redis入门第四步:Redis发布与订阅

欢迎继续跟随《Redis新手指南:从入门到精通》专栏的步伐!在本文中,我们将深入探讨Redis的发布与订阅(Pub/Sub)模式。这是一种强大的消息传递机制,适用于各种实时通信场景,如聊天应用、实时通知和事件驱动架构。通过学习本篇文章,你将能够掌握如何使用Redis的Pub/Sub功能来构建高效的消息传递系统。

在本文中,你将会学到:

  • 为什么需要发布与订阅模式: 通过业务实际案例解释为什么需要Pub/Sub模式
  • 什么是发布与订阅模式:理解Pub/Sub的基本概念及其在分布式系统中的重要性。
  • Redis的Pub/Sub命令 :详细介绍如何使用PUBLISH, SUBSCRIBE, UNSUBSCRIBE, 和 PSUBSCRIBE等命令。
  • 频道与模式:了解频道(channels)和模式(patterns)的区别及用法。
  • 实战案例:通过一个简单的聊天应用示例,展示如何在实际项目中使用Redis的Pub/Sub功能。
  • 最佳实践:分享一些使用Pub/Sub时的最佳实践,包括性能优化和故障处理。

无论你是初学者还是有经验的开发者,本文都将为你提供清晰易懂的指导,帮助你在项目中有效利用Redis的发布与订阅功能。让我们一起探索这一强大的消息传递机制吧!

也许有的小伙伴对这个功能比较陌生,不太清楚这个功能是干什么的. 不妨我们先举个案例

假设我们有这么一个业务场景,在网站下单支付以后,需要通知库存服务进行发货处理。

上面业务实现不难,我们只要让库存服务提供给相关的给口,下单支付之后只要调用库存服务即可。

后面如果又有新的业务,比如说积分服务,他需要获取下单支付的结果,然后增加用户的积分。

这个实现也不难,让积分服务同样提供一个接口,下单支付之后只要调用库存服务即可。

如果就两个业务需要获取下单支付的结果,那也还好,程序改造也快。可是随着业务不断的发展,越来越多的新业务说是要下单支付的结果。

这时我们会发现上面这样的系统架构存在很多问题:

第一,下单支付业务与其他业务重度耦合,每当有个新业务需要支付结果,就需要改动下单支付的业务。

第二,如果调用业务过多,会导致下单支付接口响应时间变长。另外,如果有任一下游接口响应变慢,就会同步导致下单支付接口响应也变长。

第三,如果任一下游接口失败,可能导致数据不一致的情况。比如说下图,先调用 A,成功之后再调用 B,最后再调用 C。

如果在调用 B 接口的发生异常,此时可能就导致下单支付接口返回失败,但是此时 A 接口其实已经调用成功,这就代表它内部已经处理下单支付成功的结果。

这样就会导致 A,B,C 三个下游接口,A 获取成功获取支付结果,但是 B,C 没有拿到,导致三者系统数据不一致的情况。

其实我们仔细想一下,对于下单支付业务来讲,它其实不需要关心下游调用结果,只要有某种机制通知能通知到他们就可以了。

讲到这里,这就需要引入今天需要介绍发布订阅机制

1 什么是发布订阅🍀

Redis 发布订阅(pub/sub) 是一种消息通信模式: 发送者(pub)发送消息,订阅者(sub)接收消息。Redis 的 subscribe 命令可以让客户端订阅任意数量的频道, 每当有新信息发送到被订阅的频道时, 信息就会被发送给所有订阅指定频道的客户端

下图展示了频道channel , 以及订阅这个频道的三个客户端 ------ client2 、 client5 和 client1 之间的关系:

当有新消息通过 publish 命令发送给频道 channel 时, 这个消息就会被发送给订阅它的三个客户端:

如上图所示,消息发布者只需要想指定的频道发布消息,订阅该频道的每个客户端都可以接受到到这个消息。

使用 Redis 发布订阅这种机制,对于上面业务,下单支付业务只需要向**「支付结果」** 这个频道发送消息,其他下游业务订阅**「支付结果」**这个频道,就能收相应消息,然后做出业务处理即可

这样就可以解耦系统上下游之间调用关系

2 为什么要用发布订阅🍀

针对消息订阅发布功能,市面上很多大厂使用的是kafkaRabbitMQActiveMQ, RocketMQ等这几种,redis的订阅发布功能跟这三者相比,相对轻量,针对数据准确和安全性要求没有那么高可以直接使用,适用于小公司

1.双端队列

redis 的List数据类型结构提供了 blpop 、brpop 命令结合 rpush、lpush 命令可以实现消息队列机制,基于双端链表实现的发布与订阅功能

这种方式存在两个局限性:

  • 不能支持一对多的消息分发。

  • 如果生产者生成的速度远远大于消费者消费的速度,易堆积大量未消费的消息

◇ 双端队列图解如下:

**解析:**双端队列模式只能有一个或多个消费者轮着去消费,却不能将消息同时发给其他消费者

2. 发布/订阅模式

发布/订阅模式图解如下:

**解析:**redis订阅发布模式,生产者生产完消息通过频道分发消息,给订阅了该频道的所有消费

3 发布/订阅如何使用🍀

Redis有两种发布/订阅模式

1.基于频道(Channel)的发布/订阅

允许用户订阅一个或多个频道,以接收所有发送到这些频道的消息。这种模型与其它消息队列不同,因为它具有消息的持久性,即使消费者在消息发送时不在线,当它们上线时,它们仍然可以接收到所有的消息

命令 命令描述
subscribe channel [cha...] 订阅给定的一个或多个频道
unsubscribe channel [cha...] 退订给定的频道.说明:若没有指定channel,则默认退订所有频道
publish channel message 将消息发送给指定频道的channel,返回结果:接收到信息的订阅者数量,无订阅者返回0
publish channels [argument [arg]....] 查看订阅与发布系统的状态.说明:返回活跃频道列表(即至少有一个订阅者的频道,订阅模式的客户端除外)

"发布/订阅" 包含2种角色:发布者和订阅者。发布者可以向指定的频道(channel)发送消息;订阅者可以订阅一个或者多个频道(channel),所有订阅此频道的订阅者都会收到此消息

  • 订阅者订阅频道 subscribe channel [channel ...]
bash 复制代码
--------------------------客户端1(订阅者) :订阅频道 ---------------------
 
# 订阅 "meihuashisan" 和 "csdn" 频道(如果不存在则会创建频道)
127.0.0.1:6379> subscribe meihuashisan csdn 
Reading messages... (press Ctrl-C to quit)
 
1) "subscribe"    -- 返回值类型:表示订阅成功!
2) "meihuashisan" -- 订阅频道的名称
3) (integer) 1    -- 当前客户端已订阅频道的数量
 
1) "subscribe"
2) "csdn"
3) (integer) 2
 
#注意:订阅后,该客户端会一直监听消息,如果发送者有消息发给频道,这里会立刻接收到消息
  • 发布者发布消息 publish channel message
bash 复制代码
-----------------------客户端2(发布者):发布消息给频道 -------------------

# 给"meihuashisan"这个频道 发送一条消息:"I am meihuashisan"
127.0.0.1:6379> publish meihuashisan "I am meihuashisan"
(integer) 1  # 接收到信息的订阅者数量,无订阅者返回0
  • 客户端2(发布者)发布消息给频道后,此时我们再来观察 客户端1(订阅者)的客户端窗口变化
bash 复制代码
--------------------------客户端1(订阅者) :订阅频道 -----------------
127.0.0.1:6379> subscribe meihuashisan csdn 
Reading messages... (press Ctrl-C to quit)
 
1) "subscribe"    -- 返回值类型:表示订阅成功!
2) "meihuashisan" -- 订阅频道的名称
3) (integer) 1    -- 当前客户端已订阅频道的数量
 
1) "subscribe"
2) "csdn"
3) (integer) 2
 
--------------------变化如下:(实时接收到了该频道的发布者的消息)------------
1) "message"           -- 返回值类型:消息
2) "meihuashisan"      -- 来源(从哪个频道发过来的)
3) "I am meihuashisan" -- 消息内容

注意:如果是先发布消息,再订阅频道,不会收到订阅之前就发布到该频道的消息!

注意: 进入订阅状态的客户端,不能使用除了subscribeunsubscribepsubscribepunsubscribe 这四个属于"发布/订阅"之外的命令,否则会报错!

这里的客户端指的是 jedis、lettuce的客户端,redis-cli是无法退出订阅状态的

底层实现逻辑

底层通过字典实现。pubsub_channels 是一个字典类型,保存订阅频道的信息:字典的key为订阅的频道, 字典的value是一个链表, 链表中保存了所有订阅该频道的客户端

java 复制代码
struct redisServer { 
  /* General */ 
  pid_t pid; 
  
  //省略百十行 
 
  // 将频道映射到已订阅客户端的列表(就是保存客户端和订阅的频道信息)
  dict *pubsub_channels; /* Map channels to list of subscribed clients */ 
}
  • 数据结构

比如说,在下图展示的这个 pubsub_channels 示例中, client2 、 client5 和 client1 就订阅了 channel1 , 而其他频道也分别被别的客户端所订阅:

  • 订阅

当客户端调用 SUBSCRIBE 命令时, 程序就将客户端和要订阅的频道在 pubsub_channels 字典中关联起来。

举个例子,如果客户端 client10086 执行命令 SUBSCRIBE channel1 channel2 channel3 ,那么前面展示的 pubsub_channels 将变成下面这个样子:

  • 发布

当调用 PUBLISH channel message 命令, 程序首先根据 channel 定位到字典的键, 然后将信息发送给字典值链表中的所有客户端。

比如说,对于以下这个 pubsub_channels 实例, 如果某个客户端执行命令 PUBLISH channel1 "hello moto" ,那么 client2 、 client5 和 client1 三个客户端都将接收到 "hello moto" 信息:

  • 退订

使用 UNSUBSCRIBE 命令可以退订指定的频道, 这个命令执行的是订阅的反操作:它从 pubsub_channels 字典的给定频道(键)中, 删除关于当前客户端的信息, 这样被退订频道的信息就不会再发送给这个客户端。

2.基于模式(pattern)的发布/订阅
命令 命令描述
psubscribe pattern1 [pattern....] 订阅一个或多个符合给定模式的频道.说明:每个模式以*为匹配符;例如cn*匹配所有以cn开头的频道
punsubscribe pattern1 [pattern....] 退订所有给定模式的频道.说明:pattern未指定,则订阅的所有模式都会被退订,否则只退订指定的订阅模式

如果有某个/某些模式和该频道匹配,所有订阅这个/这些频道的客户端也同样会收到信息

图解

下图展示了一个带有频道和模式的例子, 其中 com.ahead.* 频道匹配了 com.ahead.juc 频道和 com.ahead.thread 频道, 并且有不同的客户端分别订阅它们三个,如下图:

当有信息发送到com.ahead.thread 频道时, 信息除了发送给 client 4client 5 之外, 还会发送给订阅 com.ahead.* 频道模式的 client xclient y

解析: 反之也是,如果当有消息发送给 com.ahead.juc 频道,消息发送给订阅了 juc 频道的客户端之外,还会发送给订阅了 com.ahead.* 频道的客户端: client x 、client y

通配符中?表示1个占位符,*表示任意个占位符(包括0),?*表示1个以上占位符。

  • 订阅者订阅频道 psubscribe pattern [pattern ...]

    bash 复制代码
    --------------------------客户端1(订阅者) :订阅频道 --------------------
     
    #  1. ------------订阅 "a?" "com.*" 2种模式频道--------------
    127.0.0.1:6379> psubscribe a? com.*
    # 进入订阅状态后处于阻塞,可以按Ctrl+C键退出订阅状态
    Reading messages... (press Ctrl-C to quit) 
     
    ---------------订阅成功-------------------
     
    1) "psubscribe"  -- 返回值的类型:显示订阅成功
    2) "a?"          -- 订阅的模式
    3) (integer) 1   -- 目前已订阅的模式的数量
     
    1) "psubscribe"
    2) "com.*"
    3) (integer) 2
     
    ---------------接收消息 (已订阅 "a?" "com.*" 两种模式!)-----------------
     
    # ---- 发布者第1条命令:publish ahead "hello"
    结果:没有接收到消息,匹配失败,不满足 "a?" ,"?"表示一个占位符, a后面的head有4个占位符
     
    # ---- 发布者第2条命令:  publish aa "hello" (满足 "a?")
    1) "pmessage" -- 返回值的类型:信息
    2) "a?"       -- 信息匹配的模式:a?
    3) "aa"       -- 信息本身的目标频道:aa
    4) "hello"    -- 信息的内容:"hello"
     
    # ---- 发布者第3条命令:publish com.juc "hello2"(满足 "com.*", *表示任意个占位符)
    1) "pmessage" -- 返回值的类型:信息
    2) "com.*"    -- 匹配模式:com.*
    3) "com.juc"  -- 实际频道:com.juc
    4) "hello2"   -- 信息:"hello2"
     
    ---- 发布者第4条命令:publish com. "hello3"(满足 "com.*", *表示任意个占位符)
    1) "pmessage" -- 返回值的类型:信息
    2) "com.*"    -- 匹配模式:com.*
    3) "com."     -- 实际频道:com.
    4) "hello3"   -- 信息:"hello3"
  • 发布者发布消息 publish channel message

    bash 复制代码
    ------------------------客户端2(发布者):发布消息给频道 ------------------
     
    注意:订阅者已订阅 "a?" "com.*" 两种模式!
     
    # 1. ahead 不符合"a?"模式,?表示1个占位符
    127.0.0.1:6379> publish ahead "hello"  
    (integer) 0    -- 匹配失败,0:无订阅者
     
    # 2. aa 符合"a?"模式,?表示1个占位符
    127.0.0.1:6379> publish aa "hello"      
    (integer) 1
     
    # 3. 符合"com.*"模式,*表示任意个占位符
    127.0.0.1:6379> publish com.juc "hello2" 
    (integer) 1
     
    # 4. 符合"com.*"模式,*表示任意个占位符
    127.0.0.1:6379> publish com. "hello3" 
    (integer) 1

底层是pubsubPattern节点的链表。

  • 数据结构 redisServer.pubsub_patterns 属性是一个链表,链表中保存着所有和模式相关的信息:
java 复制代码
struct redisServer {
    // ...
    list *pubsub_patterns;
    // ...
};

链表中的每个节点都包含一个 redis.h/pubsubPattern 结构:

java 复制代码
typedef struct pubsubPattern {
    redisClient *client;
    robj *pattern;
} pubsubPattern;

client 属性保存着订阅模式的客户端,而 pattern 属性则保存着被订阅的模式。

每当调用 PSUBSCRIBE 命令订阅一个模式时, 程序就创建一个包含客户端信息和被订阅模式的 pubsubPattern 结构, 并将该结构添加到 redisServer.pubsub_patterns 链表中。

作为例子,下图展示了一个包含两个模式的 pubsub_patterns 链表, 其中 client123 和 client256 都正在订阅 tweet.shop.* 模式:

  • 订阅

如果这时客户端 client10086 执行 PSUBSCRIBE broadcast.list.* , 那么 pubsub_patterns 链表将被更新成这样:

通过遍历整个 pubsub_patterns 链表,程序可以检查所有正在被订阅的模式,以及订阅这些模式的客户端。

  • 发布

发送信息到模式的工作也是由 PUBLISH 命令进行的, 显然就是匹配模式获得Channels,然后再把消息发给客户端。

  • 退订

使用 PUNSUBSCRIBE 命令可以退订指定的模式, 这个命令执行的是订阅模式的反操作:程序会删除 redisServer.pubsub_patterns 链表中, 所有和被退订模式相关联的 pubsubPattern 结构, 这样客户端就不会再收到和模式相匹配的频道发来的信息

4 Redis 发布订阅实际应用🍀

Redis Sentinel 节点发现

「Redis Sentinel」 是 Redis 一套高可用方案,可以在主节点故障的时候,自动将从节点提升为主节点,从而转移故障。

今天这里我们不详细解释 「Redis Sentinel」 详细原理,主要来看下 「Redis Sentinel」 如何使用发布订阅机制。

「Redis Sentinel」 节点主要使用发布订阅机制,实现新节点的发现,以及交换主节点的之间的状态。

如下所示,每一个 「Sentinel」 节点将会定时向 _sentinel_:hello 频道发送消息,并且每个 「Sentinel」 都会订阅这个节点。

这样一旦有节点往这个频道发送消息,其他节点就可以立刻收到消息。

这样一旦有的新节点加入,它往这个频道发送消息,其他节点收到之后,判断本地列表并没有这个节点,于是就可以当做新的节点加入本地节点列表。

除此之外,每次往这个频道发送消息内容可以包含节点的状态信息,这样可以作为后面 「Sentinel」 领导者选举的依据。

以上都是对于 Redis 服务端来讲,对于客户端来讲,我们也可以用到发布订阅机制。

「Redis Sentinel」 进行主节点故障转移,这个过程各个阶段会通过发布订阅对外提供。

对于我们客户端来讲,比较关心切换之后的主节点,这样我们及时切换主节点的连接(旧节点此时已故障,不能再接受操作指令),

客户端可以订阅 +switch-master频道,一旦 「Redis Sentinel」 结束了对主节点的故障转移就会发布主节点的的消息。

redission 分布式锁

redission 开源框架提供一些便捷操作 Redis 的方法,其中比较出名的 redission 基于 Redis 的实现分布式锁。

今天我们来看下 Redis 的实现分布式锁中如何使用 Redis 发布订阅机制,提高加锁的性能。

首先我们来看下 redission 加锁的方法:

Redisson redisson = ....
RLock redissonLock = redisson.getLock("xxxx");
redissonLock.lock();

RLock 继承自 Java 标准的 Lock 接口,调用 lock 方法,如果当前锁已被其他客户端获取,那么当前加锁的线程将会被阻塞,直到其他客户端释放这把锁。

这里其实有个问题,当前阻塞的线程如何感知分布式锁已被释放呢?

这里其实有两种实现方法:

第一钟,定时查询分布时锁的状态,一旦查到锁已被释放(Redis 中不存在这个键值),那么就去加锁。

实现伪码如下:

while (true) {
  boolean result=lock();
  if (!result) {
    Thread.sleep(N);
  }
}

这种方式实现起来起来简单,不过缺点也比较多。

如果定时任务时间过短,将会导致查询次数过多,其实这些都是无效查询。

如果定时任务休眠时间过长,那又会导致加锁时间过长,导致加锁性能不好。

那么第二种实现方案,就是采用服务通知的机制,当分布式锁被释放之后,客户端可以收到锁释放的消息,然后第一时间再去加锁。

这个服务通知的机制我们可以使用 Redis 发布订阅模式。

当线程加锁失败之后,线程将会订阅 redisson_lock__channel_xxx(xx 代表锁的名称) 频道,使用异步线程监听消息,然后利用 Java 中 Semaphore 使当前线程进入阻塞。

一旦其他客户端进行解锁,redission 就会往这个redisson_lock__channel_xxx 发送解锁消息。

等异步线程收到消息,将会调用 Semaphore 释放信号量,从而让当前被阻塞的线程唤醒去加锁。

ps:这里只是简单描述了 redission 加锁部分原理,出于篇幅,这里就不再消息解析源码。

感兴趣的小伙伴可以自己看下 redission 加锁的源码。

通过发布订阅机制,被阻塞的线程可以及时被唤醒,减少无效的空转的查询,有效的提高的加锁的效率。

ps: 这种方式,性能确实提高,但是实现起来的复杂度也很高,这部分源码有点东西,快看晕了。

5 实时聊天应用示例🍀

场景描述

假设你正在开发一个实时聊天应用,用户可以在不同的聊天室中进行交流。每个聊天室都有多个用户,当一个用户发送消息时,其他所有在该聊天室中的用户都应该立即收到这条消息。

传统解决方案的问题
  • 轮询(Polling):客户端定期向服务器发送请求,检查是否有新消息。这种方式效率低下,且响应时间较长。
  • 长轮询(Long Polling):客户端发送请求后,服务器保持连接打开,直到有新消息或超时。虽然比普通轮询更高效,但仍存在延迟和资源浪费问题。
  • WebSocket:使用WebSocket可以实现全双工通信,但设置和维护相对复杂,并且需要处理更多的网络层细节。
使用Redis Pub/Sub的优势
  • 实时性:Redis的Pub/Sub机制可以实现实时的消息传递,确保消息几乎即时送达。
  • 简单易用:Redis的Pub/Sub命令简单直观,易于集成到现有系统中。
  • 高效率:Redis是内存数据库,消息传递速度快,适合高频次的消息传输。
  • 灵活性:可以轻松地扩展到多个聊天室和大量用户。
示例实现
  1. 初始化Redis客户端

    在服务器端和客户端分别初始化Redis客户端

  2. 创建聊天室频道

    每个聊天室对应一个Redis频道。例如,chatroom:1 表示第一个聊天室

  3. 客户端订阅频道

    客户端在进入某个聊天室时,订阅对应的频道

  4. 客户端发布消息

    当用户发送消息时,客户端将消息发布到对应的频道

  5. 服务器端管理聊天室

    服务器端可以负责管理和验证用户的订阅和发布操作,确保只有合法用户能够参与聊天

python 复制代码
# 相关代码如下: 
---------- 聊天室 服务器 代码 如下 ------------
#! /usr/bin/python
# -*- coding: UTF-8 -*-
import redis

# 初始化Redis客户端
r = redis.Redis(host='localhost', port=6379, db=0, password='kdlrdsha2019')

# 订阅聊天室列表
pubsub = r.pubsub()
pubsub.subscribe('chatroom:1')

for message in pubsub.listen():
    if message['type'] == 'message':
        print(f"Received message: {message['data'].decode('utf-8')}")

---------- 客户端使用 代码如下 ----------------
[root@test ~]# python3
Python 3.12.0 (main, Oct 27 2023, 14:11:11) [GCC 4.8.5 20150623 (Red Hat 4.8.5-44)] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import redis
>>> r = redis.Redis(host='localhost', port=6379, db=0, password='kdlrdsha2019')
>>> r.publish('chatroom:1', 'Hello, everyone!')
1

---------- 服务端返回如下 ------------------
[root@test Redis]# python3 Redis_Pub_Sub.py
Received message: Hello, everyone!
代码示例

以下是Python示例代码,展示了如何使用Redis的Pub/Sub功能实现简单的实时聊天应用。

python 复制代码
#! /usr/bin/python
# -*- coding: UTF-8 -*-
import redis
import threading

# 初始化Redis客户端, Redis服务器有密码的话需要带password参数
r = redis.Redis(host='localhost', port=6379, db=0)

def subscribe_to_chatroom(chatroom):
    pubsub = r.pubsub()
    pubsub.subscribe(chatroom)
    
    print(f"Subscribed to {chatroom}")
    for message in pubsub.listen():
        if message['type'] == 'message':
            print(f"Received message in {chatroom}: {message['data'].decode('utf-8')}")

def send_message(chatroom, message):
    r.publish(chatroom, message)
    print(f"Message sent to {chatroom}: {message}")

# 创建两个线程,模拟两个客户端订阅同一个聊天室
thread1 = threading.Thread(target=subscribe_to_chatroom, args=('chatroom:1',))
thread2 = threading.Thread(target=subscribe_to_chatroom, args=('chatroom:1',))

thread1.start()
thread2.start()

# 模拟发送消息
send_message('chatroom:1', 'Hello, everyone!')
send_message('chatroom:1', 'How are you?')

总结

在这个示例中,Redis的Pub/Sub功能使得实时聊天应用变得非常简单和高效。客户端可以轻松地订阅和接收消息,而无需复杂的网络配置。这种模式不仅适用于聊天应用,还可以用于各种需要实时消息传递的场景,如实时通知、日志聚合、事件驱动架构等。

通过这个例子,读者可以清楚地看到Redis的Pub/Sub功能在实际项目中的强大作用和实用性。希望这能帮助你更好地讲解这个功能的重要性!

相关推荐
工业甲酰苯胺1 小时前
Redis性能优化的18招
数据库·redis·性能优化
没书读了2 小时前
ssm框架-spring-spring声明式事务
java·数据库·spring
i道i2 小时前
MySQL win安装 和 pymysql使用示例
数据库·mysql
小怪兽ysl2 小时前
【PostgreSQL使用pg_filedump工具解析数据文件以恢复数据】
数据库·postgresql
wqq_9922502773 小时前
springboot基于微信小程序的食堂预约点餐系统
数据库·微信小程序·小程序
爱上口袋的天空3 小时前
09 - Clickhouse的SQL操作
数据库·sql·clickhouse
Oak Zhang4 小时前
sharding-jdbc自定义分片算法,表对应关系存储在mysql中,缓存到redis或者本地
redis·mysql·缓存
聂 可 以5 小时前
Windows环境安装MongoDB
数据库·mongodb
web前端神器5 小时前
mongodb多表查询,五个表查询
数据库·mongodb
门牙咬脆骨5 小时前
【Redis】redis缓存击穿,缓存雪崩,缓存穿透
数据库·redis·缓存