IM 消息同步方案选型

7 阅读32分钟

一、简介

即时通讯(IM)系统的消息同步是确保用户在不同设备、不同网络状态下都能获得一致消息体验的核心技术。消息同步不仅仅是简单地将消息从一个设备传输到另一个设备,而是涉及实时性、可靠性、一致性、性能优化等多方面挑战的综合解决方案。

1.1 消息同步的核心价值

  1. 多设备一致性:用户可能在手机、电脑、平板等多个设备上使用IM应用,需要保证所有设备看到的消息历史、顺序和状态一致。

  2. 离线消息处理:用户网络不稳定或完全离线时,需要可靠存储消息并在网络恢复后同步。

  3. 实时通信体验:消息延迟需控制在毫秒级,提供流畅的实时对话体验。

  4. 数据可靠性:防止消息丢失、重复或乱序,保证通信的可靠性。

1.2 技术挑战

  • 网络环境复杂:弱网、断网、网络切换等场景下的可靠传输

  • 高并发处理:海量用户同时在线,消息吞吐量巨大

  • 多端状态同步:消息的已读/送达状态在多设备间保持一致

  • 存储与检索性能:海量消息的高效存储和快速检索

  • 安全性保障:消息加密、防篡改、权限控制

1.3 同步维度

IM消息同步涉及多个层面的同步需求:

同步维度描述技术要点
消息内容同步同步具体的消息文本、图片、文件等内容增量同步、实时推送、离线队列
消息状态同步同步消息的发送中/已发送/已送达/已读状态状态广播、ACK机制
会话状态同步同步会话的未读数、置顶、静音等状态状态变更通知、增量更新
设备状态同步同步多设备在线状态、同步进度设备注册、状态追踪





二、业界的生产企业级方案

方案一:增量同步方案(基于版本号)

设计思路

核心思想:基于严格的递增序列号(SyncKey/Seq)只拉取上次同步之后的新消息,避免全量传输,提高同步效率。

设计要点

  1. 序列号生成:使用分布式ID生成器(如雪花算法)生成严格递增的序列号

  2. 同步游标:客户端维护最后同步序列号(last_sync_key),可以是全局的或按会话维护

  3. 增量查询:客户端携带last_sync_key请求服务端,获取to_user_id = ? AND sync_key > last_sync_key的消息

  4. 分页机制:支持分页拉取,避免单次响应数据量过大

  5. 冲突处理:通过序列号保证消息顺序,处理消息重复等边界情况

  6. 状态同步:支持消息撤回、已读状态等操作的序列号关联

核心优势

  • 严格顺序:序列号严格递增,绝无重复,保证消息和操作的精确顺序

  • 全局唯一:跨设备、跨服务器全局唯一,支持分布式部署

  • 操作同步:每个操作(消息、撤回、已读等)都有独立的序列号,增量拉取时能拿到所有变更

  • 断点续传:任意断点都可以精确恢复,不会漏掉任何数据

适用场景

  • 历史消息加载

  • 网络恢复后的消息补全

  • 多设备间的消息同步

  • 离线消息同步

  • 作为实时推送的兜底方案


流程图


sequenceDiagram

participant CR as 客户端(渲染进程)

participant CM as 客户端(主进程)

participant CD as 客户端(数据库)

participant S as 服务端

participant SD as 服务端数据库

  


Note over CR,CD: 初始化阶段(首次登录/启动应用)

CR->>CD: 读取本地last_sync_key(上次同步的序列号)

alt last_sync_key不存在(首次启动)

CR->>CR: 设置last_sync_key=0(触发全量拉取)

end

  


Note over CR,S: 增量同步阶段(事件驱动触发)

Note over CR: 触发时机:网络恢复、应用前台切换、定时兜底

  


CR->>S: HTTP POST /messages/sync?last_sync_key=xxx&page_size=100

Note over CR: 携带参数:user_id、session_id(可选)、last_sync_key、page_size

  


S->>S: 参数验证(验证last_sync_key合法性)

S->>SD: 查询sync_key > last_sync_key的消息

Note over SD: WHERE to_user_id=? AND sync_key>? ORDER BY sync_key ASC LIMIT 100<br/>如果指定session_id则追加:AND session_id=?

  


SD-->>S: 返回消息列表(含sync_key、message_id、content等)

  


alt 查询到新消息

S->>S: 按sync_key升序排序

S->>S: 分页限制结果(page_size=100)

  


S-->>CR: 返回消息列表(含nextSyncKey、has_more标志)

Note over S: 返回数据:messages[]、nextSyncKey、has_more、total

  


CR->>CM: IPC调用存储消息

CM->>CD: 事务开始(批量存储消息)

loop 遍历消息列表

CM->>CD: 检查消息是否已存在(基于message_id防重复)

alt 消息不存在

CM->>CD: 插入消息到本地数据库

end

end

CM->>CD: 事务提交

CD-->>CM: 批量存储成功

CM-->>CR: 存储完成

  


CR->>CD: 将nextSyncKey(最新序列号)更新到last_sync_key

CD-->>CR: 更新成功

  


CR->>CR: 刷新UI显示新消息

  


alt has_more=true(还有更多消息)

CR->>S: 请求下一页(?last_sync_key=yyy&page_size=100)

Note over CR: 使用返回的nextSyncKey作为新的游标

else has_more=false

CR->>CR: 标记同步完成

CR->>CD: 更新last_sync_time为当前时间戳

end

  


else 无新消息(last_sync_key已是最新)

S-->>CR: 返回空消息列表(has_more=false)

CR->>CR: 标记同步完成

CR->>CD: 更新last_sync_time为当前时间戳

end

  


Note over CR,CD: 消息操作同步(撤回、删除等)

Note over CR: 服务端会推送或客户端主动拉取操作记录

  


alt 收到消息撤回操作(operation_sync_key > last_sync_key)

CR->>S: 请求操作记录(?last_sync_key=xxx)

S->>SD: 查询操作记录

SD-->>S: 返回操作列表

S-->>CR: 返回操作数据

  


CR->>CM: IPC调用更新消息状态

CM->>CD: 更新对应消息的状态(标记为已撤回)

CM-->>CR: 更新完成

CR->>CR: 刷新UI(移除或标记被撤回的消息)

CR->>CD: 将最新的operation_sync_key更新到last_sync_key

end


流程描述

客户端(渲染进程)流程
  1. 游标读取:通过IPC调用从**客户端(主进程)**读取最后同步序列号(last_sync_key),可以是全局的或按会话维护

  2. 增量请求:通过IPC调用请求客户端(主进程)服务端发送同步请求,携带user_idlast_sync_key和分页大小(可选携带session_id

  3. 消息处理:收到响应后,通过IPC调用通知客户端(主进程)sync_key升序存储消息到客户端(数据库)

  4. 去重处理:在存储前检查message_id去重,避免消息重复

  5. 游标更新:使用响应中的nextSyncKey(最新的sync_key)通过IPC调用通知**客户端(主进程)**更新本地last_sync_key

  6. 循环拉取:如果响应指示还有更多消息,继续请求下一页

  7. 状态保存:同步完成后通过IPC调用保存同步状态和时间戳

  8. 操作同步:通过IPC调用请求**客户端(主进程)**处理消息撤回、删除等操作记录

  9. UI刷新:收到新消息或操作记录后刷新UI显示

客户端(主进程)流程
  1. 接收请求:接收**客户端(渲染进程)**的IPC调用请求

  2. 数据库操作

  • 读取last_sync_key:从**客户端(数据库)**读取同步游标

  • 存储消息:开启事务,遍历消息列表,检查message_id去重后插入消息

  • 更新游标:将nextSyncKey(最新的sync_key)存储到客户端(数据库)

  • 更新时间戳:记录同步完成时间

  1. 网络请求:向服务端发送HTTP请求(/messages/sync

  2. 返回结果:将服务端的响应数据通过IPC返回给客户端(渲染进程)

服务端流程
  1. 参数验证:验证last_sync_key合法性,确保为正整数

  2. 增量查询:在服务端数据库中查询to_user_id = ? AND sync_key > last_sync_key的消息,如果指定session_id则追加AND session_id = ?

  3. 排序分页:按sync_key升序排序,限制返回数量(如100条)

  4. 元数据计算:计算是否还有更多消息,返回has_more标志

  5. 数据封装:返回消息列表和nextSyncKey(最新的sync_key值)

  6. 操作查询:查询sync_key大于last_sync_key的操作记录(撤回、删除等)


流程图中字段的详细说明

流程图中的字段名说明
last_sync_key客户端(渲染进程)维护的同步游标(前端代码中可使用lastSyncKey),记录上次同步到的sync_key值,用于增量查询。首次启动时为0,触发全量拉取。通过IPC调用客户端(主进程)从客户端(数据库)读取和更新
sync_key服务端为每条消息分配的严格递增的全局唯一标识(前端代码中可使用syncKey),用于消息排序、增量查询和防重复
message_id服务端消息ID(前端代码中可使用messageId),消息的主键标识,用于客户端去重处理
operation_sync_key操作记录(如撤回、删除)的同步版本号(前端代码中可使用operationSyncKey),与消息共用同一序列号空间,用于同步消息状态的变更
session_id会话ID(前端代码中可使用sessionId),标识消息所属的会话,用于按会话查询消息
has_more服务端返回的分页标志(前端代码中可使用hasMore),true表示还有更多消息,false表示已同步完成(非数据库字段)
total查询结果的总数,用于客户端(渲染进程)显示消息总数(非数据库字段)
user_id用户ID(前端代码中可使用userId),标识操作的用户,用于区分不同用户的数据(服务端字段)
device_id设备ID(前端代码中可使用deviceId),标识具体的设备,用于支持多设备同步(服务端字段)
page_size分页大小(前端代码中可使用pageSize),单次同步的消息数量限制(非数据库字段)
from_user_id发送者用户ID(前端代码中可使用fromUserId),标识消息的发送方
to_user_id接收者用户ID(前端代码中可使用toUserId),标识消息的接收方
message_type消息类型(前端代码中可使用messageType):1-文本 2-图片 3-语音 4-视频 5-文件
content消息内容,存储消息的具体内容信息
status消息状态(前端代码中可使用status):0-发送中 1-发送成功 2-发送失败
create_time消息创建时间(前端代码中可使用createTime),记录消息的生成时间
last_sync_time最后同步时间(前端代码中可使用lastSyncTime),记录上次成功同步的时间戳(存储在客户端(数据库),由客户端(渲染进程)通过IPC调用客户端(主进程)读写)

数据库设计

后端MySQL表设计

-- 消息表

CREATE TABLE `message_base_info` (

`message_id` VARCHAR(64) NOT NULL COMMENT '消息ID,服务端生成,唯一标识符',

`sync_key` BIGINT(20) NOT NULL COMMENT '同步版本号(严格递增)',

`session_id` VARCHAR(64) NOT NULL COMMENT '会话ID',

`from_user_id` BIGINT(20) NOT NULL COMMENT '发送者用户ID',

`to_user_id` BIGINT(20) NOT NULL COMMENT '接收者用户ID',

`to_type` TINYINT(4) NOT NULL COMMENT '接收方类型:1-用户(单聊),2-群组(群聊)',

`message_type` TINYINT(4) NOT NULL COMMENT '消息类型:1-文本,2-图片附件,3-语音附件,4-视频附件,5-文件附件,6-位置,99-系统',

`content` TEXT NOT NULL COMMENT '消息内容',

`status` TINYINT(4) NOT NULL DEFAULT 0 COMMENT '消息状态:0-发送中,1-发送成功,2-发送失败',

`is_deleted` BOOLEAN DEFAULT FALSE COMMENT '是否删除(软删除)',

`send_time` BIGINT(20) NOT NULL COMMENT '发送时间戳',

`created_at` BIGINT(20) NOT NULL COMMENT '创建时间',

`updated_at` BIGINT(20) NOT NULL COMMENT '更新时间',

`version` INT DEFAULT 0 COMMENT '数据版本号(用于乐观锁)',

PRIMARY KEY (`message_id`),

UNIQUE KEY `uk_sync_key` (`sync_key`),

KEY `idx_to_user_sync` (`to_user_id`, `sync_key`),

KEY `idx_session_sync` (`session_id`, `sync_key`),

KEY `idx_send_time` (`send_time DESC`)

) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='消息表';

  


-- 消息操作记录表(用于记录撤回、删除等操作)

CREATE TABLE `message_revoke_logs` (

`id` BIGINT(20) NOT NULL AUTO_INCREMENT,

`message_id` VARCHAR(64) NOT NULL UNIQUE COMMENT '消息ID',

`sync_key` BIGINT(20) NOT NULL COMMENT '同步版本号(与消息共享同一序列号空间)',

`operation_type` TINYINT(4) NOT NULL COMMENT '操作类型:1-撤回 2-删除',

`operator_id` BIGINT(20) NOT NULL COMMENT '操作者ID',

`revoke_time` BIGINT(20) NOT NULL COMMENT '撤回时间戳',

`revoke_reason` VARCHAR(200) COMMENT '撤回原因',

`device_id` VARCHAR(100) COMMENT '撤回设备ID',

`created_at` BIGINT(20) NOT NULL COMMENT '创建时间',

PRIMARY KEY (`id`),

UNIQUE KEY `uk_sync_key` (`sync_key`),

KEY `idx_message_id` (`message_id`),

KEY `idx_operator_id` (`operator_id`),

KEY `idx_revoke_time` (`revoke_time DESC`)

) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='消息操作记录表';

  


-- 用户同步记录表

CREATE TABLE `user_sync_record` (

`id` BIGINT(20) NOT NULL AUTO_INCREMENT,

`user_id` BIGINT(20) NOT NULL COMMENT '用户ID',

`device_id` VARCHAR(64) NOT NULL COMMENT '设备ID',

`current_sync_key` BIGINT(20) NOT NULL DEFAULT 0 COMMENT '当前同步版本号',

`last_sync_time` BIGINT(20) NOT NULL COMMENT '最后同步时间戳',

`last_online_time` BIGINT(20) NOT NULL COMMENT '最后在线时间戳',

`sync_count` INT(11) NOT NULL DEFAULT 0 COMMENT '同步次数',

`push_count` INT(11) NOT NULL DEFAULT 0 COMMENT '推送次数',

`created_at` BIGINT(20) NOT NULL COMMENT '创建时间',

`updated_at` BIGINT(20) NOT NULL COMMENT '更新时间',

PRIMARY KEY (`id`),

UNIQUE KEY `uk_user_device` (`user_id`, `device_id`),

KEY `idx_last_sync_time` (`last_sync_time DESC`)

) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户同步记录表';

  


前端SQLite表设计

-- 本地消息表

CREATE TABLE message_base_info (

id INTEGER PRIMARY KEY AUTOINCREMENT,

message_id TEXT NOT NULL UNIQUE COMMENT '服务端消息ID',

sync_key BIGINT NOT NULL COMMENT '同步版本号',

session_id TEXT NOT NULL COMMENT '会话ID',

from_user_id INTEGER NOT NULL COMMENT '发送者用户ID',

to_user_id INTEGER NOT NULL COMMENT '接收者用户ID',

to_type INTEGER NOT NULL COMMENT '接收方类型:1-用户(单聊),2-群组(群聊)',

message_type INTEGER NOT NULL COMMENT '消息类型:1-文本,2-图片附件,3-语音附件,4-视频附件,5-文件附件,6-位置,99-系统',

content TEXT NOT NULL COMMENT '消息内容',

status INTEGER NOT NULL DEFAULT 0 COMMENT '消息状态:0-发送中,1-发送成功,2-发送失败',

is_deleted INTEGER NOT NULL DEFAULT 0 COMMENT '是否删除:0-否,1-是',

send_time INTEGER NOT NULL COMMENT '发送时间戳',

created_at INTEGER NOT NULL COMMENT '创建时间',

updated_at INTEGER NOT NULL COMMENT '更新时间',

version INTEGER NOT NULL DEFAULT 0 COMMENT '数据版本号(用于乐观锁)',

sync_time INTEGER NOT NULL COMMENT '同步时间',

receive_time INTEGER NOT NULL COMMENT '接收时间(客户端特有字段)',

receive_type INTEGER NOT NULL DEFAULT 0 COMMENT '接收类型:0-增量同步 1-实时推送(客户端特有字段)'

);

  


-- 同步配置表

CREATE TABLE sync_config (

id INTEGER PRIMARY KEY AUTOINCREMENT,

key TEXT NOT NULL UNIQUE COMMENT '配置键(如last_sync_key或last_sync_key_xxx)',

value TEXT NOT NULL COMMENT '配置值',

updated_at INTEGER NOT NULL COMMENT '更新时间'

);

  


-- 消息操作记录表(本地)

CREATE TABLE message_revoke_logs (

id INTEGER PRIMARY KEY AUTOINCREMENT,

message_id TEXT NOT NULL UNIQUE COMMENT '服务端消息ID',

sync_key BIGINT NOT NULL COMMENT '同步版本号',

operation_type INTEGER NOT NULL COMMENT '操作类型:1-撤回 2-删除',

operator_id INTEGER NOT NULL COMMENT '操作者ID',

revoke_time INTEGER NOT NULL COMMENT '撤回时间戳',

created_at INTEGER NOT NULL COMMENT '创建时间'

);


优缺点

优点
  1. 高效传输:只传输增量数据,大大减少网络流量

  2. 顺序保证:严格递增的序列号保证消息顺序和完整性

  3. 服务器压力小:相比实时推送,服务器资源消耗更低

  4. 断点续传:支持从任意游标位置继续同步

  5. 带宽友好:适合移动网络和流量敏感场景

  6. 支持操作同步:通过序列号关联撤回、删除等操作

缺点
  1. 实时性不足:依赖客户端主动拉取,存在同步延迟

  2. 游标管理复杂:需要维护准确的游标状态

  3. 客户端状态依赖:游标丢失或损坏会导致同步问题

  4. 需要配合定时任务:为了及时发现新消息,需要定时拉取

  5. 需要分布式ID生成器:需要额外组件生成严格递增的序列号


代码示例

后端代码(Node.js + MySQL)

// 伪代码:生成sync_key(使用分布式ID生成器,如雪花算法)

function generateSyncKey() {

return Snowflake.generateId();

}

  


// 伪代码:生成message_id(消息ID)

function generateMessageId() {

return Snowflake.generateId();

}

  


// 伪代码:发送消息时生成sync_key(版本号)

async function sendMessage(fromUserId, toUserId, sessionId, msgType, content) {

try {

// 生成严格递增的版本号和消息ID

const syncKey = generateSyncKey();

const messageId = generateMessageId();

const currentTime = Date.now();

  


const query = `

INSERT INTO message_base_info (message_id, sync_key, session_id, from_user_id, to_user_id, message_type, content, status, send_time, created_at, updated_at)

VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)

`;

  


await db.query(query, [messageId, syncKey, sessionId, fromUserId, toUserId, msgType, content, 1, currentTime, currentTime, currentTime]);

  


return {

code: 0,

data: {

messageId: messageId,

syncKey: syncKey

}

};

} catch (error) {

console.error('发送消息失败:', error);

return {

code: -1,

message: '发送消息失败'

};

}

}

  


// 伪代码:增量同步消息接口(基于sync_key)

async function syncMessagesBySyncKey(userId, deviceId, lastSyncKey, sessionId = null) {

try {

const limit = 100;

  


// 构建查询条件

let whereClause = 'to_user_id = ? AND sync_key > ?';

let queryParams = [userId, lastSyncKey];

  


if (sessionId) {

whereClause += ' AND session_id = ?';

queryParams.push(sessionId);

}

  


// 查询sync_key之后的消息

const query = `

SELECT * FROM message_base_info

WHERE ${whereClause}

ORDER BY sync_key ASC

LIMIT ?

`;

  


const messages = await db.query(query, [...queryParams, limit]);

  


// 转换字段名为驼峰命名(用于返回给前端)

const formattedMessages = messages.map(msg => ({

messageId: msg.message_id,

syncKey: msg.sync_key,

sessionId: msg.session_id,

fromUserId: msg.from_user_id,

toUserId: msg.to_user_id,

toType: msg.to_type,

messageType: msg.message_type,

content: msg.content,

status: msg.status,

isDeleted: msg.is_deleted,

sendTime: msg.send_time,

createdAt: msg.created_at,

updatedAt: msg.updated_at,

version: msg.version

}));

  


// 获取最新的sync_key

let nextSyncKey = lastSyncKey;

if (messages.length > 0) {

nextSyncKey = messages[messages.length - 1].sync_key;

}

  


// 更新用户同步记录(服务端记录)

// 注意:user_sync_record是服务端用于记录用户同步状态的表,用于服务端监控和统计

// 客户端使用sync_config表来维护lastSyncKey

const currentTime = Date.now();

const updateQuery = `

INSERT INTO user_sync_record (user_id, device_id, current_sync_key, last_sync_time, last_online_time, created_at, updated_at)

VALUES (?, ?, ?, ?, ?, ?, ?)

ON DUPLICATE KEY UPDATE

current_sync_key = VALUES(current_sync_key),

last_sync_time = VALUES(last_sync_time),

last_online_time = VALUES(last_online_time),

updated_at = VALUES(updated_at)

`;

await db.query(updateQuery, [userId, deviceId, nextSyncKey, currentTime, currentTime, currentTime, currentTime]);

  


return {

code: 0,

data: {

messages: formattedMessages,

nextSyncKey: nextSyncKey,

has_more: messages.length >= limit

}

};

} catch (error) {

console.error('增量同步消息失败:', error);

return {

code: -1,

message: '增量同步消息失败'

};

}

}

  


// 伪代码:撤回消息时生成sync_key(版本号)

async function recallMessage(userId, messageId) {

try {

// 生成操作记录的版本号(与消息共用同一序列号空间)

const syncKey = generateSyncKey();

  


const currentTime = Date.now();

  


// 插入操作记录

const query = `

INSERT INTO message_revoke_logs (sync_key, operation_type, message_id, operator_id, revoke_time, created_at)

VALUES (?, 1, ?, ?, ?, ?)

`;

  


await db.query(query, [syncKey, messageId, userId, currentTime, currentTime]);

  


// 更新原消息状态

await db.query('UPDATE message_base_info SET status = 2 WHERE message_id = ?', [messageId]);

  


return {

code: 0,

data: {

syncKey: syncKey

}

};

} catch (error) {

console.error('撤回消息失败:', error);

return {

code: -1,

message: '撤回消息失败'

};

}

}

前端代码(客户端)

// 伪代码:增量同步消息(基于sync_key)【前端伪代码 - 客户端(渲染进程)】

async function syncMessagesBySyncKey(session_id = null) {

try {

// 通过IPC调用从客户端(主进程)读取last_sync_key(版本号游标)

const last_sync_key = await ipcRenderer.invoke('read-sync-key', session_id);

  


// 向服务端请求增量同步(查询sync_key > last_sync_key的消息)

const response = await ipcRenderer.invoke('sync-messages-by-synckey', last_sync_key, session_id);

  


if (response.code === 0) {

const { messages, nextSyncKey, has_more } = response.data;

  


if (messages.length > 0) {

// 通过IPC调用客户端(主进程)批量插入新消息(使用INSERT OR IGNORE避免重复)

await ipcRenderer.invoke('save-messages', messages);

  


// 通过IPC调用客户端(主进程)更新last_sync_key为最新版本号

await ipcRenderer.invoke('update-sync-key', nextSyncKey, session_id);

  


console.log(`增量同步成功,新增 ${messages.length} 条消息,版本号: ${last_sync_key} -> ${nextSyncKey}`);

  


// 如果还有更多消息,继续拉取

if (has_more) {

await syncMessagesBySyncKey(session_id);

}

} else {

console.log('暂无新消息,last_sync_key:', last_sync_key);

}

}

} catch (error) {

console.error('增量同步消息失败:', error);

}

}

  


// 以下为客户端(主进程)的伪代码实现

// 读取同步游标

ipcMain.handle('read-sync-key', async (event, sessionId = null) => {

let config;

if (sessionId) {

config = await db.get('SELECT value FROM sync_config WHERE key = ?', [`last_sync_key_${sessionId}`]);

} else {

config = await db.get('SELECT value FROM sync_config WHERE key = ?', ['last_sync_key']);

}

return config ? parseInt(config.value) : 0;

});

  


// 保存消息列表

ipcMain.handle('save-messages', async (event, messages) => {

const currentTime = Date.now();

const insertPromises = messages.map(msg => {

return db.run(`

INSERT OR IGNORE INTO message_base_info (message_id, sync_key, session_id, from_user_id, to_user_id, to_type, message_type, content, status, send_time, created_at, updated_at, version, sync_time)

VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)

`, [msg.messageId, msg.syncKey, msg.sessionId, msg.fromUserId, msg.toUserId, msg.toType, msg.messageType, msg.content, msg.status, msg.sendTime, msg.createdAt, msg.updatedAt, msg.version, currentTime]);

});

await Promise.all(insertPromises);

return { code: 0 };

});

  


// 更新同步游标

ipcMain.handle('update-sync-key', async (event, nextSyncKey, sessionId = null) => {

const currentTime = Date.now();

if (sessionId) {

await db.run(`

INSERT OR REPLACE INTO sync_config (key, value, updated_at)

VALUES (?, ?, ?)

`, [`last_sync_key_${sessionId}`, nextSyncKey, currentTime]);

} else {

await db.run(`

INSERT OR REPLACE INTO sync_config (key, value, updated_at)

VALUES ('last_sync_key', ?, ?)

`, [nextSyncKey, currentTime]);

}

return { code: 0 };

});




方案二:增量同步 + 实时推送(混合方案)

设计思路

核心思想:综合运用WebSocket实时推送和增量同步两种方式,在线时通过WebSocket实现毫秒级实时性,离线或断线时通过增量同步补齐消息,兼顾实时性和可靠性。

设计要点

  1. 在线实时推送:用户在线时,服务端通过WebSocket主动推送新消息

  2. 离线增量同步:用户离线或断线后,通过增量同步拉取离线消息

  3. 消息去重:客户端通过message_id去重,避免WebSocket推送和增量同步导致的重复

  4. 断线重连:WebSocket断线后自动重连,重连后进行增量同步补齐

  5. 兜底机制:定时增量同步作为兜底,防止WebSocket推送失败导致消息丢失

  6. 状态同步:通过WebSocket或增量同步同步消息状态变更(已读、撤回等)

适用场景

  • 对实时性要求高的社交聊天应用

  • 多设备协同的企业IM系统

  • 需要兼顾在线和离线场景的IM应用

  • 大规模用户的高并发场景


流程图


sequenceDiagram

participant CR as 客户端(渲染进程)

participant CM as 客户端(主进程)

participant CD as 客户端(数据库)

participant S as 服务端

participant SD as 服务端数据库

participant OfflineQueue as 离线队列

  


Note over CR,SD: 初始化阶段

Note over CR: 应用启动,检查网络状态

CR->>CD: 读取本地last_sync_key

CR->>S: HTTP POST /messages/sync?last_sync_key=xxx

S->>SD: 查询to_user_id = ? AND sync_key > last_sync_key的消息

SD-->>S: 返回离线消息列表

S-->>CR: 返回离线消息数据

CR->>CM: IPC调用存储离线消息

CM->>CD: 保存离线消息到本地数据库

CR->>CD: 将最新的sync_key更新到last_sync_key

  


Note over CR,S: 建立WebSocket连接

CR->>CM: IPC调用建立WebSocket连接

CM->>S: 建立WebSocket连接

S-->>CM: 连接成功

CM->>S: 发送认证消息(user_id + token)

S-->>CM: 认证成功

CM->>CM: 启动心跳定时器(每30秒)

CM-->>CR: IPC通知连接就绪

  


Note over S,SD: 实时推送阶段(在线期间)

Note over S: 新消息到达(from_user_id -> to_user_id)

S->>SD: 保存消息(生成sync_key)

SD-->>S: 保存成功,返回message_id和sync_key

  


S->>S: 查询to_user_id的在线设备

  


alt to_user_id有在线WebSocket连接

S->>CM: WebSocket推送新消息

Note over S,CM: 推送数据:message_id、sync_key、session_id、content等

CM->>CM: 接收并解析消息

CM->>CR: IPC推送消息到渲染进程

  


alt 消息不存在(基于message_id判断)

CR->>CM: IPC调用存储消息

CM->>CD: 保存消息到本地数据库

CR->>CD: 将最新的sync_key更新到last_sync_key

CR->>CR: 刷新UI显示新消息

else 消息已存在

CR->>CR: 跳过(消息重复,可能是增量同步已经拉取过)

end

else to_user_id无在线连接

S->>OfflineQueue: 消息存入离线队列

Note over OfflineQueue: 离线队列:user_id、message_id、sync_key等(服务端内存或Redis)

end

  


Note over CM,S: 心跳维持

loop 每30秒

CM->>S: 发送心跳包(PING)

S-->>CM: 返回心跳响应(PONG)

end

  


Note over CR,S: 断线重连阶段

S-->>CM: WebSocket连接断开

CM->>CR: IPC通知连接断开

CM->>CM: 停止心跳定时器

CM->>CM: 记录断线时间

  


Note over CR: 检测到网络恢复或用户重新上线

CR->>CR: 启动重连机制

CR->>CM: IPC调用重新建立WebSocket连接

CM->>S: 重新建立WebSocket连接

S-->>CM: 连接成功

CM->>S: 发送认证消息

  


Note over CR,SD: 重连后进行增量同步(补齐断线期间的消息)

CR->>S: HTTP POST /messages/sync?last_sync_key=xxx

S->>SD: 查询to_user_id = ? AND sync_key > last_sync_key的消息

SD-->>S: 返回离线期间的新消息

S-->>CR: 返回离线消息数据

  


alt 有离线消息

CR->>CM: IPC调用存储消息

CM->>CD: 合并并存储消息(去重)

CR->>CD: 将最新的sync_key更新到last_sync_key

CR->>CR: 刷新UI显示

end

  


Note over CR,SD: 定时兜底同步阶段(每3-5分钟 - 补充方式)

Note over CR: 触发时机:客户端定时触发,作为兜底机制

CR->>S: HTTP POST /messages/sync?last_sync_key=xxx

S->>SD: 查询to_user_id = ? AND sync_key > last_sync_key的消息

SD-->>S: 返回消息列表

S-->>CR: 返回消息数据

  


alt 有新消息

CR->>CM: IPC调用存储消息

CM->>CD: 存储消息(去重)

CR->>CD: 将最新的sync_key更新到last_sync_key

CR->>CR: 刷新UI

end

  


Note over S,CR: 消息状态同步(撤回、已读等)

Note over S: 用户撤回消息

S->>SD: 更新消息状态为已撤回

S->>SD: 生成操作记录(operation_sync_key)

  


alt 用户在线(WebSocket连接)

S->>CM: WebSocket推送撤回操作

CM->>CR: IPC推送撤回操作到渲染进程

CR->>CM: IPC调用更新消息状态

CM->>CD: 更新本地消息状态

CR->>CR: 刷新UI(移除或标记被撤回的消息)

else 用户离线

Note over SD: 操作记录已存储,用户上线时通过增量同步拉取

end


流程描述

【当前客户端流程】

1. 初始化阶段

  • 客户端(渲染进程)

  • 应用启动时,检查网络状态

  • 从**客户端(数据库)**读取本地last_sync_key

  • 服务端发送HTTP请求进行增量同步,拉取离线消息(to_user_id = ? AND sync_key > last_sync_key

  • 接收离线消息后,通过IPC调用将消息传递给客户端(主进程)

  • 通知**客户端(主进程)**更新last_sync_key到数据库

  • 通过IPC调用请求**客户端(主进程)**建立WebSocket连接

  • 监听IPC消息,准备接收实时推送

  • 客户端(主进程)

  • 接收渲染进程的IPC调用

  • 将离线消息保存到客户端(数据库)

  • 更新last_sync_key到数据库

  • 服务端建立WebSocket连接,发送认证信息(包含用户Token)

  • 认证成功后,启动心跳定时器(每30秒发送一次心跳)

  • 通过IPC通知**客户端(渲染进程)**WebSocket连接就绪

  • 监听WebSocket消息

2. 实时推送阶段(在线期间)

  • 客户端(主进程)

  • 接收到服务端的WebSocket新消息推送时,解析消息数据

  • 通过IPC调用将消息传递给客户端(渲染进程)

  • 定时向服务端发送心跳包(每30秒),保持连接活跃

  • 接收心跳响应,确认连接正常

  • 客户端(渲染进程)

  • 监听**客户端(主进程)**的IPC消息

  • 当收到新消息推送时,解析消息数据

  • 通过message_id判断消息是否已存在(去重处理)

  • 如果消息不存在,通过IPC调用将消息传递给**客户端(主进程)**进行存储,更新last_sync_key,刷新UI

  • 如果消息已存在,跳过处理(可能是增量同步已经拉取过)

3. 断线重连阶段

  • 客户端(主进程)

  • 检测到WebSocket连接断开时

  • 停止心跳定时器

  • 记录断线时间

  • 通过IPC通知**客户端(渲染进程)**连接断开

  • 当检测到网络恢复时

  • 尝试重新建立WebSocket连接

  • 重连成功后,重新发送认证消息

  • 通过IPC通知**客户端(渲染进程)**重新连接成功

  • 客户端(渲染进程)

  • 接收到**客户端(主进程)**IPC通知连接断开

  • 当检测到网络恢复或用户重新上线时

  • 通过IPC通知**客户端(主进程)**启动重连机制

  • 服务端发送增量同步请求,拉取断线期间的新消息

  • 接收消息后,通过IPC调用传递给客户端(主进程)

  • 通知**客户端(主进程)**更新last_sync_key,刷新UI

4. 定时兜底同步阶段(每3-5分钟 - 补充方式)

  • 触发时机:**客户端(渲染进程)**定时(如每3-5分钟)主动发起一次增量同步请求

  • 作用:防止因网络抖动、推送失败导致的数据不一致,确保最终一致性

  • 执行流程

  • 客户端(渲染进程)

  • 服务端发送增量同步请求,携带last_sync_key

  • 接收该时间点之后所有更新的消息

  • 通过IPC调用传递给**客户端(主进程)**进行存储和去重

  • 更新last_sync_key,触发UI刷新,显示最新消息

  • 客户端(主进程)

  • 接收渲染进程的IPC调用

  • 将消息保存到客户端(数据库)(去重处理)

  • 更新last_sync_key

【服务端流程】

1. 处理增量同步请求

  • 服务端

  • 接收客户端的同步请求,包含last_sync_key和分页参数

  • 服务端数据库查询to_user_id = ? AND sync_key > last_sync_key的消息

  • 按sync_key升序排序,限制返回数量(如100条)

  • 计算是否还有更多消息,返回has_more标志

  • 返回消息列表和最新的sync_key

2. WebSocket连接管理

  • 服务端

  • 接收客户端的WebSocket连接请求

  • 验证客户端认证信息,提取用户ID

  • 保存用户ID到WebSocket连接的映射关系(支持多设备连接)

  • 处理心跳消息,更新最后心跳时间

  • 监听连接断开事件,清理映射关系

3. 消息推送(实时性保障)

  • 服务端

  • 收到新消息后,保存到服务端数据库,生成sync_key

  • 查询接收者用户的所有在线WebSocket连接

  • 如果有在线连接:通过WebSocket实时推送新消息

  • 如果无在线连接:消息存入离线队列,等待用户上线后通过增量同步拉取

4. 状态同步(撤回、已读等)

  • 服务端

  • 监听消息状态变更事件(撤回、删除、已读等)

  • 如果用户在线,通过WebSocket推送操作记录

  • 如果用户离线,操作记录存储在服务端数据库,用户上线时通过增量同步拉取


流程图中字段的详细说明

流程图中的字段名说明
last_sync_key客户端(渲染进程) 维护的同步游标(前端代码中可使用lastSyncKey),存储在**客户端(数据库)中,记录当前已同步到的sync_key值,用于增量查询。首次启动时为0,触发全量拉取。通过IPC调用通知客户端(主进程)**进行更新
sync_key服务端为每条消息分配的严格递增的全局唯一标识(前端代码中可使用syncKey),用于消息排序、增量查询和防重复
message_id服务端消息ID(前端代码中可使用messageId),消息的主键标识,用于客户端去重处理
operation_sync_key操作记录(如撤回、删除)的同步版本号(前端代码中可使用operationSyncKey),与消息共用同一序列号空间,用于同步消息状态的变更
session_id会话ID(前端代码中可使用sessionId),标识消息所属的会话,用于按会话查询消息
connection_idWebSocket连接的唯一标识,用于管理连接会话
user_id用户ID(前端代码中可使用userId),标识连接的用户,用于认证和消息推送
token认证令牌(前端代码中可使用token),用于WebSocket连接时的身份验证(非数据库字段)
device_id设备ID(前端代码中可使用deviceId),标识具体的设备,用于支持多设备连接
from_user_id发送者用户ID(前端代码中可使用fromUserId),标识消息的发送方
to_user_id接收者用户ID(前端代码中可使用toUserId),标识消息的接收方
message_type消息类型(前端代码中可使用messageType):1-文本 2-图片 3-语音 4-视频 5-文件
content消息内容,存储消息的具体内容信息
status消息状态(前端代码中可使用status):0-发送中 1-发送成功 2-发送失败
create_time消息创建时间(前端代码中可使用createTime),记录消息的生成时间

| server_ip | 服务端IP,记录WebSocket服务器地址 | | server_port | 服务端端口,记录WebSocket服务器端口 | | connect_time | 连接时间,记录WebSocket建立连接的时间 | | last_heartbeat_time | 最后心跳时间,记录最后一次心跳响应的时间,用于检测连接是否活跃 | | last_sync_time | 最后同步时间,记录上次成功同步的时间戳(服务端字段) | | last_online_time | 最后在线时间,记录用户最后在线的时间(服务端字段) | | sync_count | 同步次数,记录累计同步的次数(服务端字段) | | push_count | 推送次数,记录累计推送的次数(服务端字段) | | sync_time | 同步时间,记录消息同步到**客户端(数据库)的时间(前端字段) | | receive_time | 接收时间,记录客户端(渲染进程)**接收到消息的时间(前端字段) | | receive_type | 接收类型:0-增量同步 1-实时推送(前端字段) |


数据库设计

后端MySQL表设计

-- 消息表

CREATE TABLE `message_base_info` (

`message_id` VARCHAR(64) NOT NULL COMMENT '消息ID,服务端生成,唯一标识符',

`sync_key` BIGINT(20) NOT NULL COMMENT '同步版本号(严格递增)',

`session_id` VARCHAR(64) NOT NULL COMMENT '会话ID',

`from_user_id` BIGINT(20) NOT NULL COMMENT '发送者用户ID',

`to_user_id` BIGINT(20) NOT NULL COMMENT '接收者用户ID',

`to_type` TINYINT(4) NOT NULL COMMENT '接收方类型:1-用户(单聊),2-群组(群聊)',

`message_type` TINYINT(4) NOT NULL COMMENT '消息类型:1-文本,2-图片附件,3-语音附件,4-视频附件,5-文件附件,6-位置,99-系统',

`content` TEXT NOT NULL COMMENT '消息内容',

`status` TINYINT(4) NOT NULL DEFAULT 0 COMMENT '消息状态:0-发送中,1-发送成功,2-发送失败',

`is_deleted` BOOLEAN DEFAULT FALSE COMMENT '是否删除(软删除)',

`send_time` BIGINT(20) NOT NULL COMMENT '发送时间戳',

`created_at` BIGINT(20) NOT NULL COMMENT '创建时间',

`updated_at` BIGINT(20) NOT NULL COMMENT '更新时间',

`version` INT DEFAULT 0 COMMENT '数据版本号(用于乐观锁)',

PRIMARY KEY (`message_id`),

UNIQUE KEY `uk_sync_key` (`sync_key`),

KEY `idx_to_user_sync` (`to_user_id`, `sync_key`),

KEY `idx_session_sync` (`session_id`, `sync_key`),

KEY `idx_send_time` (`send_time DESC`)

) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='消息表';

  


-- 消息操作记录表

CREATE TABLE `message_revoke_logs` (

`id` BIGINT(20) NOT NULL AUTO_INCREMENT,

`message_id` VARCHAR(64) NOT NULL UNIQUE COMMENT '消息ID',

`sync_key` BIGINT(20) NOT NULL COMMENT '同步版本号',

`operation_type` TINYINT(4) NOT NULL COMMENT '操作类型:1-撤回 2-删除',

`operator_id` BIGINT(20) NOT NULL COMMENT '操作者ID',

`revoke_time` BIGINT(20) NOT NULL COMMENT '撤回时间戳',

`revoke_reason` VARCHAR(200) COMMENT '撤回原因',

`device_id` VARCHAR(100) COMMENT '撤回设备ID',

`created_at` BIGINT(20) NOT NULL COMMENT '创建时间',

PRIMARY KEY (`id`),

UNIQUE KEY `uk_sync_key` (`sync_key`),

KEY `idx_message_id` (`message_id`),

KEY `idx_operator_id` (`operator_id`),

KEY `idx_revoke_time` (`revoke_time DESC`)

) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='消息操作记录表';

  


-- 用户同步记录表

CREATE TABLE `user_sync_record` (

`id` BIGINT(20) NOT NULL AUTO_INCREMENT,

`user_id` BIGINT(20) NOT NULL COMMENT '用户ID',

`device_id` VARCHAR(64) NOT NULL COMMENT '设备ID',

`current_sync_key` BIGINT(20) NOT NULL DEFAULT 0 COMMENT '当前同步版本号',

`last_sync_time` BIGINT(20) NOT NULL COMMENT '最后同步时间戳',

`last_online_time` BIGINT(20) NOT NULL COMMENT '最后在线时间戳',

`sync_count` INT(11) NOT NULL DEFAULT 0 COMMENT '同步次数',

`push_count` INT(11) NOT NULL DEFAULT 0 COMMENT '推送次数',

`created_at` BIGINT(20) NOT NULL COMMENT '创建时间',

`updated_at` BIGINT(20) NOT NULL COMMENT '更新时间',

PRIMARY KEY (`id`),

UNIQUE KEY `uk_user_device` (`user_id`, `device_id`),

KEY `idx_last_sync_time` (`last_sync_time DESC`)

) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户同步记录表';

  


前端SQLite表设计

-- 本地消息表

-- 注意:receive_time 和 receive_type 是客户端特有的字段,用于追踪消息的接收来源和时间

-- 服务端数据库中不包含这两个字段

CREATE TABLE message_base_info (

id INTEGER PRIMARY KEY AUTOINCREMENT,

message_id TEXT NOT NULL UNIQUE COMMENT '服务端消息ID',

sync_key BIGINT NOT NULL COMMENT '同步版本号',

session_id TEXT NOT NULL COMMENT '会话ID',

from_user_id INTEGER NOT NULL COMMENT '发送者用户ID',

to_user_id INTEGER NOT NULL COMMENT '接收者用户ID',

to_type INTEGER NOT NULL COMMENT '接收方类型:1-用户(单聊),2-群组(群聊)',

message_type INTEGER NOT NULL COMMENT '消息类型:1-文本,2-图片附件,3-语音附件,4-视频附件,5-文件附件,6-位置,99-系统',

content TEXT NOT NULL COMMENT '消息内容',

status INTEGER NOT NULL DEFAULT 0 COMMENT '消息状态:0-发送中,1-发送成功,2-发送失败',

is_deleted INTEGER NOT NULL DEFAULT 0 COMMENT '是否删除:0-否,1-是',

send_time INTEGER NOT NULL COMMENT '发送时间戳',

created_at INTEGER NOT NULL COMMENT '创建时间',

updated_at INTEGER NOT NULL COMMENT '更新时间',

version INTEGER NOT NULL DEFAULT 0 COMMENT '数据版本号(用于乐观锁)',

sync_time INTEGER NOT NULL COMMENT '同步时间',

receive_time INTEGER NOT NULL COMMENT '接收时间(客户端特有字段)',

receive_type INTEGER NOT NULL DEFAULT 0 COMMENT '接收类型:0-增量同步 1-实时推送(客户端特有字段)'

);

  


-- 同步配置表

CREATE TABLE sync_config (

id INTEGER PRIMARY KEY AUTOINCREMENT,

key TEXT NOT NULL UNIQUE COMMENT '配置键',

value TEXT NOT NULL COMMENT '配置值',

updated_at INTEGER NOT NULL COMMENT '更新时间'

);


优缺点

优点
  1. 实时性最佳:在线时毫秒级延迟,用户体验极佳

  2. 可靠性高:离线消息通过增量同步保证不丢失

  3. 双重保障:WebSocket推送 + 增量同步,相互兜底

  4. 适应性强:同时支持在线和离线场景

  5. 状态同步完善:支持消息状态、操作记录的实时同步

缺点
  1. 实现复杂:需要同时实现WebSocket和增量同步

  2. 资源消耗大:WebSocket长连接占用服务器资源

  3. 需要去重:WebSocket推送和增量同步可能导致消息重复

  4. 断线处理复杂:需要处理断线重连和消息补齐

  5. 维护成本高:需要维护两套同步机制


代码示例

后端代码(Node.js + WebSocket + MySQL)

// WebSocket服务端实现

const WebSocket = require('ws');

const Redis = require('ioredis');

  


const wss = new WebSocket.Server({ port: 8080 });

const redis = new Redis();

  


// 存储用户连接映射

const userConnections = new Map();

  


// 生成连接ID

function generateConnectionId() {

return Snowflake.generateId();

}

  


// 生成消息ID

function generateMessageId() {

return Snowflake.generateId();

}

  


// 生成同步版本号

function generateSyncKey() {

return Snowflake.generateId();

}

  


// 处理WebSocket连接

wss.on('connection', (ws, req) => {

const connectionId = generateConnectionId();

console.log('新连接:', connectionId);

  


ws.on('message', async (message) => {

try {

const data = JSON.parse(message);

  


if (data.type === 'auth') {

// 认证

const authResult = await authenticateUser(data.token);

  


if (authResult.code === 0) {

ws.userId = authResult.user_id;

ws.deviceId = data.deviceId;

ws.connectionId = connectionId;

  


// 存储连接映射

const key = `connection:${authResult.user_id}:${data.deviceId}`;

userConnections.set(key, ws);

  


// 订阅用户的消息队列

await redis.subscribe(`user:${authResult.user_id}`);

  


// 发送认证成功响应

ws.send(JSON.stringify({

type: 'auth',

code: 0,

message: '认证成功'

}));

  


console.log('用户认证成功:', authResult.user_id);

} else {

ws.send(JSON.stringify({

type: 'auth',

code: -1,

message: '认证失败'

}));

}

} else if (data.type === 'heartbeat') {

// 心跳

ws.lastHeartbeatTime = Date.now();

ws.send(JSON.stringify({ type: 'heartbeat', code: 0 }));

}

} catch (error) {

console.error('处理消息失败:', error);

}

});

  


ws.on('close', () => {

console.log('连接断开:', connectionId);

  


// 清理连接映射

if (ws.userId && ws.deviceId) {

const key = `connection:${ws.userId}:${ws.deviceId}`;

userConnections.delete(key);

  


// 取消订阅

redis.unsubscribe(`user:${ws.userId}`);

}

});

  


ws.on('error', (error) => {

console.error('WebSocket错误:', error);

});

});

  


// 发送消息

// 消息状态定义:0-发送中,1-发送成功,2-发送失败

async function sendMessage(fromUserId, toUserId, sessionId, msgType, content) {

try {

const syncKey = generateSyncKey();

const messageId = generateMessageId();

const currentTime = Date.now();

  


// 保存消息到数据库

const query = `

INSERT INTO message_base_info (message_id, sync_key, session_id, from_user_id, to_user_id, message_type, content, status, send_time, created_at, updated_at)

VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)

`;

await db.query(query, [messageId, syncKey, sessionId, fromUserId, toUserId, msgType, content, 0, currentTime, currentTime, currentTime]);

  


const messageData = {

messageId: messageId,

syncKey: syncKey,

sessionId: sessionId,

fromUserId: fromUserId,

toUserId: toUserId,

msgType: msgType,

content: content,

status: 0,

sendTime: currentTime,

createdAt: currentTime,

updatedAt: currentTime

};

  


// 第二步:尝试WebSocket推送

let pushed = false;

for (const [key, ws] of userConnections) {

if (ws.userId === parseInt(toUserId) && ws.readyState === WebSocket.OPEN) {

try {

// 推送时更新status为1(发送成功)

messageData.status = 1;

ws.send(JSON.stringify({

type: 'message',

data: messageData

}));

pushed = true;

console.log('WebSocket推送成功:', toUserId);

} catch (error) {

console.error('WebSocket推送失败:', error);

}

}

}

  


// 第三步:根据推送结果更新消息状态

if (pushed) {

// 推送成功:status=1(发送成功)

await db.query('UPDATE message_base_info SET status=1 WHERE message_id=?', [messageId]);

console.log('消息状态更新为发送成功:', messageId);

} else {

// 推送失败,尝试存入离线队列

try {

await redis.lpush(`offline:${toUserId}`, JSON.stringify(messageData));

// 存入离线队列成功:status=1(发送成功,因为有离线队列兜底)

await db.query('UPDATE message_base_info SET status=1 WHERE message_id=?', [messageId]);

console.log('消息存入离线队列,状态更新为发送成功:', messageId);

} catch (error) {

// 离线队列也失败:status=2(发送失败)

await db.query('UPDATE message_base_info SET status=2 WHERE message_id=?', [messageId]);

console.error('离线队列存储失败,消息状态更新为发送失败:', error);

}

}

  


return {

code: 0,

data: messageData

};

} catch (error) {

console.error('发送消息失败:', error);

return {

code: -1,

message: '发送消息失败'

};

}

}

  


// 增量同步接口(用于离线消息拉取)

async function syncMessagesMixed(userId, deviceId, lastSyncKey) {

try {

const limit = 100;

  


// 查询sync_key之后的消息

const query = `

SELECT * FROM message_base_info

WHERE to_user_id = ?

AND sync_key > ?

ORDER BY sync_key ASC

LIMIT ?

`;

  


const messages = await db.query(query, [userId, lastSyncKey, limit]);

  


// 转换字段名为驼峰命名(用于返回给前端)

const formattedMessages = messages.map(msg => ({

messageId: msg.message_id,

syncKey: msg.sync_key,

sessionId: msg.session_id,

fromUserId: msg.from_user_id,

toUserId: msg.to_user_id,

toType: msg.to_type,

messageType: msg.message_type,

content: msg.content,

status: msg.status,

isDeleted: msg.is_deleted,

sendTime: msg.send_time,

createdAt: msg.created_at,

updatedAt: msg.updated_at,

version: msg.version

}));

  


// 获取最新的sync_key

let nextSyncKey = lastSyncKey;

if (messages.length > 0) {

nextSyncKey = messages[messages.length - 1].sync_key;

}

  


// 更新用户同步记录(服务端记录)

// 注意:user_sync_record是服务端用于记录用户同步状态的表,用于服务端监控和统计

// 客户端使用sync_config表来维护lastSyncKey

const currentTime = Date.now();

const updateQuery = `

INSERT INTO user_sync_record (user_id, device_id, current_sync_key, last_sync_time, last_online_time, created_at, updated_at)

VALUES (?, ?, ?, ?, ?, ?, ?)

ON DUPLICATE KEY UPDATE

current_sync_key = VALUES(current_sync_key),

last_sync_time = VALUES(last_sync_time),

last_online_time = VALUES(last_online_time),

updated_at = VALUES(updated_at)

`;

await db.query(updateQuery, [userId, deviceId, nextSyncKey, currentTime, currentTime, currentTime, currentTime]);

  


return {

code: 0,

data: {

messages: formattedMessages,

nextSyncKey: nextSyncKey,

has_more: messages.length >= limit

}

};

} catch (error) {

console.error('增量同步消息失败:', error);

return {

code: -1,

message: '增量同步消息失败'

};

}

}

前端代码(客户端)

// WebSocket客户端实现

class WebSocketClient {

constructor() {

this.ws = null;

this.userId = null;

this.deviceId = null;

this.reconnectCount = 0;

this.maxReconnectCount = 10;

this.reconnectInterval = 3000;

this.heartbeatInterval = 30000;

this.heartbeatTimer = null;

this.lastSyncKey = 0;

}

  


// 初始化

async init(userId, deviceId, token) {

this.userId = userId;

this.deviceId = deviceId;

  


// 先进行增量同步(拉取离线消息)

await this.syncIncremental(userId, deviceId);

  


// 然后建立WebSocket连接

await this.connect(userId, deviceId, token);

}

  


// 增量同步(拉取离线消息)

async syncIncremental(userId, deviceId) {

try {

// 读取本地sync_key

const config = await db.get('SELECT value FROM sync_config WHERE key = ?', ['last_sync_key']);

this.lastSyncKey = config ? parseInt(config.value) : 0;

  


// 请求增量消息

const response = await ipcRenderer.invoke('sync-messages-mixed', this.lastSyncKey, userId, deviceId);

  


if (response.code === 0) {

const { messages, nextSyncKey, has_more } = response.data;

  


if (messages.length > 0) {

// 存储离线消息

const currentTime = Date.now();

const insertPromises = messages.map(msg => {

return db.run(`

INSERT OR IGNORE INTO message_base_info (message_id, sync_key, session_id, from_user_id, to_user_id, to_type, message_type, content, status, send_time, created_at, updated_at, version, sync_time, receive_time, receive_type)

VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)

`, [msg.messageId, msg.syncKey, msg.sessionId, msg.fromUserId, msg.toUserId, msg.toType, msg.messageType, msg.content, msg.status, msg.sendTime, msg.createdAt, msg.updatedAt, msg.version, currentTime, currentTime, 0]);

});

  


await Promise.all(insertPromises);

  


// 更新sync_key

this.lastSyncKey = nextSyncKey;

await db.run(`

INSERT OR REPLACE INTO sync_config (key, value, updated_at)

VALUES ('last_sync_key', ?, ?)

`, [nextSyncKey, currentTime]);

  


console.log(`增量同步成功,新增 ${messages.length} 条离线消息`);

  


// 如果还有更多消息,继续拉取

if (hasMore) {

await this.syncIncremental(userId, deviceId);

}

} else {

console.log('暂无离线消息');

}

}

} catch (error) {

console.error('增量同步失败:', error);

}

}

  


// 建立WebSocket连接

async connect(userId, deviceId, token) {

return new Promise((resolve, reject) => {

try {

this.ws = new WebSocket(`ws://localhost:8080?token=${token}&deviceId=${deviceId}`);

  


this.ws.onopen = () => {

console.log('WebSocket连接成功');

  


// 发送认证信息

this.ws.send(JSON.stringify({

type: 'auth',

token: token,

deviceId: deviceId

}));

  


// 启动心跳

this.startHeartbeat();

  


this.reconnectCount = 0;

resolve();

};

  


this.ws.onmessage = async (event) => {

try {

const data = JSON.parse(event.data);

  


if (data.type === 'auth') {

if (data.code === 0) {

console.log('WebSocket认证成功');

} else {

reject(new Error('认证失败'));

}

} else if (data.type === 'message') {

// 处理接收到的消息

await this.handleMessage(data.data);

} else if (data.type === 'heartbeat') {

console.log('收到心跳响应');

}

} catch (error) {

console.error('处理消息失败:', error);

}

};

  


this.ws.onerror = (error) => {

console.error('WebSocket错误:', error);

reject(error);

};

  


this.ws.onclose = () => {

console.log('WebSocket连接关闭');

this.stopHeartbeat();

  


// 断线重连

if (this.reconnectCount < this.maxReconnectCount) {

this.reconnectCount++;

console.log(`尝试重连 (${this.reconnectCount}/${this.maxReconnectCount})`);

setTimeout(() => {

this.connect(userId, deviceId, token);

}, this.reconnectInterval);

}

};

} catch (error) {

console.error('连接WebSocket失败:', error);

reject(error);

}

});

}

  


// 启动心跳

startHeartbeat() {

this.stopHeartbeat();

  


this.heartbeatTimer = setInterval(() => {

if (this.ws && this.ws.readyState === WebSocket.OPEN) {

this.ws.send(JSON.stringify({ type: 'heartbeat' }));

}

}, this.heartbeatInterval);

}

  


// 停止心跳

stopHeartbeat() {

if (this.heartbeatTimer) {

clearInterval(this.heartbeatTimer);

this.heartbeatTimer = null;

}

}

  


// 处理WebSocket接收到的消息

async handleMessage(message) {

try {

console.log('收到实时消息:', message);

  


// 检查消息是否已经存在(去重)

const exists = await db.get('SELECT id FROM message_base_info WHERE message_id = ?', [message.messageId]);

  


if (!exists) {

// 存储新消息(标记为实时推送)

const currentTime = Date.now();

await db.run(`

INSERT OR IGNORE INTO message_base_info (message_id, sync_key, session_id, from_user_id, to_user_id, to_type, message_type, content, status, send_time, created_at, updated_at, version, sync_time, receive_time, receive_type)

VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)

`, [message.messageId, message.syncKey, message.sessionId, message.fromUserId, message.toUserId, message.toType, message.messageType, message.content, message.status, message.sendTime, message.createdAt, message.updatedAt, message.version || 0, currentTime, currentTime, 1]);

  


// 更新syncKey

this.lastSyncKey = message.syncKey;

await db.run(`

INSERT OR REPLACE INTO sync_config (key, value, updated_at)

VALUES ('last_sync_key', ?, ?)

`, [message.syncKey, currentTime]);

  


console.log(`实时消息存储成功,syncKey: ${message.syncKey}`);

} else {

console.log('消息已存在,跳过存储');

}

  


// 更新UI

updateUI(message);

} catch (error) {

console.error('处理实时消息失败:', error);

}

}

  


// 断开连接

disconnect() {

this.stopHeartbeat();

if (this.ws) {

this.ws.close();

this.ws = null;

}

this.reconnectCount = this.maxReconnectCount; // 停止重连

}

}






三、方案对比分析

3.1 综合对比表

对比维度增量同步(基于版本号)增量同步 + 实时推送(混合方案)
实时性⭐⭐⭐ (秒级,依赖拉取频率)⭐⭐⭐⭐⭐ (毫秒级)
网络效率⭐⭐⭐⭐⭐ (只传增量)⭐⭐⭐⭐ (推送模式 + 增量)
服务器压力⭐⭐⭐⭐ (短连接,按需请求)⭐⭐⭐ (长连接多,但客户端按需加载可减轻压力)
客户端资源⭐⭐⭐⭐⭐ (按需请求)⭐⭐⭐⭐ (持续连接 + 按会话懒加载,资源可控)
实现复杂度⭐⭐ (游标管理、去重)⭐⭐⭐⭐ (连接管理、断线重连、去重)
可靠性⭐⭐⭐⭐⭐ (严格顺序、断点续传)⭐⭐⭐⭐⭐ (双重保障)

3.2 方案特性深度对比

3.2.1 消息获取时机对比

维度增量同步(基于版本号)增量同步 + 实时推送(混合方案)
历史消息获取应用启动时全量拉取,或定时轮询首次进入会话时按需拉取,灵活高效
新消息获取定时轮询(如每30秒)或用户手动刷新WS实时推送(毫秒级) + 定时兜底
多设备同步完全依赖增量同步,需频繁轮询WS推送 + 增量同步,按需拉取更高效
离线消息上线后自动增量同步上线后自动增量同步

3.2.2 会话管理对比

维度增量同步(基于版本号)增量同步 + 实时推送(混合方案)
会话初始化全局统一管理,无需特殊处理按会话懒加载,每个会话独立维护last_sync_key
未读数更新增量同步时计算WS推送实时更新,或增量同步时计算
会话切换无需特殊处理,消息已全部加载切换会话时按需加载该会话消息
历史消息加载全量加载,内存占用大按需加载,内存占用可控

3.2.3 技术架构对比

维度增量同步(基于版本号)增量同步 + 实时推送(混合方案)
连接方式纯HTTP短连接WebSocket长连接 + HTTP短连接
推送机制客户端主动拉取(Pull模式)服务端主动推送(Push模式) + 客户端拉取(Pull模式)
消息去重增量同步时基于message_id去重WS推送和增量同步双源去重
断线处理恢复网络后自动增量同步WS断线重连 + 增量同步补齐
状态同步依赖增量同步拉取操作记录WS推送操作记录 + 增量同步兜底

3.3 场景化选型建议

3.3.1 适用增量同步(方案一)的场景

  1. 无实时性要求的IM系统
  • 邮件类应用

  • 客服系统(可接受秒级延迟)

  • 内部通知系统

  1. 弱网环境或流量敏感场景
  • 移动网络不稳定的地区

  • 对流量有严格限制的应用

  1. 离线优先的应用
  • 用户主要离线使用

  • 网络连接不稳定

  1. 资源受限的设备
  • 低配手机或嵌入式设备

  • WebSocket长连接资源消耗过大

3.3.2 适用增量同步 + 实时推送(方案二)的场景

  1. 社交聊天应用
  • 需要毫秒级实时性

  1. 企业IM系统
  • 多设备协同办公

  1. 已建立WebSocket连接的应用
  • 应用启动时已建立WS连接

  • 利用现有WS基础设施

  1. 按会话懒加载架构
  • 首次进入会话时加载历史消息

  • WS实时推送新消息

  • 多会话并发,按需加载

  1. 高并发场景
  • 海量用户同时在线

  • 需要兼顾实时性和性能


3.4 Electron桌面应用特殊考量

Electron桌面应用在消息同步方面有以下特殊性:

优势

  1. 本地数据库性能强
  • SQLite查询速度快

  • 可存储大量历史消息

  • 支持复杂查询和索引

  1. 网络稳定
  • 桌面端网络通常比移动端稳定

  • WebSocket连接更可靠

  • 断线重连更容易

  1. 资源充足
  • CPU、内存资源相对充足

  • 可支持多个会话同时加载

  • WebSocket长连接资源消耗可接受

挑战

  1. 多窗口同步
  • Electron支持多窗口

  • 多个渲染进程需要同步消息

  • 主进程需要统一管理消息分发

  1. 跨进程通信
  • 主进程 ↔ 渲染进程IPC通信

  • 消息需要在多个进程间同步

  • 需要考虑IPC通信性能

  1. 应用生命周期
  • 应用最小化/最大化

  • 系统休眠/唤醒

  • 需要处理各种状态变化

建议

Electron应用推荐使用方案二(增量同步 + 实时推送),理由:

  1. 桌面端网络稳定:WebSocket长连接更可靠,实时性优势明显

  2. 资源充足:可支持多个会话同时加载和WebSocket连接

  3. 按会话懒加载:结合WS实时推送,既保证实时性又控制内存占用

  4. 多窗口场景:WS统一在主进程管理,多个渲染进程共享消息推送

具体实现要点:

  • 主进程维护WebSocket连接,统一接收服务端推送

  • 每个渲染进程按会话懒加载历史消息

  • 主进程收到WS消息后,IPC广播给所有相关渲染进程

  • 每个会话独立维护last_sync_key_${sessionId}

  • 渲染进程首次加载会话时增量同步历史消息






四、总结与建议

4.1 核心总结

IM消息同步是一个系统工程,业界主流方案主要有两种:

  1. 增量同步方案(基于版本号):这是最基础、最可靠的同步方案,通过严格递增的序列号保证消息顺序和完整性,适合作为同步的基础设施。该方案无需维护WebSocket连接,实现简单,但实时性依赖客户端轮询频率。

  2. 增量同步 + 实时推送(混合方案):这是业界最主流、最完善的方案,在线时通过WebSocket实现毫秒级实时性,离线时通过增量同步补齐消息,兼顾实时性和可靠性。该方案特别适合已建立WebSocket连接、按会话懒加载消息的场景。

核心原则

  • 实时性 vs 可靠性:实时推送提供最佳体验但实现复杂,增量同步更可靠但实时性不足

  • 网络适应性:不同网络环境下需要不同的同步策略

  • 多设备一致性:消息状态在多设备间的同步是技术难点

  • 按需加载:懒加载架构下,首次进入会话时加载历史消息,实时消息通过推送接收

  • 没有银弹:每种同步策略都有其适用场景和局限性


4.2 实施建议

技术选型建议

  1. 协议层面
  • 实时推送:WebSocket(主流选择)或gRPC(高性能场景)

  • 增量同步:HTTP/2 + RESTful API(兼容性好)

  1. 数据存储
  • 消息存储:MySQL/PostgreSQL(关系型,事务支持)

  • 实时状态:Redis(内存缓存,快速读写)

  • 离线队列:消息队列(RabbitMQ/Kafka)

  1. 序列号生成
  • 使用雪花算法等分布式ID生成器

  • 保证严格递增和全局唯一

  • 预留足够的位数,避免溢出

  • 支持消息和操作的序列号关联

  1. 客户端框架
  • Web端:原生WebSocket + IndexedDB

  • 移动端:平台特定推送 + 本地数据库(SQLite/Core Data)

  • 桌面端:Electron + SQLite

性能优化要点

  1. 连接管理:连接池、心跳优化、断线快速重连

  2. 数据传输:消息压缩、协议优化、批量传输

  3. 存储优化:数据库索引、读写分离、缓存策略

  4. 客户端优化:本地缓存、懒加载、虚拟滚动

  5. 去重机制:基于message_id去重,避免重复消息

  6. 按需加载:按会话懒加载历史消息,减少初始流量和内存占用

关键技术点

  1. 序列号设计:严格递增,全局唯一,支持消息操作(撤回、删除)

  2. 消息去重:客户端通过message_id判断消息是否已存在

  3. 断线重连:WebSocket断线后自动重连,重连后进行增量同步

  4. ACK机制:客户端收到消息后发送确认,保证可靠性

  5. 心跳维持:定时发送心跳包,检测连接状态

  6. 兜底机制:定时增量同步作为兜底,防止消息丢失

  7. 会话懒加载:首次进入会话时按需加载历史消息,每个会话独立维护last_sync_key

  8. 多窗口同步:Electron多窗口场景下,主进程统一管理WebSocket,渲染进程按需拉取消息


4.3 特殊场景处理建议

4.3.1 会话懒加载场景

当采用按会话懒加载架构时(如Electron应用):

  1. 同步游标设计
  • 使用last_sync_key_${sessionId}为每个会话维护独立游标

  • 首次进入会话时,若不存在则初始化为0(触发全量拉取)

  1. 消息获取策略
  • 历史消息:首次进入会话时,通过增量同步接口拉取历史消息

  • 实时消息:WebSocket推送实时消息,根据会话加载状态决定是否展示

  1. 未读消息处理
  • 会话未打开时:仅更新会话列表的未读数,不存储消息详情

  • 会话打开后:实时消息直接插入消息列表并展示

  1. WebSocket与增量同步协同
  • WebSocket:负责实时推送,保持长连接

  • 增量同步:负责历史消息加载和断线补齐

4.3.2 多窗口同步场景(Electron特有)

  1. 主进程职责
  • 维护WebSocket连接

  • 接收服务端推送

  • 通过IPC广播给所有渲染进程

  1. 渲染进程职责
  • 按会话懒加载历史消息

  • 监听IPC消息接收实时推送

  • 根据当前打开的会话决定是否展示消息

  1. 消息分发策略
  • 主进程收到消息后,判断哪些渲染进程打开了对应会话

  • 仅向相关渲染进程IPC推送消息

  1. 本地数据库共享
  • 主进程统一管理本地数据库

  • 渲染进程通过IPC读写数据

4.3.3 断线重连场景

  1. 断线检测
  • WebSocket心跳超时

  • 网络状态变化监听

  1. 重连策略
  • 指数退避算法,避免频繁重连

  • 最大重连次数限制

  1. 重连后补齐
  • 重连成功后,对每个打开的会话进行增量同步

  • 补齐断线期间的消息

  • 更新会话的last_sync_key

  1. 用户感知优化
  • 断线时显示连接状态提示

  • 重连成功后自动刷新消息






版权声明

本文档内容为原创技术文档,仅供学习交流使用。文档中的代码示例、架构设计等技术内容为通用技术实践,不涉及任何特定公司的商业机密。如需引用本文档内容,请注明出处。