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)都必须支持无限水平扩容以应对未来增长。
相关推荐
毕设源码-朱学姐4 小时前
【开题答辩全过程】以 基于JavaWeb的网上家具商城设计与实现为例,包含答辩的问题和答案
java
C雨后彩虹6 小时前
CAS与其他并发方案的对比及面试常见问题
java·面试·cas·同步·异步·
java1234_小锋7 小时前
Java高频面试题:SpringBoot为什么要禁止循环依赖?
java·开发语言·面试
2501_944525547 小时前
Flutter for OpenHarmony 个人理财管理App实战 - 账户详情页面
android·java·开发语言·前端·javascript·flutter
计算机学姐7 小时前
基于SpringBoot的电影点评交流平台【协同过滤推荐算法+数据可视化统计】
java·vue.js·spring boot·spring·信息可视化·echarts·推荐算法
Filotimo_7 小时前
Tomcat的概念
java·tomcat
索荣荣8 小时前
Java Session 全面指南:原理、应用与实践(含 Spring Boot 实战)
java·spring boot·后端
Amumu121388 小时前
Vue Router(二)
java·前端
念越9 小时前
数据结构:栈堆
java·开发语言·数据结构