CrabNote螃蟹笔记

Redis常见面试题总结

[![《SpringAI 智能面试平台+RAG 知识库》](/content/images/javaguide/xingqiu/interview-guide-banner.png)](https://

前言 这是 JavaGuide 面试突击版本 ,只保留最常问的面试题,并对重点内容进行了 ⭐️ 标注。提供亮色和暗色两个主题,需要打印的朋友请选择亮色版本。 时间充裕的朋友,推荐使用 JavaGuide 网站系统学习,内容更全面深入。 如果你想要付费支持/面试辅导(比如简历优化、一对一提问、高频考点突击资料等)的话,欢迎了解我的知识星球。已经坚持维护六年,内容持续更新,虽白菜价(0.4元/天)但质量很高,主打一个良心! 面试突击最新版可在公众号回复「 PDF 」获取(知识星球会提前同步最新版)。 重要说明 本站所有面试题保持 年度系统性优化完善 ,严格同步 Java 技术生态与招聘市场的最新动态, 确保内容时效性与前瞻性 。 这部分内容摘自 JavaGuide 下面几篇文章中的重点: Redis 常见面试题总结(上)(Redis 基础、应用、数据类型、持久化机制、线程模型等) Redis 常见面试题总结(下)(Redis 事务、性能优化、生产问题、集群、使用规范等) 如何基于Redis实现延时任务 Redis 5 种基本数据类型详解 Redis 3 种特殊数据类型详解 Redis为什么用跳表实现有序集合 Redis 持久化机制详解 Redis 内存碎片详解 Redis 常见阻塞原因总结 前言 这是 JavaGuide 面试突击版本 ,只保留最常问的面试题,并对重点内容进行了 ⭐️ 标注。提供亮色和暗色两个主题,需要打印的朋友请选择亮色版本。 时间充裕的朋友,推荐使用 JavaGuide 网站系统学习,内容更全面深入。 如果你想要付费支持/面试辅导(比如简历优化、一对一提问、高频考点突击资料等)的话,欢迎了解我的知识星球。已经坚持维护六年,内容持续更新,虽白菜价(0.4元/天)但质量很高,主打一个良心! 面试突击最新版可在公众号回复「 PDF 」获取(知识星球会提前同步最新版)。 重要说明 本站所有面试题保持 年度系统性优化完善 ,严格同步 Java 技术生态与招聘市场的最新动态, 确保内容时效性与前瞻性 。 Redis 基础 什么是 Redis? Redis ( RE mote DI ctionary S erver)是一个基于 C 语言开发的开源 NoSQL 数据库(BSD 许可)。与传统数据库不同的是,Redis 的数据是保存在内存中的(内存数据库,支持持久化),因此读写速度非常快,被广泛应用于分布式缓存方向。并且,Redis 存储的是 KV 键值对数据。 为了满足不同的业务场景,Redis 内置了多种数据类型实现(比如 String、Hash、Sorted Set、Bitmap、HyperLogLog、GEO)。并且,Redis 还支持事务、持久化、Lua 脚本、发布订阅模型、多种开箱即用的集群方案(Redis Sentinel、Redis Cluster)。 Redis 没有外部依赖,Linux 和 OS X 是 Redis 开发和测试最多的两个操作系统,官方推荐生产环境使用 Linux 部署 Redis。 个人学习的话,你可以自己本机安装 Redis 或者通过 Redis 官网提供的在线 Redis 环境(少部分命令无法使用)来实际体验 Redis。 全世界有非常多的网站使用到了 Redis,techstacks.io 专门维护了一个使用 Redis 的热门站点列表,感兴趣的话可以看看。 ⭐️Redis 为什么这么快? Redis 内部做了非常多的性能优化,比较重要的有下面 4 点: 1. 纯内存操作 (Memory Based Storage) :这是最主要的原因。Redis 数据读写操作都发生在内存中,访问速度是纳秒级别,而传统数据库频繁读写磁盘的速度是毫秒级别,两者相差数个数量级。 2. 高效的 I/O 模型 (I/O Multiplexing & Single Threaded Event Loop) :Redis 使用单线程事件循环配合 I/O 多路复用技术,让单个线程可以同时处理多个网络连接上的 I/O 事件(如读写),避免了多线程模型中的上下文切换和锁竞争问题。虽然是单线程,但结合内存操作的高效性和 I/O 多路复用,使得 Redis 能轻松处理大量并发请求(Redis 线程模型会在后文中详细介绍到)。 3. 优化的内部数据结构 (Optimized Data Structures) :Redis 提供多种数据类型(如 String, List, Hash, Set, Sorted Set 等),其内部实现采用高度优化的编码方式(如 ziplist, quicklist, skiplist, hashtable 等)。Redis 会根据数据大小和类型动态选择最合适的内部编码,以在性能和空间效率之间取得最佳平衡。 4. 简洁高效的通信协议 (Simple Protocol RESP) :Redis 使用的是自己设计的 RESP (REdis Serialization Protocol) 协议。这个协议实现简单、解析性能好,并且是二进制安全的。客户端和服务端之间通信的序列化/反序列化开销很小,有助于提升整体的交互速度。 下面这张图片总结的挺不错的,分享一下,出自 Why is Redis so fast?。 那既然都这么快了,为什么不直接用 Redis 当主数据库呢?主要是因为内存成本太高,并且 Redis 提供的数据持久化仍然有数据丢失的风险。 除了 Redis,你还知道其他分布式缓存方案吗? 如果面试中被问到这个问题的话,面试官主要想看看: 1. 你在选择 Redis 作为分布式缓存方案时,是否是经过严谨的调研和思考,还是只是因为 Redis 是当前的“热门”技术。 2. 你在分布式缓存方向的技术广度。 如果你了解其他方案,并且能解释为什么最终选择了 Redis(更进一步!),这会对你面试表现加分不少! 下面简单聊聊常见的分布式缓存技术选型。 分布式缓存的话,比较老牌同时也是使用的比较多的还是 Memcached 和 Redis 。不过,现在基本没有看过还有项目使用 Memcached 来做缓存,都是直接用 Redis 。 Memcached 是分布式缓存最开始兴起的那会,比较常用的。后来,随着 Redis 的发展,大家慢慢都转而使用更加强大的 Redis 了。 有一些大厂也开源了类似于 Redis 的分布式高性能 KV 存储数据库,例如,腾讯开源的 Tendis 。Tendis 基于知名开源项目 RocksDB 作为存储引擎 ,100% 兼容 Redis 协议和 Redis4.0 所有数据模型。关于 Redis 和 Tendis 的对比,腾讯官方曾经发过一篇文章:Redis vs Tendis:冷热混合存储版架构揭秘,可以简单参考一下。 不过,从 Tendis 这个项目的 Github 提交记录可以看出,Tendis 开源版几乎已经没有被维护更新了,加上其关注度并不高,使用的公司也比较少。因此,不建议你使用 Tendis 来实现分布式缓存。 目前,比较业界认可的 Redis 替代品还是下面这两个开源分布式缓存(都是通过碰瓷 Redis 火的): Dragonfly:一种针对现代应用程序负荷需求而构建的内存数据库,完全兼容 Redis 和 Memcached 的 API,迁移时无需修改任何代码,号称全世界最快的内存数据库。 KeyDB:Redis 的一个高性能分支,专注于多线程、内存效率和高吞吐量。 不过,个人还是建议分布式缓存首选 Redis,毕竟经过了这么多年的考验,生态非常优秀,资料也很全面! PS:篇幅问题,我这并没有对上面提到的分布式缓存选型做详细介绍和对比,感兴趣的话,可以自行研究一下。 说一下 Redis 和 Memcached 的区别和共同点 现在公司一般都是用 Redis 来实现缓存,而且 Redis 自身也越来越强大了!不过,了解 Redis 和 Memcached 的区别和共同点,有助于我们在做相应的技术选型的时候,能够做到有理有据! 共同点 : 1. 都是基于内存的数据库,一般都用来当做缓存使用。 2. 都有过期策略。 3. 两者的性能都非常高。 区别 : 1. 数据类型 :Redis 支持更丰富的数据类型(支持更复杂的应用场景)。Redis 不仅仅支持简单的 k/v 类型的数据,同时还提供 list、set、zset、hash 等数据结构的存储;而 Memcached 只支持最简单的 k/v 数据类型。 2. 数据持久化 :Redis 支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用;而 Memcached 把数据全部存在内存之中。也就是说,Redis 有灾难恢复机制,而 Memcached 没有。 3. 集群模式支持 :Memcached 没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据;而 Redis 自 3.0 版本起是原生支持集群模式的。 4. 线程模型 :Memcached 是多线程、非阻塞 IO 复用的网络模型;而 Redis 使用单线程的多路 IO 复用模型(Redis 6.0 针对网络数据的读写引入了多线程)。 5. 特性支持 :Redis 支持发布订阅模型、Lua 脚本、事务等功能,而 Memcached 不支持。并且,Redis 支持更多的编程语言。 6. 过期数据删除 :Memcached 过期数据的删除策略只用了惰性删除,而 Redis 同时使用了惰性删除与定期删除。 相信看了上面的对比之后,我们已经没有什么理由可以选择使用 Memcached 来作为自己项目的分布式缓存了。 ⭐️为什么要用 Redis? 1、访问速度更快 传统数据库数据保存在磁盘,而 Redis 基于内存,内存的访问速度比磁盘快很多。引入 Redis 之后,我们可以把一些高频访问的数据放到 Redis 中,这样下次就可以直接从内存中读取,速度可以提升几十倍甚至上百倍。 2、高并发 一般像 MySQL 这类的数据库的 QPS 大概都在 4k 左右(4 核 8g),但是使用 Redis 缓存之后很容易达到 5w+,甚至能达到 10w+(就单机 Redis 的情况,Redis 集群的话会更高)。 QPS(Query Per Second):服务器每秒可以执行的查询次数; 由此可见,直接操作缓存能够承受的数据库请求数量是远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。进而,我们也就提高了系统整体的并发。 3、功能全面 Redis 除了可以用作缓存之外,还可以用于分布式锁、限流、消息队列、延时队列等场景,功能强大! ⭐️为什么用 Redis 而不用本地缓存呢? | 特性 | 本地缓存 | Redis | | | | | | 数据一致性 | 多服务器部署时存在数据不一致问题 | 数据一致 | | 内存限制 | 受限于单台服务器内存 | 独立部署,内存空间更大 | | 数据丢失风险 | 服务器宕机数据丢失 | 可持久化,数据不易丢失 | | 管理维护 | 分散,管理不便 | 集中管理,提供丰富的管理工具 | | 功能丰富性 | 功能有限,通常只提供简单的键值对存储 | 功能丰富,支持多种数据结构和功能 | 常见的缓存读写策略有哪些? 关于常见的缓存读写策略的详细介绍,可以看我写的这篇文章:3 种常用的缓存读写策略详解。 什么是 Redis Module?有什么用? Redis 从 4.0 版本开始,支持通过 Module 来扩展其功能以满足特殊的需求。这些 Module 以动态链接库(so 文件)的形式被加载到 Redis 中,这是一种非常灵活的动态扩展功能的实现方式,值得借鉴学习! 我们每个人都可以基于 Redis 去定制化开发自己的 Module,比如实现搜索引擎功能、自定义分布式锁和分布式限流。 目前,被 Redis 官方推荐的 Module 有: RediSearch:用于实现搜索引擎的模块。 RedisJSON:用于处理 JSON 数据的模块。 RedisGraph:用于实现图形数据库的模块。 RedisTimeSeries:用于处理时间序列数据的模块。 RedisBloom:用于实现布隆过滤器的模块。 RedisAI:用于执行深度学习/机器学习模型并管理其数据的模块。 RedisCell:用于实现分布式限流的模块。 …… 关于 Redis 模块的详细介绍,可以查看官方文档:<https://redis.io/modules 。 ⭐️Redis 应用 Redis 除了做缓存,还能做什么? 分布式锁 :通过 Redis 来做分布式锁是一种比较常见的方式。通常情况下,我们都是基于 Redisson 来实现分布式锁。关于 Redis 实现分布式锁的详细介绍,可以看我写的这篇文章:分布式锁详解。 限流 :一般是通过 Redis + Lua 脚本的方式来实现限流。如果不想自己写 Lua 脚本的话,也可以直接利用 Redisson 中的 来实现分布式限流,其底层实现就是基于 Lua 代码+令牌桶算法。 消息队列 :Redis 自带的 List 数据结构可以作为一个简单的队列使用。Redis 5.0 中增加的 Stream 类型的数据结构更加适合用来做消息队列。它比较类似于 Kafka,有主题和消费组的概念,支持消息持久化以及 ACK 机制。 延时队列 :Redisson 内置了延时队列(基于 Sorted Set 实现的)。 分布式 Session :利用 String 或者 Hash 数据类型保存 Session 数据,所有的服务器都可以访问。 复杂业务场景 :通过 Redis 以及 Redis 扩展(比如 Redisson)提供的数据结构,我们可以很方便地完成很多复杂的业务场景,比如通过 Bitmap 统计活跃用户、通过 Sorted Set 维护排行榜、通过 HyperLogLog 统计网站 UV 和 PV。 …… 如何基于 Redis 实现分布式锁? 关于 Redis 实现分布式锁的详细介绍,可以看我写的这篇文章:分布式锁详解。 Redis 可以做消息队列么? 实际项目中使用 Redis 来做消息队列的非常少,毕竟有更成熟的消息队列中间件可以用。 先说结论: 可以是可以,但不建议使用 Redis 来做消息队列。和专业的消息队列相比,还是有很多欠缺的地方。 Redis 2.0 之前,如果想要使用 Redis 来做消息队列的话,只能通过 List 来实现。 通过 或者 即可实现简易版消息队列: 不过,通过 或者 这样的方式存在性能问题,我们需要不断轮询去调用 或 来消费消息。当 List 为空时,大部分的轮询的请求都是无效请求,这种方式大量浪费了系统资源。 因此,Redis 还提供了 、 这种阻塞式读取的命令(带 B Blocking 的都是阻塞式),并且还支持一个超时参数。如果 List 为空,Redis 服务端不会立刻返回结果,它会等待 List 中有新数据后再返回或者是等待最多一个超时时间后返回空。如果将超时时间设置为 0 时,即可无限等待,直到弹出消息 List 实现消息队列功能太简单,像消息确认机制等功能还需要我们自己实现,最要命的是没有广播机制,消息也只能被消费一次。 Redis 2.0 引入了发布订阅 (pub/sub) 功能,解决了 List 实现消息队列没有广播机制的问题。 pub/sub 中引入了一个概念叫 channel(频道) ,发布订阅机制的实现就是基于这个 channel 来做的。 pub/sub 涉及发布者(Publisher)和订阅者(Subscriber,也叫消费者)两个角色: 发布者通过 投递消息给指定 channel。 订阅者通过 订阅它关心的 channel。并且,订阅者可以订阅一个或者多个 channel。 我们这里启动 3 个 Redis 客户端来简单演示一下: pub/sub 既能单播又能广播,还支持 channel 的简单正则匹配。不过,消息丢失(客户端断开连接或者 Redis 宕机都会导致消息丢失)、消息堆积(发布者发布消息的时候不会管消费者的具体消费能力如何)等问题依然没有一个比较好的解决办法。 为此,Redis 5.0 新增加的一个数据结构 来做消息队列。 支持: 发布 / 订阅模式; 按照消费者组进行消费(借鉴了 Kafka 消费者组的概念); 消息持久化( RDB 和 AOF); ACK 机制(通过确认机制来告知已经成功处理了消息); 阻塞式获取消息。 的结构如下: 这是一个有序的消息链表,每个消息都有一个唯一的 ID 和对应的内容。ID 是一个时间戳和序列号的组合,用来保证消息的唯一性和递增性。内容是一个或多个键值对(类似 Hash 基本数据类型),用来存储消息的数据。 这里再对图中涉及到的一些概念,进行简单解释: :消费者组用于组织和管理多个消费者。消费者组本身不处理消息,而是再将消息分发给消费者,由消费者进行真正的消费。 :标识消费者组当前消费位置的游标,消费者组中任意一个消费者读取了消息都会使 last\ delivered\ id 往前移动。 :记录已经被客户端消费但没有 ack 的消息的 ID。 下面是 用作消息队列时常用的命令: :向流中添加新的消息。 :从流中读取消息。 :从消费组中读取消息。 :根据消息 ID 范围读取流中的消息。 :与 类似,但以相反顺序返回结果。 :从流中删除消息。 :修剪流的长度,可以指定修建策略( / )。 :获取流的长度。 :创建消费者组。 :删除消费者组。 :从消费者组中删除一个消费者。 :为消费者组设置新的最后递送消息 ID。 :确认消费组中的消息已被处理。 :查询消费组中挂起(未确认)的消息。 :将挂起的消息从一个消费者转移到另一个消费者。 :获取流( )、消费组( )或消费者( )的详细信息。 使用起来相对要麻烦一些,这里就不演示了。 总的来说, 已经可以满足一个消息队列的基本要求了。不过, 在实际使用中依然会有一些小问题不太好解决,比如在 Redis 发生故障恢复后不能保证消息至少被消费一次。 综上,和专业的消息队列相比,使用 Redis 来实现消息队列还是有很多欠缺的地方,比如消息丢失和堆积问题不好解决。因此,我们通常建议不要使用 Redis 来做消息队列,你完全可以选择市面上比较成熟的一些消息队列,比如 RocketMQ、Kafka。不过,如果你就是想要用 Redis 来做消息队列的话,那我建议你优先考虑 ,这是目前相对最优的 Redis 消息队列实现。 相关阅读:Redis 消息队列发展历程 阿里开发者 2022。 Redis 可以做搜索引擎么? Redis 是可以实现全文搜索引擎功能的,需要借助 RediSearch ,这是一个基于 Redis 的搜索引擎模块。 RediSearch 支持中文分词、聚合统计、停用词、同义词、拼写检查、标签查询、向量相似度查询、多关键词搜索、分页搜索等功能,算是一个功能比较完善的全文搜索引擎了。 相比较于 Elasticsearch 来说,RediSearch 主要在下面两点上表现更优异一些: 1. 性能更优秀:依赖 Redis 自身的高性能,基于内存操作(Elasticsearch 基于磁盘)。 2. 较低内存占用实现快速索引:RediSearch 内部使用压缩的倒排索引,所以可以用较低的内存占用来实现索引的快速构建。 对于小型项目的简单搜索场景来说,使用 RediSearch 来作为搜索引擎还是没有问题的(搭配 RedisJSON 使用)。 对于比较复杂或者数据规模较大的搜索场景,还是不太建议使用 RediSearch 来作为搜索引擎,主要是因为下面这些限制和问题: 1. 数据量限制:Elasticsearch 可以支持 PB 级别的数据量,可以轻松扩展到多个节点,利用分片机制提高可用性和性能。RedisSearch 是基于 Redis 实现的,其能存储的数据量受限于 Redis 的内存容量,不太适合存储大规模的数据(内存昂贵,扩展能力较差)。 2. 分布式能力较差:Elasticsearch 是为分布式环境设计的,可以轻松扩展到多个节点。虽然 RedisSearch 支持分布式部署,但在实际应用中可能会面临一些挑战,如数据分片、节点间通信、数据一致性等问题。 3. 聚合功能较弱:Elasticsearch 提供了丰富的聚合功能,而 RediSearch 的聚合功能相对较弱,只支持简单的聚合操作。 4. 生态较差:Elasticsearch 可以轻松和常见的一些系统/软件集成比如 Hadoop、Spark、Kibana,而 RedisSearch 则不具备该优势。 Elasticsearch 适用于全文搜索、复杂查询、实时数据分析和聚合的场景,而 RediSearch 适用于快速数据存储、缓存和简单查询的场景。 如何基于 Redis 实现延时任务? 类似的问题: 订单在 10 分钟后未支付就失效,如何用 Redis 实现? 红包 24 小时未被查收自动退还,如何用 Redis 实现? 基于 Redis 实现延时任务的功能无非就下面两种方案: 1. Redis 过期事件监听。 2. Redisson 内置的延时队列。 Redis 过期事件监听存在时效性较差、丢消息、多服务实例下消息重复消费等问题,不被推荐使用。 Redisson 内置的延时队列具备下面这些优势: 1. 减少了丢消息的可能 :DelayedQueue 中的消息会被持久化,即使 Redis 宕机了,根据持久化机制,也只可能丢失一点消息,影响不大。当然了,你也可以使用扫描数据库的方法作为补偿机制。 2. 消息不存在重复消费问题 :每个客户端都是从同一个目标队列中获取任务的,不存在重复消费的问题。 关于 Redis 实现延时任务的详细介绍,可以看我写的这篇文章:如何基于 Redis 实现延时任务?。 ⭐️Redis 数据类型 关于 Redis 5 种基础数据类型和 3 种特殊数据类型的详细介绍请看下面这两篇文章以及 Redis 官方文档: Redis 5 种基本数据类型详解 Redis 3 种特殊数据类型详解 Redis 常用的数据类型有哪些? Redis 中比较常见的数据类型有下面这些: 5 种基础数据类型 :String(字符串)、List(列表)、Set(集合)、Hash(散列)、Zset(有序集合)。 3 种特殊数据类型 :HyperLogLog(基数统计)、Bitmap (位图)、Geospatial (地理位置)。 除了上面提到的之外,还有一些其他的比如 Bloom filter(布隆过滤器)、Bitfield(位域)。 String 的应用场景有哪些? String 是 Redis 中最简单同时也是最常用的一个数据类型。它是一种二进制安全的数据类型,可以用来存储任何类型的数据比如字符串、整数、浮点数、图片(图片的 base64 编码或者解码或者图片的路径)、序列化后的对象。 String 的常见应用场景如下: 常规数据(比如 Session、Token、序列化后的对象、图片的路径)的缓存; 计数比如用户单位时间的请求数(简单限流可以用到)、页面单位时间的访问数; 分布式锁(利用 命令可以实现一个最简易的分布式锁); …… 关于 String 的详细介绍请看这篇文章:Redis 5 种基本数据类型详解。 String 还是 Hash 存储对象数据更好呢? 简单对比一下二者: 对象存储方式 :String 存储的是序列化后的对象数据,存放的是整个对象,操作简单直接。Hash 是对对象的每个字段单独存储,可以获取部分字段的信息,也可以修改或者添加部分字段,节省网络流量。如果对象中某些字段需要经常变动或者经常需要单独查询对象中的个别字段信息,Hash 就非常适合。 内存消耗 :Hash 通常比 String 更节省内存,特别是在字段较多且字段长度较短时。Redis 对小型 Hash 进行优化(如使用 ziplist 存储),进一步降低内存占用。 复杂对象存储 :String 在处理多层嵌套或复杂结构的对象时更方便,因为无需处理每个字段的独立存储和操作。 性能 :String 的操作通常具有 O(1) 的时间复杂度,因为它存储的是整个对象,操作简单直接,整体读写的性能较好。Hash 由于需要处理多个字段的增删改查操作,在字段较多且经常变动的情况下,可能会带来额外的性能开销。 总结: 在绝大多数情况下, String 更适合存储对象数据,尤其是当对象结构简单且整体读写是主要操作时。 如果你需要频繁操作对象的部分字段或节省内存, Hash 可能是更好的选择。 String 的底层实现是什么? Redis 是基于 C 语言编写的,但 Redis 的 String 类型的底层实现并不是 C 语言中的字符串(即以空字符 结尾的字符数组),而是自己编写了 SDS(Simple Dynamic String,简单动态字符串)来作为底层实现。 SDS 最早是 Redis 作者为日常 C 语言开发而设计的 C 字符串,后来被应用到了 Redis 上,并经过了大量的修改完善以适合高性能操作。 Redis7.0 的 SDS 的部分源码如下(<https://github.com/redis/redis/blob/7.0/src/sds.h ): 通过源码可以看出,SDS 共有五种实现方式:SDS\ TYPE\ 5(并未用到)、SDS\ TYPE\ 8、SDS\ TYPE\ 16、SDS\ TYPE\ 32、SDS\ TYPE\ 64,其中只有后四种实际用到。Redis 会根据初始化的长度决定使用哪种类型,从而减少内存的使用。 | 类型 | 字节 | 位 | | | | | | sdshdr5 | < 1 | <8 | | sdshdr8 | 1 | 8 | | sdshdr16 | 2 | 16 | | sdshdr32 | 4 | 32 | | sdshdr64 | 8 | 64 | 对于后四种实现都包含了下面这 4 个属性: :字符串的长度也就是已经使用的字节数。 :总共可用的字符空间大小,alloc len 就是 SDS 剩余的空间大小。 :实际存储字符串的数组。 :低三位保存类型标志。 SDS 相比于 C 语言中的字符串有如下提升: 1. 可以避免缓冲区溢出 :C 语言中的字符串被修改(比如拼接)时,一旦没有分配足够长度的内存空间,就会造成缓冲区溢出。SDS 被修改时,会先根据 len 属性检查空间大小是否满足要求,如果不满足,则先扩展至所需大小再进行修改操作。 2. 获取字符串长度的复杂度较低 :C 语言中的字符串的长度通常是经过遍历计数来实现的,时间复杂度为 O(n)。SDS 的长度获取直接读取 len 属性即可,时间复杂度为 O(1)。 3. 减少内存分配次数 :为了避免修改(增加/减少)字符串时,每次都需要重新分配内存(C 语言的字符串是这样的),SDS 实现了空间预分配和惰性空间释放两种优化策略。当 SDS 需要增加字符串时,Redis 会为 SDS 分配好内存,并且根据特定的算法分配多余的内存,这样可以减少连续执行字符串增长操作所需的内存重分配次数。当 SDS 需要减少字符串时,这部分内存不会立即被回收,会被记录下来,等待后续使用(支持手动释放,有对应的 API)。 4. 二进制安全 :C 语言中的字符串以空字符 作为字符串结束的标识,这存在一些问题,像一些二进制文件(比如图片、视频、音频)就可能包括空字符,C 字符串无法正确保存。SDS 使用 len 属性判断字符串是否结束,不存在这个问题。 🤐 多提一嘴,很多文章里 SDS 的定义是下面这样的: 这个也没错,Redis 3.2 之前就是这样定义的。后来,由于这种方式的定义存在问题, 和 的定义用了 4 个字节,造成了浪费。Redis 3.2 之后,Redis 改进了 SDS 的定义,将其划分为了现在的 5 种类型。 购物车信息用 String 还是 Hash 存储更好呢? 由于购物车中的商品频繁修改和变动,购物车信息建议使用 Hash 存储: 用户 id 为 key 商品 id 为 field,商品数量为 value 那用户购物车信息的维护具体应该怎么操作呢? 用户添加商品就是往 Hash 里面增加新的 field 与 value; 查询购物车信息就是遍历对应的 Hash; 更改商品数量直接修改对应的 value 值(直接 set 或者做运算皆可); 删除商品就是删除 Hash 中对应的 field; 清空购物车直接删除对应的 key 即可。 这里只是以业务比较简单的购物车场景举例,实际电商场景下,field 只保存一个商品 id 是没办法满足需求的。 使用 Redis 实现一个排行榜怎么做? Redis 中有一个叫做 (有序集合)的数据类型经常被用在各种排行榜的场景,比如直播间送礼物的排行榜、朋友圈的微信步数排行榜、王者荣耀中的段位排行榜、话题热度排行榜等等。 相关的一些 Redis 命令: (从小到大排序)、 (从大到小排序)、 (指定元素排名)。 《Java 面试指北》 的「技术面试题篇」就有一篇文章详细介绍如何使用 Sorted Set 来设计制作一个排行榜,感兴趣的小伙伴可以看看。 Redis 的有序集合底层为什么要用跳表,而不用平衡树、红黑树或者 B+ 树? 这道面试题很多大厂比较喜欢问,难度还是有点大的。 平衡树 vs 跳表:平衡树的插入、删除和查询的时间复杂度和跳表一样都是 O(log n) 。对于范围查询来说,平衡树也可以通过中序遍历的方式达到和跳表一样的效果。但是它的每一次插入或者删除操作都需要保证整颗树左右节点的绝对平衡,只要不平衡就要通过旋转操作来保持平衡,这个过程是比较耗时的。跳表诞生的初衷就是为了克服平衡树的一些缺点。跳表使用概率平衡而不是严格强制的平衡,因此,跳表中的插入和删除算法比平衡树的等效算法简单得多,速度也快得多。 红黑树 vs 跳表:相比较于红黑树来说,跳表的实现也更简单一些,不需要通过旋转和染色(红黑变换)来保证黑平衡。并且,按照区间来查找数据这个操作,红黑树的效率没有跳表高。 B+ 树 vs 跳表:B+ 树更适合作为数据库和文件系统中常用的索引结构之一,它的核心思想是通过可能少的 IO 定位到尽可能多的索引来获得查询数据。对于 Redis 这种内存数据库来说,它对这些并不感冒,因为 Redis 作为内存数据库它不可能存储大量的数据,所以对于索引不需要通过 B+ 树这种方式进行维护,只需按照概率进行随机维护即可,节约内存。而且使用跳表实现 zset 时相较前者来说更简单一些,在进行插入时只需通过索引将数据插入到链表中合适的位置再随机维护一定高度的索引即可,也不需要像 B+ 树那样插入时发现失衡时还需要对节点分裂与合并。 另外,我还单独写了一篇文章从有序集合的基本使用到跳表的源码分析和实现,让你会对 Redis 的有序集合底层实现的跳表有着更深刻的理解和掌握:Redis 为什么用跳表实现有序集合。 Set 的应用场景是什么? Redis 中 是一种无序集合,集合中的元素没有先后顺序但都唯一,有点类似于 Java 中的 。 的常见应用场景如下: 存放的数据不能重复的场景:网站 UV 统计(数据量巨大的场景还是 更适合一些)、文章点赞、动态点赞等等。 需要获取多个数据源交集、并集和差集的场景:共同好友(交集)、共同粉丝(交集)、共同关注(交集)、好友推荐(差集)、音乐推荐(差集)、订阅号推荐(差集+交集)等等。 需要随机获取数据源中的元素的场景:抽奖系统、随机点名等等。 使用 Set 实现抽奖系统怎么做? 如果想要使用 实现一个简单的抽奖系统的话,直接使用下面这几个命令就可以了: :向指定集合添加一个或多个元素。 :随机移除并获取指定集合中一个或多个元素,适合不允许重复中奖的场景。 :随机获取指定集合中指定数量的元素,适合允许重复中奖的场景。 使用 Bitmap 统计活跃用户怎么做? Bitmap 存储的是连续的二进制数字(0 和 1),通过 Bitmap,只需要一个 bit 位来表示某个元素对应的值或者状态,key 就是对应元素本身。我们知道 8 个 bit 可以组成一个 byte,所以 Bitmap 本身会极大的节省储存空间。 你可以将 Bitmap 看作是一个存储二进制数字(0 和 1)的数组,数组中每个元素的下标叫做 offset(偏移量)。 如果想要使用 Bitmap 统计活跃用户的话,可以使用日期(精确到天)作为 key,然后用户 ID 为 offset,如果当日活跃过就设置为 1。 初始化数据: 统计 20210308 20210309 总活跃用户数: 统计 20210308 20210309 在线活跃用户数: HyperLogLog 适合什么场景? HyperLogLog (HLL) 是一种非常巧妙的概率性数据结构,它专门解决一类非常棘手的大数据问题:在海量数据中,用极小的内存,估算一个集合中不重复元素的数量,也就是我们常说的基数(Cardinality) HLL 做的最核心的权衡,就是用一点点精确度的损失,来换取巨大的内存空间节省。它给出的不是一个 100%精确的数字,而是一个带有很小标准误差(Redis 中默认是 0.81%)的近似值。 基于这个核心权衡,HyperLogLog 最适合以下特征的场景: 1. 数据量巨大,内存敏感: 这是 HLL 的主战场。比如,要统计一个亿级日活 App 的每日独立访客数。如果用传统的 Set 来存储用户 ID,一个 ID 占几十个字节,上亿个 ID 可能需要几个 GB 甚至几十 GB 的内存,这在很多场景下是不可接受的。而 HLL,在 Redis 中只需要固定的 12KB 内存,就能处理天文数字级别的基数,这是一个颠覆性的优势。 2. 对结果的精确度要求不是 100%: 这是使用 HLL 的前提。比如,产品经理想知道一个热门帖子的 UV(独立访客数)是大约 1000 万还是 1010 万,这个细微的差别通常不影响商业决策。但如果场景是统计一个交易系统的准确交易笔数,那 HLL 就完全不适用,因为金融场景要求 100%的精确。 所以,HyperLogLog 具体的应用场景就非常清晰了: 网站/App 的 UV(Unique Visitor)统计: 比如统计首页每天有多少个不同的 IP 或用户 ID 访问过。 搜索引擎关键词统计: 统计每天有多少个不同的用户搜索了某个关键词。 社交网络互动统计: 比如统计一条微博被多少个不同的用户转发过。 在这些场景下,我们关心的是数量级和趋势,而不是个位数的差异。 最后,Redis 的实现还非常智能,它内部会根据基数的大小,在 稀疏矩阵 (占用空间更小)和 稠密矩阵 (固定的 12KB)之间自动切换,进一步优化了内存使用。总而言之,当您需要对海量数据进行去重计数,并且可以接受微小误差时,HyperLogLog 就是不二之选。 使用 HyperLogLog 统计页面 UV 怎么做? 使用 HyperLogLog 统计页面 UV 主要需要用到下面这两个命令: :添加一个或多个元素到 HyperLogLog 中。 :获取一个或者多个 HyperLogLog 的唯一计数。 1、将访问指定页面的每个用户 ID 添加到 中。 2、统计指定页面的 UV。 如果我想判断一个元素是否不在海量元素集合中,用什么数据类型? 这是布隆过滤器的经典应用场景。布隆过滤器可以告诉你一个元素一定不存在或者可能存在,它也有极高的空间效率和一定的误判率,但绝不会漏报。也就是说,布隆过滤器说某个元素存在,小概率会误判。布隆过滤器说某个元素不在,那么这个元素一定不在。 Bloom Filter 的简单原理图如下: 当字符串存储要加入到布隆过滤器中时,该字符串首先由多个哈希函数生成不同的哈希值,然后将对应的位数组