前言

感觉自己实习期间的工作没有很好地和现在大厂的技术栈结合起来

所以让 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 存储的数据安全
    1. 对于 SessionId 的安全:
      • 首先要强制开启 Https 防止 SessionId 被劫持;
      • 其次对于 Cookie 我们应该做如下配置:
        • Httponly:防止 JS 用 document.cookit 访问 cookie;
        • Secure:仅在 Https 连接时发送 Cookie;
        • SameSite=Strict/Lax:提供 CSRF 防护,Strict 最安全,但可能影响跨站跳转的用户体验;Lax 则是较好的平衡。
    2. 对于 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,客户端还需重新订阅关系。

  • 那么 RedisPub/Sub 就不能用来作为消息队列,只能用来实时消息广播。在我们的业务场景中,Pub/Sub 通常作为无需持久化的实时消息推送,如用户加入房间通知、实时弹幕、协作文档光标位置等。

Q3. 如何保证大量状态更新时的性能和可靠性

A3. 分架构和 Redis 优化两个层面来回答:

  • 首先是架构层面

    1. 使用 Redis 集群架构,通过将不同直播间数据分片到不同的物理节点上,分散单点直播间巨大压力,避免单点瓶颈
    2. 通过读写分离分散 master 读密集操作压力。
    3. 最后是将状态存储与消息广播型集群功能分离,使用专门实例负责 Pub/Sub,配置高带宽,避免广播流量挤占状态存储 CPU/网络资源。
  • 其次是 Redis 优化,对于内存,合理控制 Key 大小,控制 String Value 大小;对于持久化,结合 RDB + AOF 保障需要持久化的数据,对于无需持久化数据则设置 save "" 禁用持久化。


Q4. 是否遇到消息积压问题?

A4. 其实这个问题本质就是因为 Pub/Sub 本身不持久化消息,但发布的消息会暂存在每个订阅者的输出缓冲区中。

积压触发条件有:

  • 消费者处理能力不足 (慢消费者):客户端(如 WebSocket 服务实例)消费消息的速度跟不上发布速度。

  • 网络延迟/抖动:消费者与 Redis 节点之间的网络不稳定。

  • 消费者宕机/重启:断开期间的消息会直接丢失(无积压,但需重建状态)。

这三种情况都非常常见,我们的解决办法一般是使用 Grafana 监测 Redis 实时指标,根据预警提前扩容 Redis 和消费者集群。