面试知识库

08 - 关注与社交#

知识图谱#

                  关注系统
        ┌──────────┼──────────┐
        ▼          ▼          ▼
     关注/取关   共同关注    Feed扇出
     (双写)    (Set交集)   (发布推送)
        │          │          │
        ▼          ▼          ▼
    DB tb_follow  SINTER    ZADD feed:
    + Redis Set   O(N+M)   {粉丝id}
plaintext

核心代码走读#

关注/取关(双写一致)#

文件: src/main/java/com/hmdp/service/impl/FollowServiceImpl.java

共同关注(Set 交集)#

Feed 扇出(与关注联动)#

文件: src/main/java/com/hmdp/service/impl/BlogServiceImpl.java

数据模型#

tb_follow 表:
┌────┬────────┬──────────────┬────────────┐
│ id │ userId │ followUserId │ createTime │
├────┼────────┼──────────────┼────────────┤
│ 1  │ 1001   │ 1002         │ 2024-03-15 │  ← 用户1001关注了1002
│ 2  │ 1001   │ 1003         │ 2024-03-15 │  ← 用户1001关注了1003
│ 3  │ 1002   │ 1003         │ 2024-03-16 │  ← 用户1002关注了1003
└────┴────────┴──────────────┴────────────┘

Redis Set:
follows:1001 = {1002, 1003}
follows:1002 = {1003}

SINTER follows:1001 follows:1002 = {1003}  ← 共同关注
plaintext

面试 Q&A#

Q1: 关注数据为什么要双写 DB 和 Redis?#

DB 是数据源,Redis 是加速层,各有职责:

存储职责操作
MySQL持久化存储,保证数据不丢关注列表查询、粉丝列表查询
Redis Set支持 SINTER 求交集共同关注计算

如果只用 MySQL,共同关注需要 SQL JOIN:

SELECT f1.follow_user_id
FROM tb_follow f1 JOIN tb_follow f2
ON f1.follow_user_id = f2.follow_user_id
WHERE f1.user_id = ? AND f2.user_id = ?
sql

在大表上这个 JOIN 很慢。Redis Set 的 SINTER 是内存操作,O(min(N,M)),毫秒级返回。

追问:双写的一致性怎么保证?#

当前实现的策略是:先写 DB,成功后再写 Redis

不一致场景:

  1. DB 写成功,Redis 写失败 → DB 有关注记录但 Redis 没有 → 共同关注会漏
  2. 这种不一致是暂时的,可以通过定时任务全量同步修复

更好的方案:

  1. 使用 @Transactional + Redis 事务(但 Redis 事务不支持回滚)
  2. Canal 监听 MySQL binlog 异步同步到 Redis
  3. 发消息到 MQ,异步更新 Redis(最终一致)

Q2: SINTER 的性能如何?大规模场景下怎么优化?#

SINTER 的复杂度是 O(N×M),其中 N 和 M 是两个 Set 的大小(Redis 文档标注的最坏情况,实际优化后接近 O(min(N,M)))。

当两个用户各关注 10 万人时,SINTER 可能需要几十毫秒,且阻塞 Redis 主线程

优化方案:

  1. 限制 Set 大小:只存最近 N 个关注(LRU淘汰)
  2. 预计算:对热门用户对预计算共同关注,存入缓存
  3. 使用 SINTERSTORE:将结果存入新 key,设置 TTL 缓存
  4. 迁移到 Bitmap:如果用户 ID 是连续整数,Bitmap 的 BITOP AND 更快

Q3: Feed 推送是同步还是异步?有什么问题?#

当前是同步的BlogServiceImpl.saveBlog() 中 for 循环)。

问题:

  1. 阻塞发布接口:如果有 1 万粉丝,1 万次 ZADD 可能需要 100ms+
  2. 部分失败无处理:如果中间某个 ZADD 失败,已推送的无法回滚
  3. 无限扩展:粉丝越多,发布越慢

改进方案:

  1. 发布时只写 DB + 发 Kafka 消息
  2. 消费者分批推送(如每批 1000 个粉丝)
  3. 大 V 走拉模式(粉丝主动拉取,不推送)

追问:取关后 Feed 里还有 ta 的内容怎么办?#

当前实现不处理。取关后 feed:{userId} ZSet 中仍然保留了之前推送的博客 ID。

解决方案:

  1. 惰性清理:读取 Feed 时检查作者是否仍被关注,过滤掉已取关的
  2. 即时清理:取关时 ZREM 该作者的所有博客(但需要知道哪些博客是 ta 的)
  3. 不处理:大多数社交产品的做法——取关后时间线不变,未来不再推送新内容

Q4: isFollow 方法为什么查 DB 而不查 Redis Set?#

public Result isFollow(Long followUserId) {
    Long userId = UserHolder.getUser().getId();
    Integer count = query()
            .eq("user_id", userId)
            .eq("follow_user_id", followUserId)
            .count();
    return Result.ok(count > 0);
}
java

这是一个可以优化的点。Redis Set 的 SISMEMBER 是 O(1) 操作:

Boolean isMember = stringRedisTemplate.opsForSet()
        .isMember("follows:" + userId, followUserId.toString());
java

比 MySQL 的 COUNT 查询快得多。但前提是 Redis Set 与 DB 保持一致。


踩坑点#

踩坑点说明面试官考察意图
双写不一致DB 成功 Redis 失败,或反之分布式一致性
同步推送阻塞发布接口耗时随粉丝数线性增长性能设计
isFollow 查 DB有 Redis Set 但不用缓存利用
取关不清理 Feed旧博客仍在 Feed 中数据清理策略
Redis Set 无 TTLfollows:{userId} 永不过期内存管理

加分回答#

  • 对比 Twitter 的关注系统:将关注关系存入 FlockDB(图数据库),专门优化社交图谱查询
  • 分析 SINTER vs SUNION vs SDIFF 的使用场景(共同关注/全部关注/你关注但ta没关注的)
  • 提到可以用 Redis Cluster 的 hash tag {userId} 保证同一用户的 follows Set 在同一节点
  • 讨论”关注推荐”算法:基于共同关注数做推荐(SINTER 结果越多,推荐权重越高)

关联文档#