读扩散 vs 写扩散:IM系统消息分发策略深度解析

593 阅读4分钟

在构建分布式系统(特别是在面对社交场景中“1对多”的聊天模型:比如群聊、粉丝广播时)时,消息分发策略是系统设计的核心难题之一。读扩散(Fan-out on Read)和写扩散(Fan-out on Write)作为两种基础策略,各有优劣。本文将深入探讨这两种模式,并通过实际代码示例展示它们的应用场景和优化方案。

1. 什么是读扩散和写扩散?

这两个术语本质上是在讲:“消息写入和分发的责任,应该交给谁?”

1.1 读扩散(Fan-out on Read)

写的时候就把消息“扩散”出去。也就是说,一条消息发送后,立刻写入每个接收者的消息收件箱中(inbox)。

特点:

  • 写入时负担重(写放大),但读取简单快捷。
  • 适用于读频繁、写不频繁的场景。
  • 更适合社交类场景,如微博、朋友圈。

工作流程

  1. 消息写入中央存储
  2. 接收者查询自己的关联会话
  3. 系统聚合各会话的未读消息
  4. 返回聚合结果给接收者
// 读扩散消息拉取示例
public List<Message> pullMessages(String userId) {
    List<Message> result = new ArrayList<>();
    
    // 1. 获取用户所有会话
    List<Conversation> conversations = getUserConversations(userId);
    
    // 2. 遍历每个会话拉取未读消息
    for (Conversation conv : conversations) {
        List<Message> unread = getUnreadMessages(userId, conv.id);
        result.addAll(unread);
    }
    
    return result;
}

1.2 写扩散(Fan-out on Write)

写的时候只写一份,读的时候再扩散。消息仅写入公共消息池,谁来读,就在读取时做“扩散处理”。

特点:

  • 写入快(写一次),读取时放大(读放大)。
  • 适用于写频繁、读不频繁的场景。
  • 更适合实时通讯场景,如群聊、聊天室。

工作流程

  1. 发送者产生消息
  2. 系统识别所有接收者
  3. 消息写入每个接收者的收件箱
  4. 接收者直接从本地收件箱读取
// 写扩散消息推送示例
public void sendMessage(String sender, String content, List<String> receivers) {
    Message msg = new Message(sender, content);
    
    // 推送给所有接收者
    for (String receiver : receivers) {
        userInboxes.computeIfAbsent(receiver, k -> new ArrayList<>())
                   .add(msg);
    }
}

2. 两种模式对比分析

特性读扩散写扩散
写操作复杂度O(1)(仅写中央存储)O(N)(N为接收者数量)
读操作复杂度O(M)(M为关注/会话数量)O(1)(直接读本地)
存储开销低(无冗余)高(多份存储)
实时性延迟高(需主动拉取)实时性好(消息直达)
适用场景大型群聊、关注关系稀疏1对1聊天、小型群组
代表产品Twitter、知乎Facebook、微信(小群)

3. 实际应用场景分析

3.1 IM系统中的读扩散实现

在一个上千人的群聊中,如果每条消息都写入所有人的 inbox,那么写放大会非常严重。此时采用读扩散方案,把消息写入一个共享的“群聊消息池”,用户读取时拉取“对这个群”的消息就足够了

public class IMReadSystem {
    // 中央消息存储
    static Map<String, List<Message>> conversationMessages = new ConcurrentHashMap<>();
    
    // 用户会话映射
    static Map<String, List<Conversation>> userConversations = new ConcurrentHashMap<>();
    
    // 获取用户所有未读消息
    public Map<String, List<Message>> getAllUnreadMessages(String userId) {
        Map<String, List<Message>> result = new HashMap<>();
        
        // 遍历用户所有会话
        for (Conversation conv : getUserConversations(userId)) {
            List<Message> unread = getUnreadMessages(userId, conv.id);
            if (!unread.isEmpty()) {
                result.put(conv.id, unread);
            }
        }
        return result;
    }
    
    // 获取会话未读消息
    private List<Message> getUnreadMessages(String userId, String conversationId) {
        int lastRead = getReadPointer(userId, conversationId);
        List<Message> messages = conversationMessages.get(conversationId);
        
        if (messages == null || lastRead >= messages.size()) {
            return Collections.emptyList();
        }
        
        return messages.subList(lastRead, messages.size());
    }
}

优势

  • 万人大群只需存储一份消息
  • 历史消息查询效率高
  • 存储成本低

劣势

  • 用户需主动拉取消息
  • 实时性较差

3.2 IM系统中的写扩散实现

写扩散适用于实时性要求高的场景:

public class IMWriteSystem {
    // 用户收件箱
    static Map<String, List<Message>> userInboxes = new ConcurrentHashMap<>();
    
    // 群组成员映射
    static Map<String, List<String>> groupMembers = new ConcurrentHashMap<>();
    
    // 发送群消息
    public void sendGroupMessage(String groupId, String sender, String content) {
        Message msg = new Message(sender, content);
        List<String> members = groupMembers.get(groupId);
        
        if (members != null) {
            for (String member : members) {
                // 排除发送者
                if (!member.equals(sender)) {
                    userInboxes.computeIfAbsent(member, k -> new ArrayList<>())
                               .add(msg);
                }
            }
        }
    }
}

优势

  • 消息实时到达
  • 接收者读取速度快
  • 实现简单

劣势

  • 万人大群需存储数万份消息
  • 存储成本高
  • 写操作延迟大

4. 混合策略:平衡的艺术

实际生产系统中,通常采用混合策略结合两者优势:

public class HybridSystem {
    // 在线用户收件箱(写扩散)
    static Map<String, List<Message>> onlineInboxes = new ConcurrentHashMap<>();
    
    // 中央消息存储(读扩散)
    static Map<String, List<Message>> centralStore = new ConcurrentHashMap<>();
    
    // 发送消息
    public void sendMessage(String conversationId, String sender, String content) {
        Message msg = new Message(sender, content);
        
        // 1. 写入中央存储(读扩散基础)
        centralStore.computeIfAbsent(conversationId, k -> new ArrayList<>())
                    .add(msg);
        
        // 2. 推送给在线用户(写扩散优化)
        pushToOnlineUsers(conversationId, msg);
    }
    
    // 用户读取消息
    public List<Message> getMessages(String userId, String conversationId) {
        List<Message> result = new ArrayList<>();
        
        // 1. 获取在线推送消息
        result.addAll(getOnlineMessages(userId));
        
        // 2. 获取离线消息(读扩散)
        result.addAll(getOfflineMessages(userId, conversationId));
        
        return result;
    }
}

混合策略设计要点:

  1. 在线消息:使用写扩散推送,保证实时性
  2. 离线消息:存储在中央仓库,登录时读扩散拉取
  3. 大群处理:万人群使用读扩散+活跃用户特殊推送
  4. 消息漫游:中央存储作为唯一数据源

5. 群组创建的特殊挑战与优化

当创建大型群组时,即使是读扩散系统也需要处理元数据分发问题:

public void createLargeGroup(String groupId, String name, List<String> members) {
    // 1. 创建群组元数据
    ConversationMeta meta = new ConversationMeta(name, members);
    conversationMeta.put(groupId, meta);
    
    // 2. 分批异步关联成员
    int batchSize = 25; // 每批25人
    ExecutorService executor = Executors.newFixedThreadPool(4);
    
    for (int i = 0; i < members.size(); i += batchSize) {
        int end = Math.min(i + batchSize, members.size());
        List<String> batch = members.subList(i, end);
        
        executor.submit(() -> {
            for (String member : batch) {
                // 添加会话记录
                addConversationToUser(member, groupId, name);
            }
        });
    }
}

// 延迟关联:用户首次访问时建立关联
public void joinConversation(String userId, String groupId) {
    if (!isUserInGroup(userId, groupId)) return;
    
    if (!hasConversation(userId, groupId)) {
        userConversations.computeIfAbsent(userId, k -> new ArrayList<>())
                         .add(new Conversation(groupId, getGroupName(groupId)));
    }
}

优化策略

  1. 元数据与消息分离

    • 元数据分发:适度写扩散
    • 消息分发:纯读扩散
  2. 分批异步处理

    • 将大群成员分成小批次
    • 使用线程池并行处理
    • 避免阻塞主线程
  3. 延迟关联

    • 用户首次访问时再建立关联
    • 减少不必要的存储开销
  4. 最终一致性

    • 允许关联操作延迟完成
    • 使用消息队列保证可靠性

6. 生产环境优化建议

6.1 性能优化

// 使用缓存优化会话列表查询
LoadingCache<String, List<Conversation>> userConversationCache = 
    Caffeine.newBuilder()
            .maximumSize(10_000)
            .expireAfterAccess(30, TimeUnit.MINUTES)
            .build(userId -> loadConversationsFromDB(userId));

// 分片存储用户会话
public List<Conversation> getUserConversations(String userId) {
    int shard = userId.hashCode() % SHARD_COUNT;
    return conversationShards[shard].get(userId);
}

6.2 限流与降级

// 创建群组的速率限制
RateLimiter groupCreateLimiter = RateLimiter.create(10); // 每秒10个

public void createGroup(...) {
    if (!groupCreateLimiter.tryAcquire()) {
        throw new RateLimitException("操作过于频繁");
    }
    // 创建逻辑...
}

// 消息推送降级策略
public void pushMessage(...) {
    if (receiverCount > 1000) { // 大群降级为读扩散
        storeToCentralOnly(...);
    } else {
        pushToAllReceivers(...);
    }
}

6.3 监控指标

  1. 写扩散系统

    • 消息分发延迟
    • 收件箱写入QPS
    • 存储空间增长率
  2. 读扩散系统

    • 消息拉取延迟
    • 中央存储查询QPS
    • 用户会话加载时间

7. 总结:如何选择合适的分发策略

  1. 选择写扩散当

    • 接收者数量有限(<100)
    • 实时性要求高
    • 读操作远多于写操作
    • 系统规模较小
  2. 选择读扩散当

    • 接收者数量巨大(>1000)
    • 存储成本敏感
    • 允许一定延迟
    • 历史消息查询频繁
  3. 混合策略适用

    • 大多数生产环境
    • 需要平衡实时性和资源消耗
    • 系统包含多种消息类型(私聊/群聊)

总结

在设计分布式消息系统时,理解业务场景的本质需求,结合读扩散和写扩散的互补优势,采用动态策略和分级处理机制,才能构建出既高效又经济的消息分发系统。

最后

如果文章对你有帮助,点个免费的赞鼓励一下吧!关注公众号:加瓦点灯, 每天推送干货知识!