在构建分布式系统(特别是在面对社交场景中“1对多”的聊天模型:比如群聊、粉丝广播时)时,消息分发策略是系统设计的核心难题之一。读扩散(Fan-out on Read)和写扩散(Fan-out on Write)作为两种基础策略,各有优劣。本文将深入探讨这两种模式,并通过实际代码示例展示它们的应用场景和优化方案。
1. 什么是读扩散和写扩散?
这两个术语本质上是在讲:“消息写入和分发的责任,应该交给谁?”
1.1 读扩散(Fan-out on Read)
写的时候就把消息“扩散”出去。也就是说,一条消息发送后,立刻写入每个接收者的消息收件箱中(inbox)。
特点:
- 写入时负担重(写放大),但读取简单快捷。
- 适用于读频繁、写不频繁的场景。
- 更适合社交类场景,如微博、朋友圈。
工作流程:
- 消息写入中央存储
- 接收者查询自己的关联会话
- 系统聚合各会话的未读消息
- 返回聚合结果给接收者
// 读扩散消息拉取示例
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)
写的时候只写一份,读的时候再扩散。消息仅写入公共消息池,谁来读,就在读取时做“扩散处理”。
特点:
- 写入快(写一次),读取时放大(读放大)。
- 适用于写频繁、读不频繁的场景。
- 更适合实时通讯场景,如群聊、聊天室。
工作流程:
- 发送者产生消息
- 系统识别所有接收者
- 消息写入每个接收者的收件箱
- 接收者直接从本地收件箱读取
// 写扩散消息推送示例
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;
}
}
混合策略设计要点:
- 在线消息:使用写扩散推送,保证实时性
- 离线消息:存储在中央仓库,登录时读扩散拉取
- 大群处理:万人群使用读扩散+活跃用户特殊推送
- 消息漫游:中央存储作为唯一数据源
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)));
}
}
优化策略:
-
元数据与消息分离:
- 元数据分发:适度写扩散
- 消息分发:纯读扩散
-
分批异步处理:
- 将大群成员分成小批次
- 使用线程池并行处理
- 避免阻塞主线程
-
延迟关联:
- 用户首次访问时再建立关联
- 减少不必要的存储开销
-
最终一致性:
- 允许关联操作延迟完成
- 使用消息队列保证可靠性
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 监控指标
-
写扩散系统:
- 消息分发延迟
- 收件箱写入QPS
- 存储空间增长率
-
读扩散系统:
- 消息拉取延迟
- 中央存储查询QPS
- 用户会话加载时间
7. 总结:如何选择合适的分发策略
-
选择写扩散当:
- 接收者数量有限(<100)
- 实时性要求高
- 读操作远多于写操作
- 系统规模较小
-
选择读扩散当:
- 接收者数量巨大(>1000)
- 存储成本敏感
- 允许一定延迟
- 历史消息查询频繁
-
混合策略适用:
- 大多数生产环境
- 需要平衡实时性和资源消耗
- 系统包含多种消息类型(私聊/群聊)
总结:
在设计分布式消息系统时,理解业务场景的本质需求,结合读扩散和写扩散的互补优势,采用动态策略和分级处理机制,才能构建出既高效又经济的消息分发系统。
最后
如果文章对你有帮助,点个免费的赞鼓励一下吧!关注公众号:加瓦点灯, 每天推送干货知识!