【进阶篇】社交App的离线消息、消息漫游、多端同步设计

0 阅读5分钟

用户换了手机,聊天记录没了;在电脑上发了消息,手机上看不到;明明在线却收不到消息……这些体验问题背后,是离线消息、消息漫游、多端同步的设计问题。这篇文章帮你彻底理清这块的技术实现。

一、问题定义:三大场景

1.1 离线消息

场景: 用户不在线时,别人发来的消息,上线后要能收到。

关键问题:

· 离线消息存哪里?

· 存多少?

· 怎么拉取?

· 什么时候清理?

1.2 消息漫游

场景: 用户换设备登录,能看到历史消息。

关键问题:

· 历史消息存多久?

· 查询性能如何保证?

· 多端数据如何一致?

1.3 多端同步

场景: 用户同时在手机和电脑登录,消息要同步。

关键问题:

· 如何判断消息已读?

· 消息状态如何同步?

· 如何避免消息重复?

二、离线消息设计

2.1 存储方案

方案一:用户维度存储(推荐)

每条消息写两份:
1、发送者的发件箱
2、接收者的收件箱(如果是离线消息)

存储结构:  
offline_message:{user_id}  
字段:
   message_id  
   from_user_id  
   content  
   created_at  
   status

方案二:消息表 + 拉取标记

消息表(message)存储所有消息

用户拉取标记:  
user_sync:{user_id}  
字段:
   last_sync_message_id  
 
拉取时:  
SELECT * FROM message  
WHERE id > {last_sync_message_id}  
AND to_user_id = {user_id}

2.2 拉取策略

策略一:全量拉取

用户上线 → 拉取所有离线消息
优势:简单
劣势:消息多时性能差

策略二:增量拉取

用户上线 → 拉取最近N条 → 用户滚动时继续拉取
优势:性能好
劣势:实现复杂

策略三:按会话拉取

用户上线 → 只拉取每个会话的最新一条 + 未读数
用户点开某个会话 → 拉取该会话的离线消息
优势:性能最好,体验好
劣势:实现复杂

2.3 离线消息清理

策略说明
TTL过期Redis设置TTL,过期自动清理
拉取后删除用户拉取后删除
定时清理定时任务清理超过N天的消息
持久化到历史表清理前先同步到历史表

三、消息漫游设计

3.1 存储方案

分层存储:

热数据(近7天) : Redis

温数据(近3个月): MySQL

冷数据(3个月以上): MySQL归档库 / 对象存储

3.2 查询优化

索引设计:

CREATE TABLE message_history (  
    id BIGINT PRIMARY KEY,  
    conversation_id VARCHAR(64) NOT NULL COMMENT '会话ID',  
    message_id BIGINT NOT NULL,  
    from_user_id BIGINT NOT NULL,  
    to_user_id BIGINT NOT NULL,  
    content TEXT,  
    created_at DATETIME,  
    INDEX idx_conversation_time (conversation_id, created_at),  
    INDEX idx_message_id (message_id)  
);

会话ID生成规则:

单聊会话ID:  
min(user_id_a, user_id_b) + '_' + max(user_id_a, user_id_b)  
示例:1001_2002  
  
群聊会话ID:  
group_{group_id}  
示例:group_5001

3.3 查询流程

用户请求历史消息  
1. 先查Redis热数据  
   > 有 → 返回  
   
2. Redis无数据,查MySQL温数据  
   > 有 → 返回 + 写入Redis缓存
     
3. MySQL无数据,查冷数据(异步)

3.4 漫游时间范围策略

用户等级漫游时间说明
免费用户7天降低存储成本
付费用户30天会员权益
VIP用户360天高端服务

四、多端同步设计

4.1 多端登录策略

策略说明适用场景
单点登录同一账号只能一个设备在线安全要求高
多点登录允许多设备同时在线用户体验优先
平台互斥手机和电脑可同时在线,同平台只能一个折中方案

4.2 设备标识

每个设备分配唯一Device ID:
手机: 设备唯一标识
电脑: MAC地址 + 浏览器指纹
Web: Session ID

4.3 消息同步机制

核心问题:如何保证多端消息一致?

方案一:服务器推送到所有设备

用户A发送消息,服务器推送到用户B的所有在线设备 (B的手机 、B的电脑 、B的Web 等)。

优势: 实时性好
劣势: 服务器压力大

方案二:设备主动拉取

用户B设备上线 ,拉取最新消息(基于last_sync_id),推送或拉取,由设备状态决定

推荐方案:推拉结合

在线设备: 实时推送
离线设备: 上线后拉取

4.4 已读状态同步

问题: 手机上读了,电脑上还显示未读。

解决方案:已读状态同步

1、设备A标记已读  

2、上报服务器  
 
3、服务器推送到其他设备  

4、设备B更新已读状态

已读状态存储:

# 用户维度  
user:{user_id}:read_pointer:{conversation_id} = message_id  
  
# 示例  
user:1001:read_pointer:1001_2002 = 500000123

**查询未读消息:**

SELECT count(*) FROM message  
WHERE conversation_id = '1001_2002'  
AND id > 500000123;

五、消息去重设计

5.1 问题场景

· 网络重试导致消息重复发送

· 多设备同步导致消息重复接收

· 消息队列重投导致重复处理

5.2 去重方案

客户端去重:

发送消息时生成唯一client_msg_id  
服务端存储message_id → client_msg_id映射  
重复请求返回已存在的message_id

服务端去重:

# 消息去重表  
message_dedup:{from_user_id}:{client_msg_id}  
  
# 存在则返回已有消息ID  
# 不存在则创建新消息

幂等设计:

def send_message(from_user, to_user, content, client_msg_id):  
    # 检查是否已处理  
    existing = redis.get(f"message_dedup:{from_user}:{client_msg_id}")  
    if existing:  
        return existing  
      
    # 创建消息  
    message_id = create_message(from_user, to_user, content)  
      
    # 记录去重  
    redis.setex(  
        f"message_dedup:{from_user}:{client_msg_id}",  
        86400,  # 24小时过期  
        message_id  
    )  
      
    return message_id

六、架构总览

在这里插入图片描述

七、关键指标与优化

指标目标优化手段
离线消息拉取延迟<500ms分页拉取、按会话聚合
历史消息查询延迟<200ms索引优化、分层存储
多端同步延迟<1s推拉结合
已读同步延迟<2s实时推送
消息去重率100%client_msg_id + Redis

下篇预告: 《礼物系统/打赏系统的高并发架构拆解》——社交变现的核心功能,高并发场景下的技术挑战。

持续输出社交App开发实战经验,关注我,一起成长。