From 2fefae797ce3df2cf31516988481f89bac87beb4 Mon Sep 17 00:00:00 2001 From: Guide Date: Sat, 23 Mar 2024 10:55:07 +0800 Subject: [PATCH] =?UTF-8?q?[docs=20add]redis=E5=BB=B6=E6=97=B6=E4=BB=BB?= =?UTF-8?q?=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/.vuepress/sidebar/index.ts | 1 + docs/database/redis/redis-delayed-task.md | 82 +++++++++++++++++++++++ docs/database/redis/redis-questions-01.md | 27 +++++++- 3 files changed, 107 insertions(+), 3 deletions(-) create mode 100644 docs/database/redis/redis-delayed-task.md diff --git a/docs/.vuepress/sidebar/index.ts b/docs/.vuepress/sidebar/index.ts index 18d5dde2..17468afa 100644 --- a/docs/.vuepress/sidebar/index.ts +++ b/docs/.vuepress/sidebar/index.ts @@ -322,6 +322,7 @@ export default sidebar({ icon: "star", collapsible: true, children: [ + "redis-delayed-task", "3-commonly-used-cache-read-and-write-strategies", "redis-data-structures-01", "redis-data-structures-02", diff --git a/docs/database/redis/redis-delayed-task.md b/docs/database/redis/redis-delayed-task.md new file mode 100644 index 00000000..bd95cb46 --- /dev/null +++ b/docs/database/redis/redis-delayed-task.md @@ -0,0 +1,82 @@ +--- +title: 如何基于Redis实现延时任务 +category: 数据库 +tag: + - Redis +--- + +基于 Redis 实现延时任务的功能无非就下面两种方案: + +1. Redis 过期事件监听 +2. Redisson 内置的延时队列 + +面试的时候,你可以先说自己考虑了这两种方案,但最后发现 Redis 过期事件监听这种方案存在很多问题,因此你最终选择了 Redisson 内置的 DelayedQueue 这种方案。 + +这个时候面试官可能会追问你一些相关的问题,我们后面会提到,提前准备就好了。 + +另外,除了下面介绍到的这些问题之外,Redis 相关的常见问题建议你都复习一遍,不排除面试官会顺带问你一些 Redis 的其他问题。 + +### Redis 过期事件监听实现延时任务功能的原理? + +Redis 2.0 引入了发布订阅 (pub/sub) 功能。在 pub/sub 中,引入了一个叫做 **channel(频道)** 的概念,有点类似于消息队列中的 **topic(主题)**。 + +pub/sub 涉及发布者(publisher)和订阅者(subscriber,也叫消费者)两个角色: + +- 发布者通过 `PUBLISH` 投递消息给指定 channel。 +- 订阅者通过`SUBSCRIBE`订阅它关心的 channel。并且,订阅者可以订阅一个或者多个 channel。 + +![Redis 发布订阅 (pub/sub) 功能](https://oss.javaguide.cn/github/javaguide/database/redis/redis-pub-sub.png) + +在 pub/sub 模式下,生产者需要指定消息发送到哪个 channel 中,而消费者则订阅对应的 channel 以获取消息。 + +Redis 中有很多默认的 channel,这些 channel 是由 Redis 本身向它们发送消息的,而不是我们自己编写的代码。其中,`__keyevent@0__:expired` 就是一个默认的 channel,负责监听 key 的过期事件。也就是说,当一个 key 过期之后,Redis 会发布一个 key 过期的事件到`__keyevent@__:expired`这个 channel 中。 + +我们只需要监听这个 channel,就可以拿到过期的 key 的消息,进而实现了延时任务功能。 + +这个功能被 Redis 官方称为 **keyspace notifications** ,作用是实时监控实时监控 Redis 键和值的变化。 + +### Redis 过期事件监听实现延时任务功能有什么缺陷? + +**1、时效性差** + +官方文档的一段介绍解释了时效性差的原因,地址: 。 + +![Redis 过期事件](https://oss.javaguide.cn/github/javaguide/database/redis/redis-timing-of-expired-events.png) + +这段话的核心是:过期事件消息是在 Redis 服务器删除 key 时发布的,而不是一个 key 过期之后就会就会直接发布。 + +我们知道常用的过期数据的删除策略就两个: + +1. **惰性删除**:只会在取出 key 的时候才对数据进行过期检查。这样对 CPU 最友好,但是可能会造成太多过期 key 没有被删除。 +2. **定期删除**:每隔一段时间抽取一批 key 执行删除过期 key 操作。并且,Redis 底层会通过限制删除操作执行的时长和频率来减少删除操作对 CPU 时间的影响。 + +定期删除对内存更加友好,惰性删除对 CPU 更加友好。两者各有千秋,所以 Redis 采用的是 **定期删除+惰性/懒汉式删除** 。 + +因此,就会存在我设置了 key 的过期时间,但到了指定时间 key 还未被删除,进而没有发布过期事件的情况。 + +**2、丢消息** + +Redis 的 pub/sub 模式中的消息并不支持持久化,这与消息队列不同。在 Redis 的 pub/sub 模式中,发布者将消息发送给指定的频道,订阅者监听相应的频道以接收消息。当没有订阅者时,消息会被直接丢弃,在 Redis 中不会存储该消息。 + +**3、多服务实例下消息重复消费** + +Redis 的 pub/sub 模式目前只有广播模式,这意味着当生产者向特定频道发布一条消息时,所有订阅相关频道的消费者都能够收到该消息。 + +这个时候,我们需要注意多个服务实例重复处理消息的问题,这会增加代码开发量和维护难度。 + +### Redisson 延迟队列原理是什么?有什么优势? + +Redisson 是一个开源的 Java 语言 Redis 客户端,提供了很多开箱即用的功能,比如多种分布式锁的实现、延时队列。 + +我们可以借助 Redisson 内置的延时队列 RDelayedQueue 来实现延时任务功能。 + +Redisson 的延迟队列 RDelayedQueue 是基于 Redis 的 SortedSet 来实现的。SortedSet 是一个有序集合,其中的每个元素都可以设置一个分数,代表该元素的权重。Redisson 利用这一特性,将需要延迟执行的任务插入到 SortedSet 中,并给它们设置相应的过期时间作为分数。 + +Redisson 使用 `zrangebyscore` 命令扫描 SortedSet 中过期的元素,然后将这些过期元素从 SortedSet 中移除,并将它们加入到就绪消息列表中。就绪消息列表是一个阻塞队列,有消息进入就会被监听到。这样做可以避免对整个 SortedSet 进行轮询,提高了执行效率。 + +相比于 Redis 过期事件监听实现延时任务功能,这种方式具备下面这些优势: + +1. **减少了丢消息的可能**:DelayedQueue 中的消息会被持久化,即使 Redis 宕机了,根据持久化机制,也只可能丢失一点消息,影响不大。当然了,你也可以使用扫描数据库的方法作为补偿机制。 +2. **消息不存在重复消费问题**:每个客户端都是从同一个目标队列中获取任务的,不存在重复消费的问题。 + +跟 Redisson 内置的延时队列相比,消息队列可以通过保障消息消费的可靠性、控制消息生产者和消费者的数量等手段来实现更高的吞吐量和更强的可靠性,实际项目中首选使用消息队列的延时消息这种方案。 diff --git a/docs/database/redis/redis-questions-01.md b/docs/database/redis/redis-questions-01.md index ec59a416..edcff8ca 100644 --- a/docs/database/redis/redis-questions-01.md +++ b/docs/database/redis/redis-questions-01.md @@ -130,7 +130,7 @@ Redis 从 4.0 版本开始,支持通过 Module 来扩展其功能以满足特 ### Redis 除了做缓存,还能做什么? - **分布式锁**:通过 Redis 来做分布式锁是一种比较常见的方式。通常情况下,我们都是基于 Redisson 来实现分布式锁。关于 Redis 实现分布式锁的详细介绍,可以看我写的这篇文章:[分布式锁详解](https://javaguide.cn/distributed-system/distributed-lock.html) 。 -- **限流**:一般是通过 Redis + Lua 脚本的方式来实现限流。相关阅读:[《我司用了 6 年的 Redis 分布式限流器,可以说是非常厉害了!》](https://mp.weixin.qq.com/s/kyFAWH3mVNJvurQDt4vchA)。 +- **限流**:一般是通过 Redis + Lua 脚本的方式来实现限流。如果不想自己写 Lua 脚本的话,也可以直接利用 Redisson 中的 `RRateLimiter` 来实现分布式限流,其底层实现就是基于 Lua 代码+令牌桶算法。 - **消息队列**:Redis 自带的 List 数据结构可以作为一个简单的队列使用。Redis 5.0 中增加的 Stream 类型的数据结构更加适合用来做消息队列。它比较类似于 Kafka,有主题和消费组的概念,支持消息持久化以及 ACK 机制。 - **延时队列**:Redisson 内置了延时队列(基于 Sorted Set 实现的)。 - **分布式 Session** :利用 String 或者 Hash 数据类型保存 Session 数据,所有的服务器都可以访问。 @@ -139,11 +139,11 @@ Redis 从 4.0 版本开始,支持通过 Module 来扩展其功能以满足特 ### 如何基于 Redis 实现分布式锁? -关于 Redis 实现分布式锁的详细介绍,可以看我写的这篇文章:[分布式锁详解](https://javaguide.cn/distributed-system/distributed-lock.html) 。 +关于 Redis 实现分布式锁的详细介绍,可以看我写的这篇文章:[分布式锁详解](https://javaguide.cn/distributed-system/distributed-lock-implementations.html) 。 ### Redis 可以做消息队列么? -> 实际项目中也没见谁使用 Redis 来做消息队列,对于这部分知识点大家了解就好了。 +> 实际项目中使用 Redis 来做消息队列的非常少,毕竟有更成熟的消息队列中间件可以用。 先说结论:**可以是可以,但不建议使用 Redis 来做消息队列。和专业的消息队列相比,还是有很多欠缺的地方。** @@ -261,6 +261,27 @@ RediSearch 支持中文分词、聚合统计、停用词、同义词、拼写检 Elasticsearch 适用于全文搜索、复杂查询、实时数据分析和聚合的场景,而 RediSearch 适用于快速数据存储、缓存和简单查询的场景。 +### 如何基于 Redis 实现延时任务? + +> 类似的问题: +> +> - 订单在 10 分钟后未支付就失效,如何用 Redis 实现? +> - 红包 24 小时未被查收自动退还,如何用 Redis 实现? + +基于 Redis 实现延时任务的功能无非就下面两种方案: + +1. Redis 过期事件监听 +2. Redisson 内置的延时队列 + +Redis 过期事件监听的存在时效性较差、丢消息、多服务实例下消息重复消费等问题,不被推荐使用。 + +Redisson 内置的延时队列具备下面这些优势: + +1. **减少了丢消息的可能**:DelayedQueue 中的消息会被持久化,即使 Redis 宕机了,根据持久化机制,也只可能丢失一点消息,影响不大。当然了,你也可以使用扫描数据库的方法作为补偿机制。 +2. **消息不存在重复消费问题**:每个客户端都是从同一个目标队列中获取任务的,不存在重复消费的问题。 + +关于 Redis 实现延时任务的详细介绍,可以看我写的这篇文章:[如何基于 Redis 实现延时任务?](./redis-delayed-task.md)。 + ## Redis 数据类型 关于 Redis 5 种基础数据类型和 3 种特殊数据类型的详细介绍请看下面这两篇文章以及 [Redis 官方文档](https://redis.io/docs/data-types/) :