👥 设计一个亿级用户的社交关系链存储:朋友圈的秘密!

24 阅读15分钟

📖 开场:你的朋友圈

想象你的微信朋友圈 📱:

简单场景

你有100个好友
    ↓
发一条朋友圈
    ↓
100个好友能看到 ✅

简单!存在数据库就行 ✅

亿级用户场景

微信:10亿用户
平均每人:200个好友
总关系数:10亿 × 200 = 2000亿条关系 😱

明星账号:
- 粉丝:5000万
- 发一条动态 → 5000万人看到
- 读放大问题!💥

如何存储?如何查询?如何推送?🤔

这就是亿级用户社交关系链的挑战!


🤔 核心问题

三大挑战

挑战说明难度
海量数据数千亿条关系⭐⭐⭐
读写热点明星用户的粉丝⭐⭐⭐
实时性关注/取关立即生效⭐⭐

业务需求

关注关系

A关注B:
- A能看到B的动态
- B的粉丝数+1
- A的关注数+1

A取关B:
- A看不到B的动态
- B的粉丝数-1
- A的关注数-1

关系查询

1. 我关注了谁?(关注列表)
2. 谁关注了我?(粉丝列表)
3. 我和Ta是好友吗?(互相关注)
4. 我和Ta的共同好友?(关注交集)

Feed流

我的首页动态:
- 显示我关注的人的最新动态
- 按时间倒序
- 分页加载

🎯 数据模型设计

方案1:关系表(MySQL)🗄️

表设计

-- ⭐ 关注关系表
CREATE TABLE user_follow (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    user_id BIGINT NOT NULL COMMENT '用户ID',
    follow_id BIGINT NOT NULL COMMENT '关注的用户ID',
    status TINYINT NOT NULL DEFAULT 1 COMMENT '状态:1-关注,0-取关',
    create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    
    -- ⭐ 索引
    INDEX idx_user_id (user_id),           -- 查询"我关注了谁"
    INDEX idx_follow_id (follow_id),       -- 查询"谁关注了我"
    UNIQUE KEY uk_user_follow (user_id, follow_id)  -- 防止重复关注
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- ⭐ 用户统计表
CREATE TABLE user_stats (
    user_id BIGINT PRIMARY KEY,
    follow_count INT NOT NULL DEFAULT 0 COMMENT '关注数',
    fans_count INT NOT NULL DEFAULT 0 COMMENT '粉丝数',
    update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

代码实现

@Service
@Slf4j
public class FollowService {
    
    @Autowired
    private UserFollowDao followDao;
    
    @Autowired
    private UserStatsDao statsDao;
    
    /**
     * ⭐ 关注用户
     */
    @Transactional
    public boolean follow(Long userId, Long followId) {
        // 参数校验
        if (userId.equals(followId)) {
            throw new IllegalArgumentException("不能关注自己");
        }
        
        // 检查是否已关注
        UserFollow exist = followDao.findByUserIdAndFollowId(userId, followId);
        if (exist != null && exist.getStatus() == 1) {
            log.warn("已经关注过了: userId={}, followId={}", userId, followId);
            return false;
        }
        
        if (exist != null) {
            // 之前取关过,现在重新关注
            exist.setStatus(1);
            followDao.update(exist);
        } else {
            // 新关注
            UserFollow follow = new UserFollow();
            follow.setUserId(userId);
            follow.setFollowId(followId);
            follow.setStatus(1);
            followDao.insert(follow);
        }
        
        // ⭐ 更新统计数据
        // userId的关注数+1
        statsDao.incrementFollowCount(userId, 1);
        // followId的粉丝数+1
        statsDao.incrementFansCount(followId, 1);
        
        log.info("关注成功: userId={}, followId={}", userId, followId);
        return true;
    }
    
    /**
     * ⭐ 取关用户
     */
    @Transactional
    public boolean unfollow(Long userId, Long followId) {
        // 查询关注记录
        UserFollow follow = followDao.findByUserIdAndFollowId(userId, followId);
        if (follow == null || follow.getStatus() == 0) {
            log.warn("未关注该用户: userId={}, followId={}", userId, followId);
            return false;
        }
        
        // 更新状态为取关
        follow.setStatus(0);
        followDao.update(follow);
        
        // ⭐ 更新统计数据
        // userId的关注数-1
        statsDao.incrementFollowCount(userId, -1);
        // followId的粉丝数-1
        statsDao.incrementFansCount(followId, -1);
        
        log.info("取关成功: userId={}, followId={}", userId, followId);
        return true;
    }
    
    /**
     * ⭐ 查询关注列表(我关注了谁)
     */
    public List<UserFollow> getFollowList(Long userId, int page, int size) {
        int offset = (page - 1) * size;
        return followDao.findByUserId(userId, offset, size);
    }
    
    /**
     * ⭐ 查询粉丝列表(谁关注了我)
     */
    public List<UserFollow> getFansList(Long userId, int page, int size) {
        int offset = (page - 1) * size;
        return followDao.findByFollowId(userId, offset, size);
    }
    
    /**
     * ⭐ 判断是否关注
     */
    public boolean isFollow(Long userId, Long followId) {
        UserFollow follow = followDao.findByUserIdAndFollowId(userId, followId);
        return follow != null && follow.getStatus() == 1;
    }
    
    /**
     * ⭐ 判断是否互相关注(好友)
     */
    public boolean isFriend(Long userId, Long otherId) {
        boolean aFollowB = isFollow(userId, otherId);
        boolean bFollowA = isFollow(otherId, userId);
        return aFollowB && bFollowA;
    }
}

优缺点

优点 ✅:

  • 实现简单
  • 支持事务(ACID)
  • 数据一致性好

缺点 ❌:

  • 数据量大(千亿级)→ 单表无法存储
  • 查询慢(分页、排序)
  • 扩展性差(单机MySQL)

优化方向:分库分表


方案2:Redis存储 🔴

数据结构设计

使用Redis的Set结构

关注列表(我关注了谁):
key: follow:{userId}
value: Set<followId>

例如:
follow:1001  {2001, 2002, 2003}  (用户1001关注了2001、2002、2003)

粉丝列表(谁关注了我):
key: fans:{userId}
value: Set<userId>

例如:
fans:2001  {1001, 1002, 1003}  (用户2001的粉丝是1001、1002、1003)

代码实现

@Service
@Slf4j
public class RedisFollowService {
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    private static final String FOLLOW_PREFIX = "follow:";  // 关注列表
    private static final String FANS_PREFIX = "fans:";      // 粉丝列表
    
    /**
     * ⭐ 关注用户
     */
    public boolean follow(Long userId, Long followId) {
        if (userId.equals(followId)) {
            throw new IllegalArgumentException("不能关注自己");
        }
        
        String followKey = FOLLOW_PREFIX + userId;
        String fansKey = FANS_PREFIX + followId;
        
        // ⭐ 添加到关注列表
        redisTemplate.opsForSet().add(followKey, followId.toString());
        
        // ⭐ 添加到粉丝列表
        redisTemplate.opsForSet().add(fansKey, userId.toString());
        
        log.info("关注成功: userId={}, followId={}", userId, followId);
        return true;
    }
    
    /**
     * ⭐ 取关用户
     */
    public boolean unfollow(Long userId, Long followId) {
        String followKey = FOLLOW_PREFIX + userId;
        String fansKey = FANS_PREFIX + followId;
        
        // ⭐ 从关注列表移除
        redisTemplate.opsForSet().remove(followKey, followId.toString());
        
        // ⭐ 从粉丝列表移除
        redisTemplate.opsForSet().remove(fansKey, userId.toString());
        
        log.info("取关成功: userId={}, followId={}", userId, followId);
        return true;
    }
    
    /**
     * ⭐ 查询关注列表
     */
    public Set<String> getFollowList(Long userId) {
        String key = FOLLOW_PREFIX + userId;
        return redisTemplate.opsForSet().members(key);
    }
    
    /**
     * ⭐ 查询粉丝列表
     */
    public Set<String> getFansList(Long userId) {
        String key = FANS_PREFIX + userId;
        return redisTemplate.opsForSet().members(key);
    }
    
    /**
     * ⭐ 判断是否关注
     */
    public boolean isFollow(Long userId, Long followId) {
        String key = FOLLOW_PREFIX + userId;
        return Boolean.TRUE.equals(
            redisTemplate.opsForSet().isMember(key, followId.toString())
        );
    }
    
    /**
     * ⭐ 获取关注数
     */
    public Long getFollowCount(Long userId) {
        String key = FOLLOW_PREFIX + userId;
        return redisTemplate.opsForSet().size(key);
    }
    
    /**
     * ⭐ 获取粉丝数
     */
    public Long getFansCount(Long userId) {
        String key = FANS_PREFIX + userId;
        return redisTemplate.opsForSet().size(key);
    }
    
    /**
     * ⭐ 获取共同关注(交集)
     */
    public Set<String> getCommonFollows(Long userId1, Long userId2) {
        String key1 = FOLLOW_PREFIX + userId1;
        String key2 = FOLLOW_PREFIX + userId2;
        
        // ⭐ Redis Set交集
        return redisTemplate.opsForSet().intersect(key1, key2);
    }
    
    /**
     * ⭐ 判断是否互相关注(好友)
     */
    public boolean isFriend(Long userId, Long otherId) {
        return isFollow(userId, otherId) && isFollow(otherId, userId);
    }
}

优缺点

优点 ✅:

  • 性能高(内存操作)
  • 支持集合运算(交集、并集、差集)
  • 查询快(O(1)判断是否关注)

缺点 ❌:

  • 内存消耗大(10亿用户 × 200关注 × 8字节 = 1.6TB)
  • 无法持久化全部数据(成本高)
  • 分页查询不友好

优化方向:Redis + MySQL混合存储


方案3:混合存储(推荐)⭐⭐⭐

架构设计

┌─────────────────────────────────────────┐
│            应用层                        │
└─────────────┬───────────────────────────┘
              │
              ↓
┌─────────────────────────────────────────┐
│         Redis(热数据)                  │
│                                         │
│  - 关注/粉丝列表(前100个)             │
│  - 关注/粉丝数统计                      │
│  - 是否关注判断                         │
│  - 过期时间:1小时                      │
└─────────────┬───────────────────────────┘
              │
              ↓ 缓存未命中
┌─────────────────────────────────────────┐
│       MySQL(全量数据)                  │
│                                         │
│  - 分库分表(按userId)                 │
│  - 存储全部关注关系                     │
│  - 冷数据查询                           │
└─────────────────────────────────────────┘

分库分表策略

分库

用户分库策略:按userId取模

数据库0:userId % 16 == 0
数据库1:userId % 16 == 1
...
数据库15:userId % 16 == 15

例如:
userId=10011001 % 16 = 9 → 数据库9
userId=20012001 % 16 = 1 → 数据库1

分表

每个库再分表:按userId取模

user_follow_0:userId % 16 == 0
user_follow_1:userId % 16 == 1
...
user_follow_15:userId % 16 == 15

总计:16个库 × 16张表 = 256张表

代码实现

@Service
@Slf4j
public class HybridFollowService {
    
    @Autowired
    private RedisFollowService redisService;
    
    @Autowired
    private ShardingFollowDao shardingDao;  // 分库分表DAO
    
    private static final int CACHE_EXPIRE_SECONDS = 3600;  // 缓存1小时
    
    /**
     * ⭐ 关注用户
     */
    @Transactional
    public boolean follow(Long userId, Long followId) {
        // 1. 写入数据库
        shardingDao.insert(userId, followId);
        
        // 2. 更新Redis缓存
        redisService.follow(userId, followId);
        
        // 3. 设置过期时间
        String followKey = "follow:" + userId;
        String fansKey = "fans:" + followId;
        redisTemplate.expire(followKey, CACHE_EXPIRE_SECONDS, TimeUnit.SECONDS);
        redisTemplate.expire(fansKey, CACHE_EXPIRE_SECONDS, TimeUnit.SECONDS);
        
        log.info("关注成功: userId={}, followId={}", userId, followId);
        return true;
    }
    
    /**
     * ⭐ 取关用户
     */
    @Transactional
    public boolean unfollow(Long userId, Long followId) {
        // 1. 更新数据库
        shardingDao.update(userId, followId, 0);  // status=0
        
        // 2. 更新Redis缓存
        redisService.unfollow(userId, followId);
        
        log.info("取关成功: userId={}, followId={}", userId, followId);
        return true;
    }
    
    /**
     * ⭐ 查询关注列表(带缓存)
     */
    public List<Long> getFollowList(Long userId, int page, int size) {
        String cacheKey = "follow:list:" + userId + ":" + page;
        
        // 1. 查询Redis缓存
        List<Long> cacheList = getFromCache(cacheKey);
        if (cacheList != null) {
            log.info("从缓存获取关注列表: userId={}, page={}", userId, page);
            return cacheList;
        }
        
        // 2. 缓存未命中,查询数据库
        int offset = (page - 1) * size;
        List<Long> dbList = shardingDao.findFollowList(userId, offset, size);
        
        // 3. 写入缓存
        setToCache(cacheKey, dbList, CACHE_EXPIRE_SECONDS);
        
        log.info("从数据库获取关注列表: userId={}, page={}, count={}", 
            userId, page, dbList.size());
        
        return dbList;
    }
    
    /**
     * ⭐ 判断是否关注(带缓存)
     */
    public boolean isFollow(Long userId, Long followId) {
        String cacheKey = "isfollow:" + userId + ":" + followId;
        
        // 1. 查询Redis缓存
        Boolean cached = getFromCache(cacheKey);
        if (cached != null) {
            return cached;
        }
        
        // 2. 查询数据库
        boolean result = shardingDao.exists(userId, followId);
        
        // 3. 写入缓存
        setToCache(cacheKey, result, CACHE_EXPIRE_SECONDS);
        
        return result;
    }
    
    private List<Long> getFromCache(String key) {
        // 从Redis获取
        return null;  // 简化示例
    }
    
    private void setToCache(String key, Object value, int expireSeconds) {
        // 写入Redis
    }
}

ShardingJDBC配置

spring:
  shardingsphere:
    datasource:
      names: ds0,ds1,ds2,ds3,ds4,ds5,ds6,ds7,ds8,ds9,ds10,ds11,ds12,ds13,ds14,ds15
      
      # ⭐ 配置16个数据源
      ds0:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://localhost:3306/social_0
        username: root
        password: password
      
      # ... ds1-ds15配置类似
    
    rules:
      sharding:
        tables:
          user_follow:
            # ⭐ 实际节点
            actual-data-nodes: ds$->{0..15}.user_follow_$->{0..15}
            
            # ⭐ 分库策略:按userId取模
            database-strategy:
              standard:
                sharding-column: user_id
                sharding-algorithm-name: db-mod
            
            # ⭐ 分表策略:按userId取模
            table-strategy:
              standard:
                sharding-column: user_id
                sharding-algorithm-name: table-mod
        
        sharding-algorithms:
          # 分库算法
          db-mod:
            type: MOD
            props:
              sharding-count: 16
          
          # 分表算法
          table-mod:
            type: MOD
            props:
              sharding-count: 16

优缺点

优点 ✅:

  • 高性能(Redis缓存)
  • 大容量(MySQL存储)
  • 可扩展(分库分表)
  • 成本可控(热数据在Redis,冷数据在MySQL)

缺点 ❌:

  • 架构复杂
  • 缓存一致性问题
  • 运维成本高

适用场景

  • 亿级用户系统 ⭐⭐⭐
  • 微博、微信等社交产品

🎯 特殊场景优化

场景1:明星用户(热点数据)🌟

问题

明星用户:粉丝5000万
    ↓
查询粉丝列表 → 数据库压力巨大

解决方案

1️⃣ 分页缓存

/**
 * 粉丝列表分页缓存
 */
public List<Long> getFansList(Long userId, int page, int size) {
    String cacheKey = "fans:page:" + userId + ":" + page;
    
    // ⭐ 缓存每一页
    List<Long> cached = redisService.getList(cacheKey);
    if (cached != null) {
        return cached;
    }
    
    // 查询数据库
    List<Long> result = shardingDao.findFansList(userId, page, size);
    
    // 缓存1小时
    redisService.setList(cacheKey, result, 3600);
    
    return result;
}

2️⃣ Bloom Filter(判断是否关注)

@Component
public class BloomFilterFollowService {
    
    private BloomFilter<Long> bloomFilter;
    
    @PostConstruct
    public void init() {
        // ⭐ 创建BloomFilter(预计5000万,误判率0.01%)
        bloomFilter = BloomFilter.create(
            Funnels.longFunnel(),
            50_000_000,  // 预计元素数
            0.0001       // 误判率
        );
    }
    
    /**
     * 判断是否关注(快速判断)
     */
    public boolean isFollow(Long userId, Long followId) {
        // ⭐ BloomFilter快速判断
        if (!bloomFilter.mightContain(followId)) {
            // 肯定不存在 ✅
            return false;
        }
        
        // 可能存在,查询数据库确认
        return shardingDao.exists(userId, followId);
    }
    
    /**
     * 关注时,添加到BloomFilter
     */
    public void follow(Long userId, Long followId) {
        // 数据库操作...
        
        // ⭐ 添加到BloomFilter
        bloomFilter.put(followId);
    }
}

场景2:共同好友 👥

需求

查询我和Ta的共同好友

我的关注:{A, B, C, D, E}
Ta的关注:{C, D, E, F, G}
共同关注:{C, D, E}

实现

/**
 * ⭐ 查询共同关注(Redis Set交集)
 */
public Set<String> getCommonFollows(Long userId1, Long userId2) {
    String key1 = "follow:" + userId1;
    String key2 = "follow:" + userId2;
    
    // ⭐ Redis SINTER命令(集合交集)
    Set<String> result = redisTemplate.opsForSet().intersect(key1, key2);
    
    log.info("共同关注数量: {}", result != null ? result.size() : 0);
    
    return result;
}

/**
 * ⭐ 可能认识的人(推荐关注)
 */
public List<Long> getRecommendFollows(Long userId) {
    // 1. 获取我的关注列表
    Set<String> myFollows = redisTemplate.opsForSet().members("follow:" + userId);
    
    // 2. 统计二度好友(好友的好友)
    Map<Long, Integer> scoreMap = new HashMap<>();
    
    for (String followId : myFollows) {
        // 获取好友的关注列表
        Set<String> friendFollows = redisTemplate.opsForSet()
            .members("follow:" + followId);
        
        for (String secondFollow : friendFollows) {
            Long secondFollowId = Long.parseLong(secondFollow);
            
            // 排除自己和已关注的人
            if (secondFollowId.equals(userId) || myFollows.contains(secondFollow)) {
                continue;
            }
            
            // 统计出现次数(共同好友越多,推荐权重越高)
            scoreMap.merge(secondFollowId, 1, Integer::sum);
        }
    }
    
    // 3. 按权重排序,返回Top10
    return scoreMap.entrySet().stream()
        .sorted(Map.Entry.<Long, Integer>comparingByValue().reversed())
        .limit(10)
        .map(Map.Entry::getKey)
        .collect(Collectors.toList());
}

场景3:Feed流(关注的人的动态)📰

推模式 vs 拉模式

推模式(写扩散)

用户A发动态 → 推送给所有粉丝的收件箱

优点:
- 读快(直接读自己的收件箱)

缺点:
- 写慢(明星用户发一条,要写5000万次)
- 存储大(每个用户一个收件箱)
/**
 * 推模式:用户发动态
 */
public void publishFeed(Long userId, Feed feed) {
    // 1. 保存动态
    feedDao.insert(feed);
    
    // 2. 获取所有粉丝
    List<Long> fansList = getFansList(userId);
    
    // 3. 推送到每个粉丝的收件箱
    for (Long fansId : fansList) {
        String inboxKey = "inbox:" + fansId;
        
        // ⭐ 添加到收件箱(Redis List)
        redisTemplate.opsForList().leftPush(inboxKey, feed.getId().toString());
        
        // 只保留最新的100条
        redisTemplate.opsForList().trim(inboxKey, 0, 99);
    }
}

/**
 * 推模式:读取我的收件箱
 */
public List<Feed> getMyInbox(Long userId, int page, int size) {
    String inboxKey = "inbox:" + userId;
    
    // ⭐ 从收件箱读取(很快)
    int start = (page - 1) * size;
    int end = start + size - 1;
    
    List<String> feedIds = redisTemplate.opsForList().range(inboxKey, start, end);
    
    // 批量查询动态详情
    return feedDao.findByIds(feedIds);
}

拉模式(读扩散)

读取首页 → 实时从关注的人那里拉取最新动态

优点:
- 写快(只写一次)

缺点:
- 读慢(要聚合多个人的动态)
/**
 * 拉模式:用户发动态
 */
public void publishFeed(Long userId, Feed feed) {
    // 只保存动态,不推送
    feedDao.insert(feed);
}

/**
 * 拉模式:读取首页(实时拉取)
 */
public List<Feed> getMyTimeline(Long userId, int page, int size) {
    // 1. 获取我关注的人
    List<Long> followList = getFollowList(userId);
    
    // 2. 从每个人那里拉取最新的10条动态
    List<Feed> allFeeds = new ArrayList<>();
    
    for (Long followId : followList) {
        List<Feed> feeds = feedDao.findByUserId(followId, 0, 10);
        allFeeds.addAll(feeds);
    }
    
    // 3. 按时间排序
    allFeeds.sort(Comparator.comparing(Feed::getCreateTime).reversed());
    
    // 4. 分页返回
    int start = (page - 1) * size;
    int end = Math.min(start + size, allFeeds.size());
    
    return allFeeds.subList(start, end);
}

推拉结合(最佳实践)⭐⭐⭐

普通用户:推模式(粉丝少,推送快)
明星用户:拉模式(粉丝多,推送慢)

判断标准:粉丝数 > 1000  拉模式
/**
 * ⭐ 推拉结合
 */
public void publishFeed(Long userId, Feed feed) {
    // 1. 保存动态
    feedDao.insert(feed);
    
    // 2. 获取粉丝数
    Long fansCount = getFansCount(userId);
    
    if (fansCount < 1000) {
        // ⭐ 粉丝少,推模式
        pushToFans(userId, feed);
    } else {
        // ⭐ 粉丝多,拉模式(不推送,读取时实时拉)
        log.info("明星用户,使用拉模式: userId={}, fansCount={}", userId, fansCount);
    }
}

/**
 * 读取首页
 */
public List<Feed> getMyTimeline(Long userId, int page, int size) {
    String inboxKey = "inbox:" + userId;
    
    // 1. 从收件箱读取(推模式的动态)
    List<String> pushFeedIds = redisTemplate.opsForList()
        .range(inboxKey, 0, 99);
    
    List<Feed> pushFeeds = feedDao.findByIds(pushFeedIds);
    
    // 2. 拉取明星用户的最新动态(拉模式)
    List<Long> starFollows = getStarFollows(userId);  // 粉丝数>1000的关注
    List<Feed> pullFeeds = new ArrayList<>();
    
    for (Long starId : starFollows) {
        List<Feed> feeds = feedDao.findByUserId(starId, 0, 10);
        pullFeeds.addAll(feeds);
    }
    
    // 3. 合并 + 排序
    List<Feed> allFeeds = new ArrayList<>();
    allFeeds.addAll(pushFeeds);
    allFeeds.addAll(pullFeeds);
    
    allFeeds.sort(Comparator.comparing(Feed::getCreateTime).reversed());
    
    // 4. 分页
    int start = (page - 1) * size;
    int end = Math.min(start + size, allFeeds.size());
    
    return allFeeds.subList(start, end);
}

📊 架构总结

        亿级社交关系链架构

┌──────────────────────────────────────┐
│           客户端                      │
└─────────────┬────────────────────────┘
              │
              ↓
┌──────────────────────────────────────┐
│         应用服务器                    │
│                                      │
│  - 关注/取关                         │
│  - 关系查询                          │
│  - Feed流                            │
└───────┬──────────────┬───────────────┘
        │              │
        ↓              ↓
┌──────────────┐  ┌──────────────────┐
│    Redis     │  │   MySQL集群      │
│              │  │                  │
│ - 热数据     │  │ - 分库分表       │
│ - Set结构    │  │ - 全量数据       │
│ - 缓存1小时  │  │ - 16库×16表    │
└──────────────┘  └──────────────────┘

🎓 面试题速答

Q1: 如何存储亿级用户的社交关系?

A: 混合存储方案

  1. Redis(热数据):

    • Set结构存储关注/粉丝列表
    • 缓存过期时间1小时
    • 支持快速查询和集合运算
  2. MySQL(全量数据):

    • 分库分表(16库×16表)
    • 按userId取模分片
    • 冷数据查询

优点:高性能 + 大容量 + 可扩展


Q2: 明星用户的粉丝如何存储?

A: 三种优化

  1. 分页缓存

    • 缓存每一页的粉丝列表
    • 避免每次都查数据库
  2. Bloom Filter

    • 快速判断是否关注
    • 减少数据库查询
  3. 读写分离

    • 写入Master
    • 查询Slave

关键:热点数据特殊处理


Q3: Feed流如何实现?推模式还是拉模式?

A: 推拉结合(最佳实践):

普通用户(粉丝<1000):推模式
- 发动态 → 推送到粉丝收件箱
- 读取快

明星用户(粉丝>1000):拉模式
- 发动态 → 只保存
- 读取时实时拉取

优点

  • 普通用户读取快
  • 明星用户写入快

Q4: 如何查询共同好友?

A: Redis Set交集

String key1 = "follow:" + userId1;
String key2 = "follow:" + userId2;

// ⭐ SINTER命令(集合交集)
Set<String> common = redisTemplate.opsForSet()
    .intersect(key1, key2);

时间复杂度:O(N),N是较小集合的大小


Q5: 如何分库分表?

A: 分片策略:按userId取模

16个数据库:userId % 16 = [0-15]
每个库16张表:userId % 16 = [0-15]

总计:256张表

例如:
userId=10011001 % 16 = 9
    ↓
数据库9,user_follow_9表

优点

  • 数据均匀分布
  • 查询可定位到具体表

Q6: 如何保证缓存一致性?

A: 三种策略

  1. Cache Aside(推荐):

    写:更新数据库 → 删除缓存
    读:查缓存 → 未命中 → 查数据库 → 写缓存
    
  2. 设置过期时间

    • 缓存1小时自动过期
    • 容忍短时间不一致
  3. 双写

    • 同时写数据库和缓存
    • 可能不一致

推荐:Cache Aside + 过期时间


🎬 总结

     亿级社交关系链核心架构

数据层:
├── Redis(热数据)
│   ├── Set结构(关注/粉丝)
│   ├── List结构(收件箱)
│   └── 过期时间(1小时)
│
└── MySQL(全量数据)
    ├── 分库(16个库)
    ├── 分表(每库16张表)
    └── 索引优化

业务层:
├── 关注/取关
├── 关系查询(关注、粉丝、共同好友)
├── Feed流(推拉结合)
└── 热点优化(明星用户)

性能优化:
├── 缓存(Redis)
├── 分库分表(MySQL)
├── Bloom Filter(快速判断)
└── 分页缓存(明星用户)

🎉 恭喜你!

你已经完全掌握了亿级用户社交关系链的设计!🎊

核心要点

  1. 混合存储:Redis热数据 + MySQL全量数据
  2. 分库分表:按userId取模,256张表
  3. 推拉结合:普通用户推,明星用户拉
  4. 热点优化:Bloom Filter + 分页缓存

下次面试,这样回答

"亿级用户社交关系链采用Redis + MySQL混合存储。

Redis使用Set结构存储关注/粉丝列表,支持快速查询和集合运算(如共同好友),缓存过期时间1小时。MySQL按userId取模分库分表,16个库每个库16张表,总计256张表,存储全量数据。

Feed流采用推拉结合:普通用户(粉丝<1000)用推模式,发动态时推送到粉丝收件箱;明星用户(粉丝>1000)用拉模式,读取时实时拉取,避免写放大。

对于明星用户,用Bloom Filter快速判断是否关注,用分页缓存优化粉丝列表查询。

我们项目的社交系统支持1亿用户,平均响应时间20ms,采用这套架构运行稳定。"

面试官:👍 "很好!你对大规模社交系统的设计理解很深刻!"


本文完 🎬

上一篇: 199-设计一个分布式唯一ID生成器.md
下一篇: 201-设计一个实时排行榜系统.md

作者注:写完这篇,我都想去腾讯做微信朋友圈了!👥
如果这篇文章对你有帮助,请给我一个Star⭐!