尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

【面朝大厂】面试官:Redis的数据是存在内存里吗?谈谈Redis各种数据类型的使用场景?2万字详解

【面朝大厂】面试官:Redis的数据是存在内存里吗?谈谈Redis各种数据类型的使用场景?2万字详解 一、开篇从一场大厂面试说起“同学你好咱们先聊一个比较基础的问题Redis 的数据是存在内存里吗”很多候选人听到这个问题第一反应是“是的Redis 是内存数据库数据都在内存里所以读写很快。”这个答案本身没有错但如果只答到这里面试官通常会顺着往下追问“那断电之后数据不就全丢了吗Redis 靠什么保证数据不丢内存那么贵数据量大了怎么办你说 Redis 是内存数据库那它内部的字符串和 Java 的 String 是一回事吗”短短几个追问就能把“背八股文”和“真正理解 Redis”的候选人区分开。其实这一连串问题的背后考察的是三个层面的能力第一层基础认知。能不能说清楚 Redis 的数据存储模型内存与磁盘之间的关系以及为什么内存存储能带来极高的性能。第二层底层原理。能不能理解 Redis 的内存管理、对象编码、过期策略以及 RDB、AOF 持久化的工作机制。第三层工程落地。能不能结合真实业务场景说清楚 String、Hash、List、Set、ZSet 等数据类型分别适合解决什么问题以及各自有哪些坑。本篇文章就从面试官的视角出发围绕“Redis 的数据到底存在哪里”和“各种数据类型的使用场景”这两个核心问题展开一次接近两万字的系统梳理。文章会尽量用通俗的语言、真实的案例和可运行的代码把知识点讲透帮助你在面试中不但答得出还能答出亮点。二、Redis 的数据到底存在哪里2.1 结论先行默认存在内存里但不只是存在内存里Redis 的全称是 Remote Dictionary Server本质上是一个基于内存的键值存储系统。它的核心数据默认保存在内存中这也是 Redis 单机读写能达到每秒数万甚至十万级别 QPS 的根本原因。内存随机访问的延迟通常在几十到一百纳秒量级而磁盘哪怕是 NVMe SSD随机读延迟也在几十到上百微秒两者相差好几个数量级。但需要注意“数据在内存里”并不等价于“数据只会在内存里”。Redis 提供了完善的持久化机制可以把内存中的数据定期或实时地写入磁盘。换句话说内存是 Redis 对外提供服务的“工作区”而磁盘是数据安全与恢复的“备份区”。真正严谨的说法是Redis 的数据以内存为主要存储介质同时通过 RDB、AOF 以及混合持久化等机制将数据异步或同步地落盘从而在性能和可靠性之间取得平衡。这句表述既回答了面试官的问题又自然地把话题引向持久化机制是一个非常稳妥的答题节奏。2.2 为什么数据放在内存里会快内存快不仅仅是因为介质本身快更重要的原因是 Redis 的整体设计配合了内存特性避免了大量不必要的开销。具体可以拆成几点单线程事件驱动模型。Redis 6.0 之前网络 IO 和命令执行都是单线程主线程只负责处理命令不存在多线程切换和锁竞争。数据都在内存里单次操作耗时极短所以单线程也完全能扛住高并发。高效的数据结构。Redis 没有直接复用编程语言自带的容器而是自己实现了简单动态字符串 SDS、压缩列表、跳表、哈希表、整数集合等结构针对内存中的读写做了大量优化。对象编码灵活切换。同一个数据类型在不同规模下会使用不同的底层编码比如小集合用整数集合元素变多后才升级为哈希表兼顾内存占用和存取效率。IO 复用。通过 epoll、kqueue 等机制同时监听大量客户端连接单线程也能高效处理成千上万个连接。从这些点可以看出Redis 的快是“内存 架构 数据结构”综合作用的结果而不是简单一句“因为放内存”就能概括的。2.3 内存存储的两大代价把数据放在内存里也有代价面试中主动提到这些代价往往能显得思考更加全面。容量受限。内存的价格远高于磁盘单机内存通常只有几十 GB 到几百 GB无法像磁盘那样轻松存下 TB 级数据。因此 Redis 更适合存热点数据、缓存数据、实时状态等而不是当主数据库存全量业务数据。易失性风险。断电、进程崩溃、机器宕机都可能导致内存数据丢失。必须依靠持久化、主从复制、哨兵或集群等手段来降低数据丢失风险。理解这两点是后面理解缓存设计、持久化选型、淘汰策略的重要前提。三、持久化机制内存数据如何不丢接下来详细拆解持久化。面试官问“数据放在内存里吗”本质上就是在考察你对持久化的理解。Redis 主要提供三种持久化思路RDB、AOF以及从 Redis 4.0 开始支持的混合持久化。3.1 RDB内存快照RDB全称 Redis Database是指 Redis 在某个时间点把内存中的全部数据生成一份快照并保存到磁盘上的二进制文件。默认文件叫 dump.rdb。触发 RDB 的方式主要有两种手动执行 SAVE 或 BGSAVE 命令以及通过配置文件中的 save 规则自动触发。SAVE 会在主线程中同步生成快照期间阻塞所有客户端请求而 BGSAVE 使用 fork 子进程在后台生成快照父进程继续处理请求是生产环境的主流方式。RDB 生成快照时由于使用写时复制机制即使子进程把内存数据写入磁盘需要较长时间父进程仍能正常读写。RDB 文件是一个紧凑的二进制文件非常适合做数据备份、灾难恢复和冷备份。它的缺点是两次快照之间发生宕机最近一次快照之后的数据会丢失。通常 RDB 的保存间隔是分钟级所以可容忍的数据丢失量相对较大。3.2 AOF追加写命令AOF全称 Append Only File记录的是 Redis 收到的每一条写命令。Redis 重启时通过重新执行 AOF 文件中的命令来恢复数据类似于数据库的 redo log。AOF 的落盘时机由 appendfsync 参数控制always每条写命令都立即同步到磁盘数据最安全但性能最差。everysec每秒同步一次性能和安全折中生产环境最常用最多可能丢失一秒的数据。no由操作系统决定何时刷盘性能最好但数据安全最不可控。随着时间推移AOF 文件会不断变大Redis 提供 AOF 重写机制。重写并不是基于原文件压缩而是直接读取当前内存中的数据生成等价的、更精简的命令集合写入新文件。重写同样通过 fork 子进程完成不会阻塞主线程。AOF 相比 RDB 数据更安全可丢失数据更少但文件通常更大、恢复速度更慢。生产中常把两者都开启。3.3 混合持久化Redis 4.0 开始支持混合持久化。开启后AOF 重写时子进程会把当前内存数据先以 RDB 格式写入 AOF 文件头部再把重写缓冲区中的增量命令以 AOF 格式追加到文件末尾。这样一份文件同时具备 RDB 恢复快、AOF 丢数据少的优点是多数生产环境的推荐方案。3.4 面试常见追问RDB 会丢多少数据取决于快照间隔比如 save 900 1 表示 900 秒内有一次修改就保存最坏可能丢 15 分钟的数据。AOF 重写会阻塞吗主线程不会阻塞但 fork 子进程瞬间可能有短暂的卡顿重写期间父进程继续写旧文件完成后原子替换。BGSAVE 和 AOF 重写能同时进行吗Redis 会通过调度避免两个子进程同时做大量磁盘写入通常先执行一个另一个延后。如果同时开启 RDB 和 AOF重启时用哪个优先用 AOF因为 AOF 通常数据更新更完整。四、Redis 的内存管理与淘汰策略既然数据放在内存里内存早晚有占满的一天。Redis 通过 maxmemory 设置可用内存上限当达到上限时依靠淘汰策略决定如何处理新的写入请求。理解淘汰策略是回答“内存数据相关问题”时的加分项。4.1 过期删除策略对于设置了过期时间的 keyRedis 使用惰性删除和定期删除相结合的策略。惰性删除是在访问 key 时检查是否过期过期则删除定期删除是每隔一段时间随机抽查一批带过期时间的 key 并清理。两种方式配合既不会因为定时任务占用过多 CPU又能及时回收大部分过期数据。4.2 内存淘汰策略当内存不足时Redis 提供多种 maxmemory-policy策略含义适用场景noeviction内存满了直接返回错误不允许丢数据的场景allkeys-lru在所有 key 中淘汰最近最少使用的缓存场景最常用volatile-lru只在设置了过期时间的 key 中淘汰 LRU部分数据必须保留的场景allkeys-random在所有 key 中随机淘汰数据访问无明显热点volatile-random只在带过期时间的 key 中随机淘汰同上volatile-ttl优先淘汰剩余存活时间短的 key希望先淘汰即将过期的数据allkeys-lfu在所有 key 中淘汰访问频率最低的访问频率差异明显的缓存volatile-lfu只在带过期时间的 key 中淘汰 LFU同上LRU 关注“最近是否被访问”LFU 关注“访问频率”后者在热点分布明显时表现更好。缓存场景通常选择 allkeys-lru 或 allkeys-lfu。4.3 为什么缓存要用内存而不是磁盘面试官有时会反问“数据库也能做缓存为什么要上 Redis”除了延迟低原因还包括Redis 提供丰富的数据类型能直接完成缓存击穿、缓存穿透等场景需要的数据结构操作单线程模型让命令具备原子性减少并发问题提供过期、淘汰、发布订阅等缓存配套能力。相比之下直接把数据库当缓存往往缺少便捷的过期管理和高并发读优化。五、Redis 数据类型总览与底层数据结构Redis 并不是只有五种简单数据类型。严格来说Redis 提供了 String、Hash、List、Set、ZSet 五大数据类型以及 Bitmaps、HyperLogLog、GEO、Stream 等扩展类型。这些类型各自封装了适合场景的操作语义而它们的底层实现又会在不同数据规模下动态切换。5.1 五大数据类型速览类型特点典型场景String字符串或数字可原子增减缓存、计数、分布式锁、限流Hash字段到值的映射表对象存储、购物车、用户资料List有序可重复的双向链表消息队列、时间线、栈与队列Set无序不可重复集合去重、交并差、抽奖、标签ZSet有序不可重复带分值排行榜、延迟队列、优先队列5.2 底层数据结构理解底层结构能帮助你在面试中解释“为什么 List 头尾快中间慢”“为什么小 Hash 省内存”等问题。Redis 主要使用以下结构SDS 简单动态字符串带长度字段的字符串避免 C 字符串常见的缓冲区溢出和 O(n) 取长度问题同时支持二进制安全。双向链表支持头尾快速插入删除但节点分散内存不连续。压缩列表 ziplist一段连续内存紧凑存储多个小元素节省内存但插入删除可能触发连锁更新。快表 quicklist由多个 ziplist 通过链表串联兼顾内存紧凑和操作性能List 在较新版本中的实现。哈希表高效的键值查找Hash、Set 等结构的底层实现之一。整数集合 intset只存整数的小集合内存紧凑。跳表 skiplist多层链表结构支持平均 O(logN) 的查找是 ZSet 的底层实现之一。listpackRedis 7.0 引入逐步替代 ziplist解决连锁更新问题。Redis 通过对象的 type 和 encoding 两个维度描述一个 key 的类型与底层编码。同一个业务类型在不同数据规模下encoding 可能完全不同。这种设计让 Redis 在面对小数据时极致省内存在大数据时依然保持高效。六、String最简单也最常用6.1 String 是什么String 是 Redis 最基础的类型一个 key 对应一个 value。value 最大可到 512MB既可以存普通文本也可以存数字、序列化后的 JSON、甚至二进制数据。虽然叫 String但它底层使用 SDS并且内部会根据内容选择 int、embstr、raw 三种编码。intvalue 是整数且能表示为 long 时使用内存只存数字。embstr短字符串Redis 会把字符串对象和 SDS 分配在一块连续内存中减少内存碎片和分配次数。raw较长字符串对象和 SDS 分开分配。6.2 常用命令SET user:1 zhangsan GET user:1 SET count 100 INCR count INCRBY count 5 DECR count SETEX token:abc 300 value SETNX lock:order:1 uuid-123 MSET k1 v1 k2 v2 MGET k1 k2 APPEND user:1 -vip STRLEN user:1其中 INCR、DECR 是原子操作这是 String 在计数场景中被广泛使用的重要原因。SETNX 常用于实现分布式锁SETEX 常用于缓存带过期时间的数据。6.3 使用场景缓存对象把数据库查询结果序列化为 JSON 存成 String并设置过期时间是最经典的缓存用法。计数统计文章阅读量、点赞数、库存数量等依靠 INCR 原子增减避免并发下的读写不一致。分布式锁用 SET key value NX EX time 的原子语义防止多个节点同时进入临界区。限流用户某个时间段内的请求次数用 INCR 加过期时间实现简单窗口限流。存储 session 或 token配合过期时间实现登录态管理。6.4 代码示例用 String 做文章点赞计数import redis.clients.jedis.Jedis; public class LikeCounter { private final Jedis jedis; public LikeCounter(Jedis jedis) { this.jedis jedis; } public long like(String articleId) { String key article:like: articleId; return jedis.incr(key); } public long unlike(String articleId) { String key article:like: articleId; return jedis.decr(key); } public long getLikeCount(String articleId) { String value jedis.get(article:like: articleId); return value null ? 0L : Long.parseLong(value); } public static void main(String[] args) { try (Jedis jedis new Jedis(127.0.0.1, 6379)) { LikeCounter counter new LikeCounter(jedis); String articleId 10001; counter.like(articleId); counter.like(articleId); System.out.println(点赞数 counter.getLikeCount(articleId)); } } }这段代码把点赞数保存在article:like:10001这个 key 中通过INCR和DECR保证点赞操作的原子性。即使同一时刻有多个请求同时点赞计数也不会出现“读旧值再写新值”导致的少加问题。七、Hash对象的天然容器Hash 适合表示一个对象的多个字段例如用户资料、商品信息、购物车等。一个 Hash key 下可以保存多个 field-value 对读写单个字段时不会影响其他字段。7.1 常用命令HSET user:1001 name zhangsan age 28 city Beijing HGET user:1001 name HMSET user:1001 name lisi age 30 HMGET user:1001 name age HGETALL user:1001 HDEL user:1001 city HINCRBY user:1001 age 1 HEXISTS user:1001 name HLEN user:10017.2 使用场景对象缓存把数据库中的一行记录映射为 Hash 的 field-value按需读取字段比把整个 JSON 存成 String 更灵活。购物车用户 id 作为 key商品 id 作为 field数量作为 value通过HINCRBY增减商品数量。计数器分组例如统计多个页面的访问量可以用一个 Hash 集中管理多个 field而不是为每个页面单独建 key。7.3 代码示例用 Hash 存用户资料import redis.clients.jedis.Jedis; import java.util.Map; public class UserProfileStore { private final Jedis jedis; public UserProfileStore(Jedis jedis) { this.jedis jedis; } public void saveUser(String userId, String name, int age, String city) { String key user: userId; jedis.hset(key, name, name); jedis.hset(key, age, String.valueOf(age)); jedis.hset(key, city, city); } public MapString, String getUser(String userId) { return jedis.hgetAll(user: userId); } public void increaseAge(String userId) { jedis.hincrBy(user: userId, age, 1); } }八、List队列、栈与时间线List 是一个有序、可重复的双向链表支持从左侧或右侧快速插入和弹出元素。它常被用来实现队列、栈和时间线。8.1 常用命令LPUSH timeline:user:1001 post:1 RPUSH timeline:user:1001 post:2 LPOP timeline:user:1001 RPOP timeline:user:1001 LRANGE timeline:user:1001 0 -1 LLEN timeline:user:1001 LINDEX timeline:user:1001 0 LTRIM timeline:user:1001 0 99 BLPOP queue:task 08.2 使用场景消息队列生产者用LPUSH写入消息消费者用BRPOP阻塞读取实现简单的任务队列。最新列表把最新动态从左侧写入再用LTRIM控制长度保留最近 N 条记录。栈结构用LPUSH配合LPOP实现后进先出。8.3 使用注意List 在头尾插入删除是 O(1)但通过下标访问中间元素是 O(n)。因此它适合做队列不适合做需要频繁随机访问的列表。消息可靠性要求高时还应结合 Stream 或专业 MQ 使用。九、Set去重、标签与集合运算Set 是无序、不可重复的字符串集合。它最重要的能力是去重以及对多个集合做交集、并集、差集运算。9.1 常用命令SADD article:tag:java 10001 SADD article:tag:redis 10001 SMEMBERS article:tag:java SISMEMBER article:tag:java 10001 SCARD article:tag:java SREM article:tag:java 10001 SINTER article:tag:java article:tag:redis SUNION article:tag:java article:tag:redis SDIFF article:tag:java article:tag:redis SPOP article:tag:java9.2 使用场景去重记录访问过某个页面的用户集合天然避免重复。共同好友两个用户的好友集合做SINTER得到共同好友。标签系统每个标签对应一个文章集合通过集合运算组合筛选。抽奖SPOP随机弹出元素适合不重复抽奖SRANDMEMBER可随机返回元素但不删除。9.3 代码示例标签交集筛选import redis.clients.jedis.Jedis; import java.util.Set; public class ArticleTagMatcher { private final Jedis jedis; public ArticleTagMatcher(Jedis jedis) { this.jedis jedis; } public void tag(String articleId, String... tags) { for (String tag : tags) { jedis.sadd(tag: tag, articleId); } } public SetString findByTags(String... tags) { String[] keys new String[tags.length]; for (int i 0; i tags.length; i) { keys[i] tag: tags[i]; } return jedis.sinter(keys); } }十、ZSet排行榜与延迟队列ZSet 在 Set 的基础上给每个元素增加了一个分值 score元素按 score 有序排列。它是有序集合适合需要排序的场景。10.1 常用命令ZADD rank 98 user:1001 ZADD rank 95 user:1002 ZADD rank 102 user:1003 ZSCORE rank user:1001 ZRANGE rank 0 -1 ZREVRANGE rank 0 2 WITHSCORES ZRANK rank user:1001 ZINCRBY rank 5 user:1001 ZREM rank user:1002 ZCARD rank10.2 使用场景排行榜按分数从高到低展示用户排名用ZREVRANGE获取 Top N。延迟队列把任务的执行时间戳作为 score消费者按 score 顺序取出到期任务。优先级队列score 表示优先级按顺序处理更高优先级的任务。滑动窗口限流把请求时间戳存入 ZSet按时间窗口统计请求数量。10.3 代码示例文章热榜 Top Nimport redis.clients.jedis.Jedis; import redis.clients.jedis.resps.Tuple; import java.util.List; public class HotArticleRank { private final Jedis jedis; public HotArticleRank(Jedis jedis) { this.jedis jedis; } public void addScore(String articleId, double score) { jedis.zincrby(hot:article, score, articleId); } public ListTuple topN(int n) { return jedis.zrevrangeWithScores(hot:article, 0, n - 1); } }十一、总结把面试答案组织成三层结构回顾整篇文章回答 Redis 相关问题时可以始终围绕一个清晰的主线基础层先答清“是什么”。例如 Redis 是内存数据库数据默认在内存之后通过持久化落盘。原理层再解释“为什么”。为什么快为什么内存有限为什么需要 RDB、AOF、淘汰策略。工程层最后落到“怎么用”。不同数据类型在缓存、计数、队列、排行榜、去重等场景下如何选择。面试时与其机械背诵零散八股不如先给出结构化结论再用具体机制和场景展开。遇到追问时主动补上代价、异常和边界往往更能体现深度。
返回列表