60万QPS下如何设计未读数系统

为支撑60万QPS(每秒查询请求) 的未读数系统,需要设计一个分层、异步、最终一致 的高可用架构。核心是将计算与存储分离,用内存扛住实时流量,用异步任务保证数据落地

下面是一个经过大规模实践验证的架构方案与关键设计。

🏗️ 核心架构设计

整个系统分为四层,各司其职:

复制代码
[客户端] --> [接入层: Nginx/LVS] --> [缓存层: Redis集群] --> [存储层: MySQL分库分表]
                                      ↑
[计算层: 异步Worker] ------------- [消息队列: Kafka/RocketMQ]

🔑 五大关键技术策略与实现

1. 两级缓存策略:抗住60万QPS查询

这是应对高并发读的核心,用两级缓存确保99.9%的请求不穿透到数据库。

层级 存储内容 数据结构 过期/更新策略 QPS能力
本地缓存 (L1, 客户端/服务端) 用户维度的未读数摘要(如总未读数) ConcurrentHashMapCaffeine 短时间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节点。
  • MySQL16-32个分库分表节点。
  • 应用服务器20-30台(4核8G配置),通过负载均衡提供服务。

🎯 总结:核心设计原则

  1. 查询为王 :一切设计优先保障读性能 ,60万QPS下平均响应时间应 <5ms
  2. 异步解耦 :写入必须异步化,通过消息队列+批量聚合将数据库写入降低1-2个数量级。
  3. 最终一致 :接受秒级延迟,通过对账补偿确保数据最终准确。
  4. 水平扩展 :每层(缓存、MQ、DB)都必须支持无限水平扩容以应对未来增长。
相关推荐
踢足球09291 小时前
寒假打卡:2026-2-7
java·开发语言·javascript
闻哥1 小时前
Kafka高吞吐量核心揭秘:四大技术架构深度解析
java·jvm·面试·kafka·rabbitmq·springboot
金牌归来发现妻女流落街头1 小时前
【Springboot基础开发】
java·spring boot·后端
考琪1 小时前
Nginx打印变量到log方法
java·运维·nginx
wangjialelele1 小时前
Linux中的进程管理
java·linux·服务器·c语言·c++·个人开发
历程里程碑2 小时前
普通数组----轮转数组
java·数据结构·c++·算法·spring·leetcode·eclipse
晔子yy2 小时前
如何设计让你的程序同时处理10w条数据
java
Yvonne爱编码2 小时前
链表高频 6 题精讲 | 从入门到熟练掌握链表操作
java·数据结构·链表
lpfasd1232 小时前
物联网后端岗位java面试题
java·物联网·php
毕设源码李师姐2 小时前
计算机毕设 java 基于 java 的图书馆借阅系统 智能图书馆借阅综合管理平台 基于 Java 的图书借阅与信息管理系统
java·开发语言·课程设计