消息序号如何保证即时通讯源码聊天记录稳定加载
复盘编号IM-HISTORY-07涉及范围单聊、群聊、文件消息、红包记录、引用回复、消息撤回核心问题历史消息重复、分页跳过、撤回状态不同步技术环境Spring Boot、MySQL 5.7、Redis、JSON即时通讯系统中的历史消息查询最初通常只是一个普通分页接口。客户端提交会话编号和页码服务端从 MySQL 中查询消息再按时间倒序返回。文本、图片和表情数量不多时这种方式基本能够满足使用。随着群聊记录逐渐增加文件、红包、个人名片、自定义表情、引用回复和系统通知同时进入消息表传统分页开始出现一些不易察觉的异常。本次复盘只讨论一件事即时通讯源码中的历史消息应该如何稳定地向前加载并保证撤回、引用和消息漫游结果一致。09:20 聊天记录出现重复内容测试过程中发现用户连续向上加载群聊历史记录时偶尔会看到同一条消息出现两次。接口使用的是常见分页语句SELECT * FROM im_message WHERE conversation_id ? ORDER BY create_time DESC LIMIT 20 OFFSET 40;第一次读取第 1 页后群聊中又产生了几条新消息。此时数据库中的消息位置发生变化。客户端继续请求第 2 页原本排在第 20 条附近的数据被新消息向后挤动部分记录便会再次进入查询范围。问题不在于 SQL 没有排序而在于页码依赖的是动态位置。消息表持续插入数据时“第 2 页”并不是一个稳定的数据区间。09:45 时间字段不能作为唯一游标将页码分页改成时间分页后重复数量有所减少但没有完全消失。接口开始携带上一页最早消息的创建时间beforeTime2026-07-18 09:42:16对应查询条件是create_time beforeTime这种写法仍然存在边界问题。同一秒内可能写入多条群消息。如果数据库时间精度不足或者多条消息拥有相同的create_time使用严格小于条件会跳过部分记录使用小于等于又会重复返回上一页最后一条。因此历史消息不能只依赖时间排序还需要一个在会话内稳定递增的消息序号。10:10 为每个会话增加消息序号改造后的消息表保留全局消息编号同时增加message_seq。CREATE TABLE im_message ( id BIGINT NOT NULL PRIMARY KEY, conversation_id VARCHAR(64) NOT NULL, message_seq BIGINT NOT NULL, sender_id BIGINT NOT NULL, message_type VARCHAR(32) NOT NULL, message_state VARCHAR(16) NOT NULL, content JSON NOT NULL, quote_message_id BIGINT DEFAULT NULL, create_time DATETIME NOT NULL, UNIQUE KEY uk_conversation_seq ( conversation_id, message_seq ), KEY idx_history_query ( conversation_id, message_seq ) );id用于全局定位一条消息。message_seq只负责当前会话内部的顺序。例如一个群聊中的记录可以是群聊90018 消息序号 1021 消息序号 1022 消息序号 1023另一个单聊也可以拥有自己的序号单聊10018与10036 消息序号 81 消息序号 82 消息序号 83客户端不需要理解全局消息编号的生成方式只需记录当前页面中最小的message_seq。10:40 历史消息接口改为游标分页首次进入聊天页面时客户端不传游标服务端从当前会话最新消息开始读取。继续向前加载时客户端把上一批数据中最小的消息序号传回来beforeSeq1021Spring Boot 查询逻辑可以写成Service RequiredArgsConstructor public class HistoryMessageService { private final ImMessageRepository messageRepository; public HistoryPage query( String conversationId, Long beforeSeq, int pageSize ) { int limit Math.min(Math.max(pageSize, 1), 50); ListImMessage messages; if (beforeSeq null) { messages messageRepository .findLatest(conversationId, limit); } else { messages messageRepository .findBeforeSeq(conversationId, beforeSeq, limit); } Long nextCursor messages.isEmpty() ? null : messages.get(messages.size() - 1).getMessageSeq(); return new HistoryPage( messages, nextCursor, messages.size() limit ); } }对应 SQL 的核心条件是conversation_id 当前会话 message_seq beforeSeq排序固定为ORDER BY message_seq DESC无论查询过程中是否有新消息进入会话中序号小于beforeSeq的范围都不会改变。11:15 撤回消息不能从历史记录中直接消失游标分页稳定后又出现了另一个现象。用户引用了一条文件消息随后发送者撤回原消息。部分设备重新加载历史记录时引用区域仍然正常但原消息已经完全查不到聊天时间线中出现了断层。最初的撤回实现执行了物理删除DELETE FROM im_message WHERE id ?这种处理会带来三个问题消息序号中间出现空缺引用关系失去目标已经加载原消息的页面与重新查询的页面显示不同。撤回操作应修改消息状态而不是删除记录。message_state REVOKED历史消息接口仍然返回这条数据但不再返回原始正文而是转换为撤回占位消息{ messageId: 78263196, messageSeq: 1023, messageType: SYSTEM, messageState: REVOKED, content: { event: MESSAGE_REVOKED, operatorId: 10018 } }这样消息序号保持连续聊天记录位置也不会突然消失。引用回复可以根据业务规则显示原摘要或者显示“引用内容已撤回”但引用消息自身无需被删除。11:50 文件和红包历史记录只保存业务引用历史消息接口并不适合返回文件二进制内容也不应直接计算红包余额变化。文件消息写入消息表时只保存文件资源引用{ fileId: file_981726, fileName: 项目排期.xlsx, fileSize: 152617, fileType: xlsx }客户端读取历史消息后再根据文件编号获取当前资源状态。文件可能处于AVAILABLE EXPIRED DELETED即使文件已经过期历史记录仍然可以保留文件名称和大小只是不再提供下载能力。红包消息也采用类似方式{ redPacketId: rp_202607180016, title: 恭喜发财 }红包是否已领取、是否领完以及是否退款由红包业务表维护。历史消息只负责保留当时发送过红包这一事实。这种分离方式可以避免每次查询聊天记录时都对钱包表执行复杂关联。12:20 收藏记录不能依赖原消息长期存在用户收藏一条文本、文件或合并转发消息时如果收藏表只保存message_id原消息撤回或群聊解散后收藏页面可能无法继续展示内容。收藏记录更适合保存一份有限快照收藏用户 原消息编号 消息类型 展示摘要 文件名称 资源编号 收藏时间这里的快照不是复制完整聊天记录而是保留收藏列表需要的必要内容。例如文件消息被撤回后聊天窗口显示撤回提示但用户之前收藏的文件记录仍可按照既定规则保留文件名称。历史消息、收藏记录和消息撤回因此形成三套不同的数据关系历史消息保存会话事实 收藏记录保存用户快照 撤回状态控制会话展示三者不能简单共用一个删除标记。13:00 群成员可见起点需要进入查询条件新成员加入群聊后是否能够读取入群前的消息需要由群聊规则决定。如果群聊不允许查看历史内容可以在成员关系表中记录加入时的消息序号join_message_seq 1056查询历史消息时服务端增加可见范围限制message_seq join_message_seq最终条件变为conversation_id 当前群聊 message_seq beforeSeq message_seq joinMessageSeq即使客户端手动修改beforeSeq也不能读取加入群聊之前的数据。成员退出群聊后是否还能查看旧记录也应根据成员状态和退出序号判断而不是只看客户端是否保存了会话编号。13:35 Redis缓存必须携带游标边界历史消息可以使用 Redis 缓存最近一段记录但缓存键不能只包含会话编号。错误的缓存方式是im:history:group:90018因为不同用户请求的游标和可见起点可能不同。可以缓存会话最近的固定消息区间例如最新 100 条再由服务端根据message_seq和成员可见范围筛选。也可以把游标放入缓存键im:history:group:90018:before:1056:size:20不过这种方式容易产生大量离散缓存一般只适合访问集中的短期数据。更常见的处理是最近消息使用Redis 较早消息查询MySQL 成员权限始终重新校验 撤回状态变更后删除相关缓存缓存不能作为历史消息的最终数据来源。14:10 消息类型变化不应修改分页协议历史记录中可能同时出现文本 图片 语音 视频 文件 红包 个人名片 群名片 自定义表情 位置 通话记录 会议邀请 系统通知分页接口不需要为每种消息类型增加一套参数。所有消息都使用统一外层结构messageId messageSeq messageType messageState senderId content createTime客户端根据messageType决定展示方式。新增一种消息类型时只需要扩展消息内容解析和展示组件历史查询的游标规则不发生变化。这也是消息外层结构保持稳定的重要原因。15:00 上线前验证记录本次改造没有继续使用页码也没有将创建时间作为单一分页条件。验证过程覆盖以下情况验证场景预期结果加载历史期间收到新消息已加载记录不重复多条消息创建时间相同不跳过任何记录最早消息被撤回保留原序号位置引用消息被撤回引用消息仍可展示文件资源已经过期保留文件历史摘要红包已经领完聊天记录仍然存在新成员禁止查看旧消息无法越过入群序号连续请求同一游标返回相同消息区间历史记录不足一页返回结束标记Redis缓存失效可以从MySQL重新查询接口返回结果中增加nextCursor和hasMore不再返回总页数。即时通讯消息会持续增加计算总页数本身没有稳定意义。客户端只需知道下一次从哪个消息序号继续读取以及前面是否还有记录。页面交互逻辑拆解记录宠友(IM即时通讯)app,支持语音、文件、图片、视频等多种类型消息的发送,安全可靠,交流轻松,私有化部署,快速开发极简部署支持群聊管理、语音视频通话...功能https://chongyou.info/1/product/im.html历史消息链路最终保持为验证会话访问权限 → 确定成员可见起点 → 读取beforeSeq游标 → 按messageSeq倒序查询 → 转换撤回与失效状态 → 返回nextCursor

相关新闻

刑事案件会见的必要性

刑事案件会见的必要性

当家属遭遇刑事案件时,往往会陷入迷茫与焦虑之中。此时,律师会见成为了连接家属与在押当事人的重要桥梁。下面,我们就来详细了解一下刑事案件律师会见的完整流程以及律师会见的核心必要性。刑事案件律师会见完整流程前置委托环节家属在决定委…

2026/10/19 3:37:56 阅读更多 →
21.多输入和输出通道

21.多输入和输出通道

每个通道都有一个卷积核 输入通道:channel: cic_ici​ 图像大小为(nh,nw)(n_h,n_w)(nh​,nw​),通道数cic_ici​,对应的卷积核大小(kh,kw)(k_h,k_w)(kh​,kw​),通道数cic_ici​,但是卷积核不是同一个,是通过学习得到。 输出图像为单通道(mh,mw)(m_h,m_w)

2026/10/16 8:39:33 阅读更多 →
Kimi-K3低调开放API,百万上下文强化编程能力,参数规模登顶国产第一

Kimi-K3低调开放API,百万上下文强化编程能力,参数规模登顶国产第一

没有预热海报,没有官方发布会通稿。7月16日晚间,月之暗面新一代旗舰大模型 Kimi K3 悄然开放API调用入口与Kimi‑Code通道。 截至目前,品牌官方公众号、社交账号均未发布正式官宣,完整技术白皮书、权威基准跑分数据暂未对外…

2026/10/19 22:45:52 阅读更多 →

最新新闻

Canvas截取视频首帧作为封面:前端零成本方案与实战避坑

Canvas截取视频首帧作为封面:前端零成本方案与实战避坑

1. 为什么视频封面首帧截取是个高频刚需做过内容平台或者带视频功能的项目的人都知道&#xff0c;视频列表页如果直接放一个<video>标签去加载&#xff0c;页面性能会崩得很难看。几十个视频同时开始加载元数据和首帧&#xff0c;带宽和内存直接爆炸&#xff0c;尤其在移…

2026/10/20 21:34:09 阅读更多 →
Linux权限管理指南:普通用户与root的授权、应急恢复与安全实践

Linux权限管理指南:普通用户与root的授权、应急恢复与安全实践

去年有一次我在测试环境里手滑&#xff0c;用root执行了一条带rm -rf的清理命令&#xff0c;变量没展开&#xff0c;差点把整个/usr目录送走。机器倒是救回来了&#xff0c;但从那以后我给自己立了条规矩&#xff1a;能用普通用户干的活&#xff0c;绝不用root&#xff1b;必须…

2026/10/20 21:34:09 阅读更多 →
Agent工具工程:从调用到契约的可靠性设计

Agent工具工程:从调用到契约的可靠性设计

1. 这不是“AI工具课”&#xff0c;而是一场对工程思维的重新校准“哥伦比亚大学 Agentic Engineering 第 5 讲&#xff1a;221 分钟讲透 Agent 工具”——这个标题刚出现在某技术社区时&#xff0c;我第一反应是点开前先关掉所有通知。不是因为不感兴趣&#xff0c;恰恰相反&a…

2026/10/20 21:34:09 阅读更多 →
RNN十年演进:从LSTM到Transformer再到线性注意力回归

RNN十年演进:从LSTM到Transformer再到线性注意力回归

做序列建模这些年&#xff0c;我是眼看着RNN从主角变成配角&#xff0c;又在最近两年换了一副面孔回到舞台中央的。2014年那会儿&#xff0c;我在一个文本分类项目里用单层LSTM调参&#xff0c;调参基本靠猜&#xff0c;训练基本靠忍&#xff0c;一个模型跑一个晚上是家常便饭。…

2026/10/20 21:34:09 阅读更多 →
MATLAB牛耕法全覆盖路径规划:有障碍物时的区域分解与绕行

MATLAB牛耕法全覆盖路径规划:有障碍物时的区域分解与绕行

简介&#xff1a;本资源面向机器人路径规划初学者与MATLAB仿真开发者&#xff0c;聚焦全覆盖算法中牛耕法在障碍物环境下的实现与回退处理&#xff0c;帮助读者理解如何让作业设备在复杂区域内完成无遗漏覆盖。包内共3个文件&#xff0c;均为m脚本&#xff0c;压缩包约2KB&…

2026/10/20 21:34:09 阅读更多 →
AI平台选型指南:从通用对话到开源自部署的实战策略

AI平台选型指南:从通用对话到开源自部署的实战策略

1. 主流AI平台的全景地图与选型逻辑1.1 为什么现在聊“AI平台”比聊“AI模型”更实际过去一年我接触了不少团队&#xff0c;从几个人的小工作室到上百人的产品线&#xff0c;大家问得最多的问题早就不是“哪个模型最强”&#xff0c;而是“我该把活儿交给哪个平台”。这个转变很…

2026/10/20 21:33:02 阅读更多 →

日新闻

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →
ESP32舵机控制指南:原理、多路驱动与避坑实战

ESP32舵机控制指南:原理、多路驱动与避坑实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →
位移法实现一阶低通滤波:单片机ADC采样降噪实战

位移法实现一阶低通滤波:单片机ADC采样降噪实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型&#xff08;GMM&#xff09;的语音识别系统&#xff0c;很多人第一反应就是一堆数学公式&#xff0c;觉得门槛很高。但如果你真的在MATLAB里把它落地跑通&#xff0c;会发现核心其实就是三件事&#xff1a;把声音变成特征向量&#xff0c;用GMM给每个类别…

2026/10/20 10:02:13 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人&#xff0c;但落到具体操作上&#xff0c;无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程&#xff1a;对图像做反白、改调色板、彩色转灰度&#xff0c;最后把像素数据导出成txt文件。单看每一…

2026/10/20 20:43:59 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词&#xff0c;你会发现一个很有意思的现象&#xff1a;每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型&#xff0c;有人指的是运动控制系统&#xff0c;还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/20 2:50:50 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 10:02:08 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:17 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:45 阅读更多 →