存储选型:MySQL、Redis、时序库与对象存储

IM 的数据至少有五种形态:会在 90s 后消失的在线态、需要强一致的点查索引、需要按会话顺序扫描的消息、几百 KB 起步的媒体文件、以及只用于观察的指标与埋点。把它们塞进同一个存储是最常见的早期错误。本章给出一张分层表与每层的容量口径,让选型有据可依。

分层结论先给

数据存储主键/键设计生命周期容量口径(以 4 亿条消息/日为例)
在线态与路由Redisroute:{uid}:{device} 带 TTL 90s90s300 万连接 × 约 300B ≈ 0.9 GB
未读数与已读位点Redis + MySQLunread:{uid} Hash;MySQL 为权威热 7 天只缓存活跃会话,全量放 Redis 会到几十 GB
会话与成员索引MySQLim_conversation、im_conv_member长期1000 万会话 × 约 500B ≈ 5 GB
消息正文MySQL 分表 + 归档id(雪花号)或 (conv_id, seq)热 7 天 / 温 180 天500B/条 → 200 GB/日,180 天约 36 TB
媒体文件对象存储 + CDNmedia/{uid}/{hash}按业务,常 1 年图片 500 万张 × 200KB = 1 TB/日
指标与埋点时序库(Prometheus、ClickHouse)时间 + 标签15 天 / 1 年见 25 章的日志量口径
全文检索可选 ES倒排索引90 天只索引近 3 个月,冷数据不检索

一条判断原则:Redis 是加速层,MySQL 是权威层,对象存储是沉淀层。任何"Redis 写成功就算成功"的路径,最终都会以丢消息收场。

选型对比

存储擅长的形态不擅长的形态在 IM 中的角色
MySQL强事务、主键点查、按 seq 范围扫描、成本可控单表上亿行、超大字段、全文检索消息与关系索引的权威存储
Redis亚毫秒 KV、计数、TTL、排行榜、分布式锁作为主存保证不丢、超大 value路由、在线态、未读数、热点缓存
MongoDB / HBase / Cassandra宽表、海量写入、按分区键顺序读事务、复杂查询、小团队运维超大集群的消息归档替代方案
时序库高写入的指标与埋点、时间窗口聚合按 ID 点查、事务更新监控与运营分析
对象存储大文件、CDN 分发、按量计费频繁改写、低延迟小对象、原子计数图片、语音、文件、导出包

不建议"消息同时写 MySQL 和 MongoDB 双主存":两份权威就有两套一致性问题,收益远小于代价。要换存储就整体替换权威层,而不是叠加。

消息表结构

消息表按会话分片(23 章展开),单表控制在 5000 万行以内。字段设计的重点是"够用且不留长字段":

CREATE TABLE im_message_0000 (
  id          BIGINT UNSIGNED NOT NULL COMMENT '雪花 ID,全局唯一且趋势递增',
  conv_id     BIGINT UNSIGNED NOT NULL,
  seq         BIGINT UNSIGNED NOT NULL COMMENT '会话内自增序号,保证有序',
  sender_uid  BIGINT UNSIGNED NOT NULL,
  msg_type    TINYINT NOT NULL COMMENT '1文本 2图片 3语音 4文件 5撤回 6系统',
  content     VARBINARY(2048) NOT NULL COMMENT '文本正文或媒体引用 JSON',
  client_msg_id VARCHAR(64) NOT NULL DEFAULT '' COMMENT '客户端幂等键',
  send_time   DATETIME(3) NOT NULL,
  PRIMARY KEY (id),
  UNIQUE KEY uk_conv_seq (conv_id, seq),
  UNIQUE KEY uk_sender_cmid (sender_uid, client_msg_id, send_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

要点:正文用 VARBINARY 而非 TEXT(避免字符集转换的开销并便于加解密);大字段一律外移,图片语音只存 media_id,否则单行膨胀会把 Buffer Pool 打穿;uk_conv_seq 同时承担顺序查询与幂等去重两个职责。

Redis 键设计与写入路径

Redis 只做加速,但键设计稍有不慎它就会变成故障放大器:

route:{uid}:{device}   String   gateway_id|conn_id,TTL 90s(心跳续期)
unread:{uid}           Hash     field=conv_id,value=未读数,TTL 7d
conv:seq:{conv_id}     String   会话最大 seq,用于发号,MySQL 兜底纠正
msg:recent:{conv_id}   List     最近 200 条消息 ID,LPUSH 后 LTRIM 裁剪
member:{conv_id}       Set      群成员 uid 集合,仅缓存千人以内的群

三条规则:所有键必须带 TTL(仅候选名单这类小集合例外)、单 value 控制在 10 KB 以内、大 key 巡检纳入日常(定期跑 redis-cli --bigkeys 与 --hotkeys)。发号用 INCR conv:seq:{conv_id} 保证会话内严格递增,但必须由 MySQL 兜底:Redis 重启丢号会让 seq 回退,因此每次发号后异步落 im_conv_seq,异常时以 MySQL 最大值 + 1000 为起点重建——宁可跳号,不可重号。

写入路径与一致性

一条消息的写入顺序固定,不能颠倒:

1. 校验:成员、频控、内容安全(见 24 章)
2. 取号:Redis INCR conv:seq:{conv_id}
3. 写 MySQL 分片表,uk_conv_seq 保证幂等;失败直接返回错误,不 ACK
4. 更新 im_conv_seq 与 im_conv_member.updated_at(同库事务或最终一致)
5. 刷新 Redis 热点缓存,失败只记日志,缓存缺失可回源
6. 异步动作:MQ 扇出、离线队列、推送任务(见 21 章)

ACK 只发生在第 3 步之后,第 4~6 步全部是可补偿的异步动作。这条边界决定了 Redis 或 MQ 抖动时用户还能不能正常发消息,也是"消息不丢"承诺在存储层的具体落点。

冷热分层与生命周期

热(0~7 天)   MySQL 分片 + Redis 缓存最近 N 条(LRU,按会话)
温(7~180 天) MySQL 归档分片(只读副本)+ 按月分表
冷(>180 天)  对象存储 Parquet/ORC 文件,按 conv_id 分区,查询走离线任务回表

落地要点:归档任务按 (conv_id, seq) 区间做,单批不超过 5000 行并限速,避免归档把线上 IOPS 抽干;归档前先写对象存储并校验行数,成功后再删除热数据,任何一步失败都保留热数据重试。

容量与成本估算口径

单条消息磁盘占用 = 正文 120B + 元数据 160B + 索引开销 220B ≈ 500B
日增原始数据 = 4 亿条 × 500B = 200 GB
副本系数 2.5(一主两从去掉一份冷备 + binlog 增量)
实际磁盘需求 = 200 GB × 180 天 × 2.5 ≈ 90 TB(不含归档冷存)

媒体:图片 500 万张/日 × 200KB = 1 TB/日
      语音 100 万条/日 × 60KB = 60 GB/日
      对象存储按 1 年保留 ≈ 380 TB,转低频存储可降本 40% 以上

Redis 内存 = 连接态 0.9 GB + 未读数(仅活跃会话)约 8 GB + 热点消息缓存 10 GB
             → 单实例 32 GB 足够,按 3 主 3 从部署

三条约束:单库不超过 300 GB(备份与恢复时间可控)、单表不超过 5000 万行(B+ 树深度与 DDL 时间可控)、单个 Redis value 不超过 10 KB(避免大 key 阻塞)。

常见坑

  • 用 Redis List 存整个会话的消息:百万条的大 key,一次阻塞就是全站抖动;Redis 只缓存最近 N 条。
  • 消息先写 Redis 再异步落 MySQL:宕机即丢消息,违反"消息不丢"的第一承诺。
  • 媒体文件与消息不在一个事务里:先传文件后发消息,失败就产生孤儿文件。做法是定时扫描未被任何消息引用的 media_id 并延迟 24 小时清理。
  • 对象存储 URL 直接入库:CDN 域名或桶迁移时全库失效,库里只存 object_key,URL 由服务端实时签发并带鉴权与过期时间。
  • 主键用随机 UUID:写入时页分裂与随机 IO 放大,用雪花号或有序 ID。
  • 未定期做恢复演练:备份"存在"不等于"能恢复",按季度做一次全量恢复演练(28 章的容灾部分)。

小结:IM 存储按形态分层——Redis 承担在线态与路由等短命数据,MySQL 承担会话索引与消息正文这类权威数据,对象存储沉淀媒体文件,时序库承接指标与埋点;选型的硬指标是单库 300 GB、单表 5000 万行、单 value 10 KB 三条上限,成本口径统一为"单条 500B × 日增量 × 保留天数 × 副本系数 2.5";所有加速层都必须能接受清空后从权威层重建。

笔记加载中…