消息写入必须用事务包裹,否则会丢消息;分表应按 conversation_id 而非 user_id;时间戳须用 bigint 存毫秒 unix 时间;查询排序必须用 created_at 字段并建联合索引 (conversation_id, created_at)。

消息写入必须用事务包裹,否则会丢消息
MySQL 或 PostgreSQL 中单条消息插入看似简单,但实际聊天场景常伴随「已读回执更新」「会话最后消息刷新」「未读数累加」等关联操作。任一环节失败都会导致状态不一致。INSERT INTO messages 单独执行不是错,但一旦业务逻辑扩展就立刻踩坑。
实操建议:
- 用
db.Begin()显式开启事务,所有相关INSERT/UPDATE在同一事务内完成 - 对高频写入的群聊消息,避免在事务中做耗时操作(如调用外部 HTTP 接口)
- PostgreSQL 用户可考虑
INSERT ... ON CONFLICT DO NOTHING防重,但注意它不触发RETURNING,若需返回自增 ID 得改用UPSERT+SELECT组合
分表策略要按 conversation_id 而非 user_id
按 user_id 分表会导致单聊消息散落在两个分表(A→B 存 A 表,B→A 存 B 表),查历史记录时得并发查两表,还容易漏掉对方发来的消息。按 conversation_id(比如 "u123_u456" 或 "g789")分片,能保证一个会话的所有消息物理聚集。
实操建议:
- 生成
conversation_id时统一排序:对单聊,取min(user_id, target_id) + "_" + max(user_id, target_id) - 分表键别用哈希后取模——哈希分布难预测,运维查问题时无法快速定位表名;推荐直接用
conversation_id % 64这类可算出的规则 - 不要等数据量大了再分表;从第一版就定好分表逻辑,后续加字段、改索引才不会被绑定到某张物理表
时间戳字段必须用 BIGINT 存毫秒 Unix 时间,别用 DATETIME
MySQL 的 DATETIME 类型精度只到秒,且时区处理混乱;PostgreSQL 的 TIMESTAMP WITH TIME ZONE 看似友好,但 Go 的 time.Time 序列化进 JSON 默认带时区,前端解析易出错。更关键的是:消息顺序依赖精确到毫秒甚至微秒的时间戳,尤其在高并发刷屏场景。
实操建议:
- 数据库字段类型固定为
BIGINT,Go 结构体里对应int64,存time.Now().UnixMilli() - 写入前校验:如果
msg.CreatedAt是零值或明显异常(如小于 1609459200000,即 2021-01-01),直接拒绝,不给脏数据入库机会 - 查询排序必须用该字段:
ORDER BY created_at DESC LIMIT 20,别依赖主键自增顺序——主键只保证插入序,不等于发送序
索引设计要覆盖「查最新 N 条」和「查某时间点之后」两种模式
用户上拉加载更多,是查 created_at ;下拉刷新,是查 <code>created_at > ? ORDER BY created_at ASC LIMIT 20。单一索引很难同时高效支持这两种方向相反的范围查询。
实操建议:
- 建联合索引:
(conversation_id, created_at)—— 覆盖会话隔离 + 时间排序 - 如果存在大量“仅查未读消息”需求,可额外建
(conversation_id, is_read, created_at),但is_read区分度低,需确认 MySQL 版本是否支持索引条件下推(8.0.13+ 更稳) - 避免在
created_at上单独建索引:没conversation_id过滤,等于全表扫描
最常被忽略的一点:消息表的 conversation_id 字段长度要和实际值匹配。用 VARCHAR(64) 存 "u123_u456" 没问题,但如果将来支持企业微信 ID(含 @ 符号和域名),就得提前留足空间,否则截断后分表逻辑彻底失效。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











