即时通讯系统作为现代互联网基础设施的重要组成部分,其消息存储服务承担着数据持久化的关键职责。在微服务架构下,消息存储子服务需要满足高并发写入、低延迟读取、数据可靠性等核心需求。根据实际业务场景统计,一个中等规模的即时通讯平台日均消息量可达数千万条,峰值QPS超过5000,这对存储系统的设计提出了严峻挑战。
我曾在多个即时通讯项目中负责消息存储模块的架构设计,发现传统单体架构下的消息存储方案(如直接使用MySQL存储)在消息量超过千万级时会出现明显的性能瓶颈。微服务化改造后,消息存储服务需要独立承担以下职责:
针对消息数据的特性(写多读少、强时效性),我们对比了三种主流方案:
| 方案 | 写入性能 | 读取性能 | 扩展性 | 适用场景 |
|---|---|---|---|---|
| MySQL分库分表 | 中等 | 中等 | 复杂 | 强事务需求场景 |
| MongoDB集群 | 高 | 高 | 易 | 灵活Schema需求 |
| Redis+持久化 | 极高 | 极高 | 中等 | 高热数据缓存场景 |
最终选择MongoDB作为主存储引擎,主要基于以下考量:
采用复合分片键解决热点问题:
javascript复制sh.shardCollection("im.messages",
{ conversationId: 1, timestamp: -1 }
)
这种设计使得:
架构示意图:
code复制客户端 → API网关 → 写入服务 → MongoDB分片集群
↓
Redis缓存集群
↑
读取服务 ← 消息同步服务
关键设计点:
MongoDB文档结构示例:
json复制{
"_id": ObjectId("5f3d7e1c6a1b2c3d4e5f6g7"),
"conversationId": "group_123456",
"senderId": "user_789",
"content": {
"text": "明天会议改到下午三点",
"attachments": [
{"type": "file", "url": "https://.../meeting.doc"}
]
},
"status": {
"readBy": ["user_123", "user_456"],
"recalled": false
},
"createdAt": ISODate("2023-08-20T09:30:00Z"),
"expireAt": ISODate("2023-09-20T09:30:00Z"),
"version": 3
}
关键索引设计:
javascript复制// 主查询索引
db.messages.createIndex({
conversationId: 1,
createdAt: -1
})
// TTL自动清理索引
db.messages.createIndex(
{ expireAt: 1 },
{ expireAfterSeconds: 0 }
)
// 用户消息索引
db.messages.createIndex({
"status.readBy": 1,
createdAt: -1
})
批量写入接口设计:
java复制public CompletableFuture<Void> batchInsert(
List<Message> messages,
DurabilityLevel level) {
// 根据持久化级别选择写入策略
WriteConcern concern = level == DurabilityLevel.HIGH ?
WriteConcern.MAJORITY :
WriteConcern.UNACKNOWLEDGED;
return mongoTemplate.insertAll(messages)
.withWriteConcern(concern)
.toFuture()
.thenRun(() -> {
// 异步更新缓存
redisTemplate.opsForStream()
.add(messages.stream()
.map(this::toCacheEntry)
.toArray());
});
}
性能优化技巧:
多级缓存策略实现:
python复制def get_message_history(conversation_id, since=None, limit=100):
# 第一层:本地内存缓存
cache_key = f"history:{conversation_id}:{since}"
if result := local_cache.get(cache_key):
return result
# 第二层:Redis缓存
if result := redis.zrevrangebyscore(
f"msg:{conversation_id}",
min=since or "+inf",
max="-inf",
start=0,
num=limit
):
local_cache.set(cache_key, result, ttl=60)
return result
# 第三层:MongoDB查询
query = {"conversationId": conversation_id}
if since:
query["createdAt"] = {"$gte": since}
result = list(mongo.messages
.find(query)
.sort("createdAt", -1)
.limit(limit))
# 异步回填缓存
pipeline = redis.pipeline()
for msg in result:
pipeline.zadd(
f"msg:{conversation_id}",
{msg["_id"]: msg["createdAt"].timestamp()}
)
pipeline.expire(f"msg:{conversation_id}", 86400)
pipeline.execute()
local_cache.set(cache_key, result, ttl=60)
return result
采用分层测试方案:
单元测试:覆盖核心业务逻辑
集成测试:验证组件协作
gherkin复制Scenario: 新消息存储流程
Given 发送者A与接收者B的会话存在
When A发送文本消息"Hello"
Then 消息应出现在:
- Redis实时缓存中
- MongoDB持久化存储中
- 消息同步队列中
And 消息状态标记为"未读"
压力测试:使用JMeter模拟以下场景:
关键监控看板包含:
Prometheus配置示例:
yaml复制- name: message_storage
rules:
- record: write_latency_p99
expr: histogram_quantile(0.99, sum(rate(mongo_write_duration_seconds_bucket[1m])) by (le))
- record: cache_hit_ratio
expr: redis_cache_hits / (redis_cache_hits + redis_cache_misses)
定期执行故障注入测试:
现象:某个MongoDB分片CPU持续100%
排查步骤:
javascript复制db.messages.getShardDistribution()
javascript复制sh.shardCollection("im.messages", {
"conversationId": "hashed"
})
现象:Redis集群CPU飙升,MongoDB查询超时
根因:大量历史消息同时过期导致缓存击穿
解决方案:
python复制def get_cache_ttl(msg):
base_ttl = 3600 * 24 # 1天基础有效期
if msg['conversationId'].startswith('group_'):
return base_ttl + random.randint(0, 3600) # 增加随机抖动
return base_ttl
现象:移动端显示消息顺序与发送顺序不一致
排查过程:
java复制@Document
public class Message {
@Field(targetType = FieldType.DATE_TIME)
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss'Z'", timezone = "UTC")
private Date createdAt;
}
经过3个迭代周期的调优,关键指标提升如下:
| 指标项 | 优化前 | 优化后 | 提升幅度 |
|---|---|---|---|
| 写入吞吐量 | 2,300 msg/s | 15,000 msg/s | 552% |
| 读取延迟(P99) | 320ms | 89ms | 72%↓ |
| 存储成本 | $1.2/万条 | $0.3/万条 | 75%↓ |
| 故障恢复时间 | 15分钟 | 2分钟 | 87%↓ |
具体优化手段:
在实际运行中,我们还在持续改进以下方面:
智能冷热数据分离
边缘缓存方案
消息搜索增强
存储成本优化