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)都必须支持无限水平扩容以应对未来增长。
相关推荐
咩咩啃树皮3 小时前
第40篇:Vue3组件化开发精讲——组件拆分、复用、父子通信、工程化架构
java·前端·架构
鱟鲥鳚3 小时前
Spring Boot 集成 LangChain4j:从模型调用到 Tool Calling(Demo版)
java·spring boot
大模型码小白5 小时前
【Python零基础教程】继承、多态与魔法函数:面向对象编程三大核心特性详解
java·大数据·开发语言·人工智能·python·ai编程
腾渊信息科技公司5 小时前
Spring Boot对接MES实战:视觉检测数据自动同步方案
java·人工智能·spring boot·后端·计算机视觉·ai·软件需求
爱笑的源码基地6 小时前
高并发 Redis 缓存门诊HIS系统源码,含财务统计药房进销存
java·程序·门诊系统·诊所系统·云诊所源码
wuqingshun3141596 小时前
TCP超时重传机制是为了解决什么问题?
java
莫逸风9 小时前
【AgentScope 2.0】 0. 学习指南
java·llm·agent·agentscope
隔窗听雨眠9 小时前
Spring Boot在云原生时代的编程范式革新研究
spring boot·后端·云原生
z123456789869 小时前
2026最新两款AI编程工具深度对比实测
java·数据库·ai编程
yaoxin52112310 小时前
470. Java 反射 - Member 接口与 AccessFlag
java·开发语言·python