为支撑60万QPS(每秒查询请求) 的未读数系统,需要设计一个分层、异步、最终一致 的高可用架构。核心是将计算与存储分离,用内存扛住实时流量,用异步任务保证数据落地。
下面是一个经过大规模实践验证的架构方案与关键设计。
🏗️ 核心架构设计
整个系统分为四层,各司其职:
[客户端] --> [接入层: Nginx/LVS] --> [缓存层: Redis集群] --> [存储层: MySQL分库分表]
↑
[计算层: 异步Worker] ------------- [消息队列: Kafka/RocketMQ]
🔑 五大关键技术策略与实现
1. 两级缓存策略:抗住60万QPS查询
这是应对高并发读的核心,用两级缓存确保99.9%的请求不穿透到数据库。
| 层级 | 存储内容 | 数据结构 | 过期/更新策略 | QPS能力 |
|---|---|---|---|---|
| 本地缓存 (L1, 客户端/服务端) | 用户维度的未读数摘要(如总未读数) | ConcurrentHashMap 或 Caffeine |
短时间TTL(如3-5秒),或监听Redis广播更新 | 单机可达百万 |
| 分布式缓存 (L2, Redis集群) | 所有用户所有维度的实时未读数(如@、评论、赞) | String (单用户聚合值) Hash (用户分维度值) Bitmap(海量用户单场景) | 不设TTL,异步写回 。更新时直接INCRBY/SET |
集群可水平扩展 |
示例:Redis数据结构设计
java
// 1. String - 存储用户聚合未读数 (Key: unread:user:{uid})
redis.set("unread:user:123456", "15");
// 2. Hash - 存储用户分维度未读数 (Key: unread:detail:{uid})
redis.hset("unread:detail:123456", "at", "5");
redis.hset("unread:detail:123456", "comment", "8");
redis.hset("unread:detail:123456", "like", "2");
// 3. Bitmap - 存储超大规模用户群的某个二元状态 (Key: unread:msg:{mid})
// 每个bit代表一个用户是否已读某条消息,适合全站公告类
redis.setbit("unread:msg:888888", 123456, 1); // 用户123456已读消息888888
2. 异步写与批量合并:解决60万QPS写入
写入路径必须与读取路径解耦,通过消息队列进行流量削峰和批量处理。
java
// 发送端:业务方只发消息,不直接写库
public void onUserLiked(long userId, long targetId) {
UnreadMessage msg = new UnreadMessage();
msg.setType("like");
msg.setUserId(userId);
msg.setTargetId(targetId);
// 发送至消息队列,毫秒级完成
kafkaTemplate.send("unread_topic", msg);
}
// 消费端:Worker批量聚合处理
@KafkaListener(topics = "unread_topic")
public void batchProcess(List<UnreadMessage> messages) {
Map<Long, Map<String, Integer>> userUnreadMap = new HashMap<>();
// 1. 在内存中按用户和类型聚合
for (UnreadMessage msg : messages) {
userUnreadMap.computeIfAbsent(msg.getUserId(), k -> new HashMap<>())
.merge(msg.getType(), 1, Integer::sum);
}
// 2. 批量更新Redis (Pipeline提升10倍性能)
try (Pipeline pipeline = jedis.pipelined()) {
for (Map.Entry<Long, Map<String, Integer>> userEntry : userUnreadMap.entrySet()) {
String redisKey = "unread:detail:" + userEntry.getKey();
for (Map.Entry<String, Integer> typeEntry : userEntry.getValue().entrySet()) {
// HINCRBY是原子操作,并发安全
pipeline.hincrBy(redisKey, typeEntry.getKey(), typeEntry.getValue());
}
}
pipeline.sync(); // 批量执行
}
// 3. 异步刷盘到MySQL(可另起线程或任务)
flushToDatabase(userUnreadMap);
}
3. 分库分表与数据归档:海量数据存储
当日增量超千万时,MySQL需按用户ID分库分表。
sql
-- 按用户ID哈希分表,例如1024张表
CREATE TABLE unread_detail_1023 (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL,
type VARCHAR(20) NOT NULL,
unread_count INT DEFAULT 0,
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_user_type (user_id, type)
);
数据生命周期管理:定期(如每月)将已读/清零的冷数据归档到HBase或对象存储,避免MySQL膨胀。
4. 最终一致性保障:数据不丢不重
这是异步系统的核心挑战,通过以下机制保证:
- 消息队列持久化:确保消息不丢。
- 消费者幂等处理:基于消息ID去重。
- 对账与补偿任务:定期扫描Redis与MySQL差异并修复。
java
@Component
public class UnreadReconcileJob {
@Scheduled(cron = "0 5 */1 * * ?") // 每小时一次对账
public void reconcile() {
// 1. 分片扫描用户(避免单次扫描数据量过大)
List<Long> userIds = getUserIdsByShard(shardId, batchSize);
for (Long userId : userIds) {
// 2. 对比Redis和DB数据
Map<String, String> redisData = jedis.hgetAll("unread:detail:" + userId);
Map<String, Integer> dbData = getFromDatabase(userId);
// 3. 发现不一致则修复(以Redis为基准)
if (!isConsistent(redisData, dbData)) {
fixDatabase(userId, redisData);
}
}
}
}
5. 容灾与降级:保障高可用
- 多机房部署:Redis集群跨机房主从,防止单点故障。
- 降级策略 :
- 读降级:Redis故障时,降级到从本地缓存或直接读DB(返回稍旧数据)。
- 写降级:MQ故障时,降级到同步写本地文件,待MQ恢复后补发。
- 监控告警:对QPS、延迟、Redis内存、MQ积压等设置阈值告警。
💰 资源配置估算(示例)
为支撑60万QPS,典型资源配置如下:
- Redis集群 :至少8-12个主节点(每个处理5-8万QPS),内存根据用户量估算(如1亿用户 * 500字节 ≈ 50GB)。
- 消息队列 :Kafka集群,6-8个Broker节点。
- MySQL :16-32个分库分表节点。
- 应用服务器 :20-30台(4核8G配置),通过负载均衡提供服务。
🎯 总结:核心设计原则
- 查询为王 :一切设计优先保障读性能 ,60万QPS下平均响应时间应 <5ms。
- 异步解耦 :写入必须异步化,通过消息队列+批量聚合将数据库写入降低1-2个数量级。
- 最终一致 :接受秒级延迟,通过对账补偿确保数据最终准确。
- 水平扩展 :每层(缓存、MQ、DB)都必须支持无限水平扩容以应对未来增长。