面试模拟-Redis
前言
感觉自己实习期间的工作没有很好地和现在大厂的技术栈结合起来
所以让 DeepSeek 融合了一个黑马点评进去
针对这份简历,同时让它问了几个面试问题,比较有深度,记录下来
内容
面试官: 很好,你提到项目中大量使用了 Redis
进行优化。我们深入聊聊 Redis
这块:
Session 管理优化
Q1.你具体是如何设计 Redis
存储 Session
的? 为什么选择这种数据结构(比如 String vs Hash)?
A1. 我在项目中主要是使用的 Hash
结构,如: (userId, userName, role, createdTime, lastAccessedTime, currentDecice...)
因为我们的项目中有需要频繁增删这种无需持久化的数据。对于我们的 SessionKey 命名,是 session:{sessionId},这里的 sessionId 是用 UUid 生成的随机 32 位 ID。对于过期时间,我们设置的是 3600 秒,也就是 1 小时;每次更新我们首先会去更新 lastAccessedTime,然后重新 Expire 一下,也是 3600 秒。
对于
Hash
结构,它可以用HINCRBY(修改数字) HSET(修改字段)
等命令来源地修改单个字段,而无需读取整个对象。而
String
结构我们还需要首先读取整个对象,然后反序列化,修改,再序列化,最后写回这一系列操作。采用
Hash
结构而非String
结构能够大大地减少网络传输量和服务器负载
Q2.需要考虑哪些安全性或过期问题?
A2. 我分成过期策略和安全策略两方面回答:
对于过期策略:在常见的过期场景下有一些问题,比如用户故意保持活跃,或者登陆后使用
Session
固定攻击。- 那么对于永不过期的活跃用户,我们应设置绝对过期时间:即通过
createTime
和当前时间进行比对,如果超过 3 天就要求重新登录。 - 对于一些敏感操作,我们应当要求重新登录,如修改密码,也就是定期安全验证。
- 与此同时也要监控
Session
,定期扫描存活时间,超过阈值则清理。
通过这些手段能够有效控制
SessionId
不轮换导致的固定攻击。- 那么对于永不过期的活跃用户,我们应设置绝对过期时间:即通过
- 对于安全策略:分为 SessionId 的安全和 Session 存储的数据安全
- 对于
SessionId
的安全:- 首先要强制开启 Https 防止 SessionId 被劫持;
- 其次对于
Cookie
我们应该做如下配置:Httponly
:防止 JS 用 document.cookit 访问 cookie;Secure
:仅在 Https 连接时发送 Cookie;SameSite=Strict/Lax
:提供CSRF 防护
,Strict 最安全,但可能影响跨站跳转的用户体验;Lax 则是较好的平衡。
- 对于
Session
数据安全性:- 首先一定不能存储非常敏感的信息,比如卡号、密码等
- 对于 userId、role 等敏感信息需要分两方面考虑
- 设置网络隔离:redis 部署在内网,只能够内网访问到它,不开放公网接口
- 设置访问控制:
- 设置强密码即设置
requirePass
,阻止任意客户端都能连接Redis
的情况,访问需要携带Auth password
- 设置 IP 白名单,在防火墙、安全组配置可访问 Redis 端口的源 IP 地址,最好 redis 默认端口也要更换
- Redis 6+ 引入了
ACL
,它可以为不同的应用或服务创建独立用户,用户的命令权限、访问权限、读写权限都可以更加细粒度的控制。
- 设置强密码即设置
- 对于
热点数据缓存
你缓存了学习资源元数据和用户状态。
Q1. 当这些数据在后台被更新时(比如老师修改了资源信息),如何保证缓存与数据库的一致性? 你采用了什么策略?(旁路缓存?读写穿透?)
A1. 我们使用的是旁路缓存策略来保证缓存与数据库一致性:
- 在读数据时,cache 命中就返回,未命中就从 db 中读取返回,随后将数据写入 cache 中
- 在写数据时先更新 db,再删除 cache
于此同时,我们使用 RocketMQ
来存储 DB 更新成功后,异步提交的删除 Cache 操作消息,由独立的消费者监听并执行删除 Cache 操作,于此同时保障消息幂等性,以此应对删除 Cache 可能失败的情况。
Q2. 为什么是 “更新 DB + 删除 Cache” 而不是 “更新 DB + 更新 Cache”?
A2. 如果有两个请求都需要更新同一片数据,如果不施加分布式锁,可能出现更新顺序出错的问题。
- 对于 先更新 db,再删除 cache 这种方法,首先是简单,其次是能够保障最终一致性
- 当然它牺牲了读数据的性能,因为直接删除 Cache 会造成 Cache miss,增加了 db 的负担。
Q3. 如何应对 “先删 Cache,后更新 DB” 的失效问题?
A3. 一般使用延迟双删的解决方案,即删除缓存,然后修改数据库,然后延时一段时间(500ms)再删缓存。
Q4. Write DB -> Delete Cache
就万无一失吗?
A5. 当然不是,读写并发时可能出现读数据在 Cache miss 情况下获取数据后阻塞,写数据完成 db 更新后,读数据重建 Cache 导致不一致的问题。
Q5. 删除 Cache 失败怎么办? 如何保证删除操作最终成功?在你们的系统中,谁负责执行 “更新 DB 后删除 Cache” 的操作?应用服务在同一个事务/线程里同步执行吗?有什么风险?更好的做法?
A5. 我们使用 RocketMQ
来存储 DB 更新成功后,异步提交的删除 Cache 操作消息,由独立的消费者监听并执行删除 Cache 操作,于此同时保障消息幂等性,以此应对删除 Cache 可能失败的情况。
- 如果放在同一个事务中执行,会增加请求延迟,如果 Cache 响应慢或者失效,会阻塞线程和引发事务回滚。
Q6. 缓存预热与击穿: 对于极热的热点数据 (如爆款课程元数据),在 Cache 被删除或失效瞬间,大量请求可能同时穿透到 DB。你们是如何预防的?
A6. 对于缓存击穿问题,我们采用了互斥锁+逻辑过期+逻辑删除的策略,即通过获取 Redis SETNX 的方式,只允许单个线程重建缓存,其余未能获取到锁的线程则返回过期数据。
- 对于逻辑过期,即在 value 中添加一个 expire 字段,在读数据时检查是否过期;
- 对于逻辑删除,我们使用了
Set
数据结构,将需要被删除的数据放入Set
中,读数据的时候检查是否被删除。
布隆过滤器
你提到用 Redission 的布隆过滤器缓解缓存穿透。
Q1. 布隆过滤器的原理是什么?它的优缺点是什么?(尤其要关注误判率和不可删除)
A1. 布隆过滤器是用来缓解缓存穿透问题的工具,通过使用多个 hash 函数对 id 进行计算,以及使用 bitmap 记录计算值的 01 分布。如果某个 id 通过 hash 计算得到多个值中的某个在 bitmap 中为 0,则这个 id 就不存在。
- 布隆过滤器很大的优点就是内存占用较少,仅需维护 bitmap,不像缓存空数据一样需要记录冗余 Key。
- 当然布隆过滤器也有缺点:实现起来比较复杂;存在误判问题,在计算 hash 时,不同的 id 也有可能映射到相同的多个 hash 值中。这一点是可控的,即将 bitmap 容量提高,那么误判率就会下降;以及它的不能扩容和不可删除特性,存入了 id 映射后就不能删除,因为不同元素的哈希位置可能重叠:删除元素 A 可能导致元素 B 被误判为不存在。
Q2. 布隆过滤器原理的数学本质?
A2. 误判率公式, p = (1 - e ^ (-kn/m)) ^ k
n = 元素数量,m = bit 数组长度,k = 哈希函数个数
优化方向:m / n (每个元素分配 bit 个数) 增大时,p 指数级下降
最优:k = (m/n) ln2
Q3. 在你的场景里,缓存穿透具体是指什么请求导致的?键是如何构造的?
A3. 一个比较常见的例子是是用户在直播结束之后没有退出,刷新已结束的直播页面、在已结束的直播间中做各种操作等。像这种请求都会携带直播 ID,许多基于该 ID 缓存的状态变量在直播结束后就会从缓存中删除,从而造成了缓存穿透。再一个就是请求不存在的 id,这种用缓存空值方法就能比较容易解决。
- 我的键命名方法是
业务名 + 数据名 + id
,比如刚才说到的直播 ID,就是 live:room:uuid,这个 UUID 对应 sql 数据库直播表中存储的 id
Q4. 为什么选择 Redission
而非 Guava
?初始化参数?过滤器更新策略?是如何集成到缓存流程中的?
A4. Guava
的实现是位于服务器本地的单机布隆过滤器,集群环境下需要独立维护。而 Redission
是基于 Redis
实现的,整个集群共享同一过滤器。由于项目已经是分布式架构了,所以我们选择了 Redission
这种全局一致防护的实现方式。
对于 Redission 布隆过滤器初始化,它需要设定
预期元素数量
和p 误判率
,对于预期元素数量,我们设定为了历史直播峰值房间数 * 5,通过权衡内存压力与 DB 压力,我们误判率选择 1%。对于过滤器更新策略,新增直播时添加到过滤器中,结束直播时就不删除过滤器中的内容,因为有之前提到的逻辑删除保底;当然这样做会导致旧的 key 越来越多,我们就需要定期重建过滤器。我们每三天会用有效
roomID
全量重建。
实时状态同步
Q1. 你使用了 Redis 的哪些数据结构和特性来实现?
A1. 基于已有业务,我们使用了 String
计数直播点赞数;使用了 Hash
结构来实现直播间中需要实时更新的各种数据;如直播状态、直播间人数等;使用了 Sorted Set
实现房间总人数、举手、直播间发消息功能。使用了 Pub/Sub
来实现无需持久化的实时消息广播,如加入直播间消息、实时弹幕(直播消息)、写作文档光标位置。
Q2. Pub/Sub
在这里扮演什么角色?
A2. 考虑 Pub/Sub 局限性
:
由于
Redis
是基于内存的,而且Pub/Sub
也是保存到了内存的字典数据结构中,所以发布的消息不会被持久化。一旦客户机下线或者重新上线,上线前已经发布过的消息是不会被订阅到的。其次,
Redis
客户端对象相应缓存有限制,一旦超过限制就会强制关闭 clinet,客户端还需重新订阅关系。- 那么
Redis
的Pub/Sub
就不能用来作为消息队列,只能用来实时消息广播。在我们的业务场景中,Pub/Sub 通常作为无需持久化的实时消息推送,如用户加入房间通知、实时弹幕、协作文档光标位置等。
Q3. 如何保证大量状态更新时的性能和可靠性?
A3. 分架构和 Redis 优化两个层面来回答:
首先是架构层面,
- 使用 Redis 集群架构,通过将不同直播间数据分片到不同的物理节点上,分散单点直播间巨大压力,避免单点瓶颈。
- 通过读写分离分散 master 读密集操作压力。
- 最后是将状态存储与消息广播型集群功能分离,使用专门实例负责 Pub/Sub,配置高带宽,避免广播流量挤占状态存储 CPU/网络资源。
其次是 Redis 优化,对于内存,合理控制 Key 大小,控制 String Value 大小;对于持久化,结合 RDB + AOF 保障需要持久化的数据,对于无需持久化数据则设置
save ""
禁用持久化。
Q4. 是否遇到消息积压问题?
A4. 其实这个问题本质就是因为 Pub/Sub 本身不持久化消息,但发布的消息会暂存在每个订阅者的输出缓冲区中。
积压触发条件有:
消费者处理能力不足 (慢消费者):客户端(如 WebSocket 服务实例)消费消息的速度跟不上发布速度。
网络延迟/抖动:消费者与 Redis 节点之间的网络不稳定。
消费者宕机/重启:断开期间的消息会直接丢失(无积压,但需重建状态)。
这三种情况都非常常见,我们的解决办法一般是使用 Grafana 监测 Redis 实时指标,根据预警提前扩容 Redis 和消费者集群。