消息系统中的「已读/未读」功能,虽然看似简单,但在高并发、大规模分布式系统中涉及到存储设计、性能优化、数据一致性、延迟与实时性权衡等诸多技术点。下面我从系统设计角度进行完整拆解。
✅ 一、场景定义与目标
1. 应用场景
-
IM系统(如微信、钉钉、飞书)
-
评论通知(如微博、知乎)
-
系统消息/站内信
-
电商站内通知
2. 核心目标
-
支持查看消息是否已读
-
支持未读消息计数
-
高并发、高性能
-
可拓展性好,支持海量消息(千万级别)
✅ 二、数据模型设计
模型一:按用户维度记录未读消息
✅ 表结构示例:
# 消息主表(公共)
Message(id, sender_id, content, type, send_time)
# 用户收件表(每人一条记录)
User_Message(user_id, message_id, is_read, read_time)
-
is_read
:标记是否已读 -
message_id
:指向消息主体,节省存储 -
每用户一份收件副本,便于个性化状态维护
✅ 三、未读消息计数的处理方式
方式一:数据库实时查询(不推荐)
SELECT COUNT(*) FROM user_message WHERE user_id = ? AND is_read = false;
-
缺点:效率差,随着数据增长性能骤降
✅ 方式二:缓存未读计数(推荐)
-
使用 Redis 存储用户维度的未读计数
HINCRBY unread_count:{user_id} {type} 1
HGET unread_count:{user_id} comment
-
消息写入时同步更新未读计数
-
消息阅读后(或批量设为已读)异步减少计数
✅ 四、已读状态更新策略
✅ 方式一:逐条标记为已读(粒度高)
UPDATE user_message SET is_read = true, read_time = NOW()
WHERE user_id = ? AND message_id = ?
✅ 方式二:批量已读(优化性能)
UPDATE user_message SET is_read = true, read_time = NOW()
WHERE user_id = ? AND is_read = false AND send_time < ?
-
对于 列表下滑加载全部视为已读,或 点击“全部已读” 也适用
-
批量操作建议走异步消息(MQ)或延迟任务处理
✅ 五、优化点与扩展设计
1. 使用 Redis Bitmap 表示已读(稀疏消息场景)
-
每个用户一份 Bitmap,
offset = message_id
-
查询快,占内存小,但不支持稀疏离散 message_id 过大场景
2. 将未读消息 ID 单独记录在 Redis Set/List
-
unread:{user_id}
→ Set(message_id) -
拉取列表时与总消息交集过滤,或快速定位未读消息
3. 分布式场景下同步一致性
-
未读计数更新建议采用异步方式(MQ 机制)
-
操作幂等处理,避免并发刷读/回滚引起脏数据
✅ 六、关键接口设计(简要)
接口 | 描述 |
---|---|
GET /messages | 拉取消息列表 |
GET /messages/unread | 获取未读计数 |
POST /message/{id}/read | 标记某条消息为已读 |
POST /message/read-all | 标记所有消息为已读 |
✅ 七、压测&挑战
场景 | 应对方案 |
---|---|
消息表千万级别 | 表分区、索引优化、冷热分离 |
大用户拉取未读消息或状态 | Redis缓存、分页加载、预聚合 |
多端同时操作已读状态 | 幂等操作、乐观锁或最终一致重试机制 |
多维度消息统计(按类型/级别) | Redis Hash 存储,定期与 DB 进行双写校准 |
✅ 八、最终架构简化图
[Web/APP客户端]
↓
[API Gateway]
↓
[消息服务] --(缓存命中)--> [Redis]
↓
[消息中心DB] <--异步补偿-- [MQ]