架构思维:构建高并发读服务_热点数据查询的架构设计与性能调优

文章目录

  • 一、引言
  • 二、热点查询定义与场景
  • 三、主从复制——垂直扩容
  • 四、应用内前置缓存
    • 4.1 容量上限与淘汰策略
    • 4.2 延迟刷新:定期 vs. 实时
    • 4.3 逃逸流量控制
    • 4.4 热点发现:被动 vs. 主动
  • 五、降级与限流兜底
  • 六、前端/接入层其他应对
  • 七、模拟压测Code
  • 八、总结

在这里插入图片描述

一、引言

在前面的几篇博客中,

架构思维:使用懒加载架构实现高性能读服务

架构思维:利用全量缓存架构构建毫秒级的读服务

架构思维:异构数据的同步一致性方案

我们依赖全量缓存与 Binlog 同步,实现了零毛刺、百毫秒级延迟的读服务,且分布式部署后轻松承载百万 QPS。但这里的“百万”是指 百万不同用户 的并发请求。

百万请求属于不同用户的架构图

在这里插入图片描述

假设一个节点最大能够支撑 10W QPS,我们只需要在集群中部署 10 台节点即可支持百万流量。


当这百万请求都集中到同一用户时(即同一键热点查询),传统集群方案将不堪重负——单节点路由所有流量,硬件和程序性能都无法无限扩容。

当百万 QPS 都属于同一用户时,即使缓存是集群化的,同一个用户的请求都会被路由至集群中的某一个节点

百万请求属于相同用户的架构图

在这里插入图片描述

即使此节点的机器配置非常好,当前能够支持住百万 QPS。但随着流量上涨,它也无法满足未来的流量诉求。原因有 2 点:

  • 单台机器无法无限升级;

  • 缓存程序本身也是有性能上限的。

这里我们讲将聚焦**单用户百万并发(热点查询)**场景,剖析架构瓶颈并给出多层次应对策略。


二、热点查询定义与场景

热点查询:对同一数据项发生极高频率的重复读取。

典型场景:

  • 社交媒体热点内容(某条微博、热搜话题)。
  • 电商秒杀商品详情持续刷新。
  • 网站排行榜页/直播间房间信息。

这些场景下,同一个 Key 的请求会被路由到同一缓存分片或服务实例,导致“集中过载”。

在这里插入图片描述


三、主从复制——垂直扩容

主从复制应对热点的架构图

在这里插入图片描述

思路:利用缓存的主从复制机制,开启 随机读 策略,让同一 Key 的并发请求分散到主+多个从节点。

在查询时,将应用内的缓存客户端开启主从随机读。此时,包含一个从的分片的并发能力,可以提升至原来的一倍。随着从节点的增加,单分片的并发性能会不断翻倍。这对于所有请求只会命中某一个固定单分片的热点查询能够很好地应对。

但此方案存在一个较大的问题,就是浪费资源。

主从复制除了有应对热点的功能,另外一个主要作用是为了高可用。当集群中的某一个主节点发生故障后,集群高可用模块会自动对该节点进行故障迁移,从该节点所属分片里选举一个从节点为主节点。为了高可用模块在故障转移时的逻辑能够简单清晰并做到统一,会将集群的从节点数量设置为相同数量。

相同从节点数量也带来了较大的资源浪费。为了应对热点查询,需要不断扩容从分片。但热点查询只会命中其中一个分片,这就导致所有其他分片的从节点全部浪费了。为了节约资源,可以对高可用模块进行改造,不强制所有分片的从节点必须相同,但这个代价也是非常高昂的。另外,热点查询很多时候是随时出现的,并不能提前预测,所以提前扩容某一个分片意义并不大。

  • 优点:实现简单,只需客户端配置;
  • 扩展:每加一个从节点,分片吞吐线性提升;
  • 缺点资源浪费——为热点单一分片预留大量从节点,而其他分片则闲置;
  • 改造:可自定义高可用中间件配置,按需增减分片,从而降低浪费。

总的来说,主从复制能够解决一定流量的热点查询且实施起来较简单。但不具备扩展性,在应对更大流量的热点时会有些吃力


四、应用内前置缓存

热点查询特点:请求次数多、数据项少。可在业务应用进程内维护一份本地缓存,彻底分散热流量。

在这里插入图片描述

4.1 容量上限与淘汰策略

  • 设置缓存上限,采用 LRU 淘汰最少访问条目;
  • 配合 TTL,过期未访问的数据自动释放。

4.2 延迟刷新:定期 vs. 实时

  • 定期刷新:设置过期时间后,按周期批量更新,简单高效;
  • 实时刷新:借助异构判断层+MQ,主动推送变更,仅针对热点更新,需额外组件支持。

在这里插入图片描述


4.3 逃逸流量控制

再者要把控好瞬间的逃逸流量

应用初始化时,前置缓存是空的。假设在初始化时,瞬间出现热点查询,所有的热点请求都会逃逸到后端缓存里。可能这个瞬间热点就会把后端缓存打挂。

其次,如果前置缓存采用定期过期,在过期时若将数据清理掉,那么所有的请求都会逃逸至后端加载最新的缓存,也有可能把后端缓存打挂。这两种情况对应的流程图如下图所示:
在这里插入图片描述

对于这两种情况,可以对逃逸流量进行前置等待或使用历史数据的方案。不管是初始化还是数据过期,在从后端加载数据时,只允许一个请求逃逸。这样最大的逃逸流量为部署的应用总数,量级可控。架构如下图 所示
在这里插入图片描述

  • 初始化或过期瞬间,只允许 1 个 请求穿透后端加载并更新本地缓存;
  • 其他并发请求 等待(带超时)或 返回历史脏数据,防止短时洪峰打挂后端。

4.4 热点发现:被动 vs. 主动

除了需要应对热点缓存,另外一个重点就是如何发现热点缓存。对于发现热点有两个方式,一种是被动发现,另外一种是主动发现。

  • 被动:凭借本地缓存容量和 LRU 策略,自然淘汰非热点,热点常驻;

    被动发现是借助前置缓存有容量上限实现的。在被动发现的方案里,读服务接受到的所有请求都会默认从前置缓存中获取数据,如不存在,则从缓存服务器进行加载。因为前置缓存的容量淘汰策略是 LRU,如果数据是热点,它的访问次数一定非常高,因此它一定会在前置缓存中。借助前置缓存的容量上限和淘汰策略,即实现了热点发现。

    但此方式也存在一个问题——所有的请求都优先从前置缓存获取数据,并在未查询到时加载服务端数据到本地的前置缓存里,此方式也会把非热点数据存储至前置缓存里,导致非热点数据产生非必要的延迟性。

  • 主动:在缓存层或接入层统计访问频次,超过阈值后 推送 到本地缓存,避免误入冷数据。

    主动发现则需要借助一些外部计数工具来实现热点的发现。外部计数工具的思路大体比较类似,都是在一个集中的位置对于请求进行计数,并根据配置的阈值判断某请求是否会命中数据。对于判定为热点的数据,主动的推送至应用内的前置缓存即可。

下图为在缓存服务器进行计数的架构方案:
> 占位图:图 8 — 主动发现架构

采用主动发现的架构后,读服务接受到请求后仍然会默认的从前置缓存获取数据,如获取到即直接返回。如未获取到,会穿透去查询后端缓存的数据并直接返回。但穿透获取到的数据并不会写入本地前置缓存。数据是否为热点且是否要写入前置缓存,均由计数工具来决定。此方案很好地解决了因误判断带来的延迟问题。


五、降级与限流兜底

在采用了前置缓存并解决了上述四大类问题之后,当再次遇到百万级并发时,基本没什么疑难问题了。但这里还存在一个前置条件,即当热点查询发生时,你所部署的容器数量所能支撑的 QPS 要大于热点查询的 QPS。

但实际情况并非如此,所部署的机器能够支持的 QPS 未必都能够大于当次的热点查询。对于可能出现的超预期流量,可以使用前置限流的策略进行应对。在系统上线前,对于开启了前置缓存的应用进行压测,得到单机最大的 QPS。根据压测值设置单机的限流阈值,阈值可以设置为压测值的一半或者更低。设置为压测阈值的一半或更低,是因为压测时应用 CPU 基本已达到 100%,为了保证线上应用能够正常运转,是不能让 CPU 达到 100% 的

> 占位图:图 9 — 前置限流架构

即便前置缓存和后端扩展俱全,也难保偶发超预期洪峰不至于打满资源。

  • 前置限流:根据单机压测 QPS 设定阈值(如 50% 负载),超过即返回降级策略或失败;
  • 降级内容:可返回缓存的历史值、静态页面或友好提示。

六、前端/接入层其他应对

在网络边界亦可做热点缓解:

  • Nginx/接入层缓存:对热点 Key 做短时缓存;
  • CDN/边缘缓存:将热点内容推至离用户更近节点;
  • 浏览器缓存:HTTP Cache-Control 头设置合理 TTL。

七、模拟压测Code

 import com.github.benmanes.caffeine.cache.LoadingCache;
import com.github.benmanes.caffeine.cache.Caffeine;import java.util.concurrent.TimeUnit;/*** FrontCacheBenchmark.java* 示例:基于 Java + Caffeine 实现前置缓存的性能对比*/
public class FrontCacheBenchmark {// 模拟后端数据源加载private static String loadFromBackend(String key) {try {// 模拟网络/数据库延迟TimeUnit.MILLISECONDS.sleep(10);} catch (InterruptedException e) {Thread.currentThread().interrupt();}return "Value-for-" + key;}public static void main(String[] args) {final int WARM_UP = 1_000;final int TEST_OPS = 100_000;final String HOT_KEY = "hot-item";// 1. 构建 Caffeine LoadingCache,自动加载逻辑LoadingCache<String, String> cache = Caffeine.newBuilder().maximumSize(10_000).expireAfterWrite(5, TimeUnit.MINUTES).build(FrontCacheBenchmark::loadFromBackend);// 2. 预热缓存for (int i = 0; i < WARM_UP; i++) {cache.get(HOT_KEY);}// 3. 测试缓存命中性能long startHits = System.nanoTime();for (int i = 0; i < TEST_OPS; i++) {cache.get(HOT_KEY);}long endHits = System.nanoTime();// 4. 测试直接后端加载性能long startMisses = System.nanoTime();for (int i = 0; i < TEST_OPS; i++) {loadFromBackend(HOT_KEY);}long endMisses = System.nanoTime();double avgHitLatencyMs   = (endHits - startHits) / 1e6 / TEST_OPS;double avgMissLatencyMs  = (endMisses - startMisses) / 1e6 / TEST_OPS;double throughputWithCache  = TEST_OPS / ((endHits - startHits) / 1e9);double throughputWithout   = TEST_OPS / ((endMisses - startMisses) / 1e9);System.out.println("=== Performance Comparison ===");System.out.printf("Operation       | Avg Latency (ms) | Throughput (ops/sec)%n");System.out.printf("----------------+------------------+--------------------%n");System.out.printf("Cache Hit       |   %8.6f      |  %10.0f%n", avgHitLatencyMs, throughputWithCache);System.out.printf("Backend Direct  |   %8.6f      |  %10.0f%n", avgMissLatencyMs, throughputWithout);}
}

八、总结

  • 单一用户百万 QPS 需额外考虑热点路由压力;
  • 主从复制 简单易行但资源浪费;
  • 应用内前置缓存 从四大维度(容量、刷新、逃逸、发现)精细打磨;
  • 限流降级 是最后的安全阀;
  • 前端与接入层也可层层加固。

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/78646.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

宝塔面板运行docker的jenkins

1.在宝塔面板装docker&#xff0c;以及jenkins 2.ip:端口访问jenkins 3.获取密钥&#xff08;点击日志&#xff09; 4.配置容器内的jdk和maven环境&#xff08;直接把jdk和maven文件夹放到jenkins容器映射的data文件下&#xff09; 点击容器-->管理-->数据存储卷--.把相…

C语言 ——— 函数

目录 函数是什么 库函数 学习使用 strcpy 库函数 自定义函数 写一个函数能找出两个整数中的最大值 写一个函数交换两个整型变量的内容 牛刀小试 写一个函数判断一个整数是否是素数 写一个函数判断某一年是否是闰年 写一个函数&#xff0c;实现一个整型有序数组的二分…

笔记本电脑升级计划(2017———2025)

ThinkPad T470 (2017) vs ThinkBook 16 (2025) 完整性能对比报告 一、核心硬件性能对比 1. CPU性能对比&#xff08;i5-7200U vs Ultra9-285H&#xff09; 参数i5-7200U (2017)Ultra9-285H (2025)提升百分比核心架构2核4线程 (Skylake)16核16线程 (6P8E2LPE)700%核心数制程工…

具身系列——PPO算法实现CartPole游戏(强化学习)

完整代码参考&#xff1a; https://gitee.com/chencib/ailib/blob/master/rl/ppo_cartpole.py 执行结果&#xff1a; 部分训练得分&#xff1a; (sd) D:\Dev\traditional_nn\feiai\test\rl>python ppo_cartpole_v2_succeed.py Ep: 0 | Reward: 23.0 | Running: 2…

Python项目源码60:电影院选票系统1.0(tkinter)

1.功能特点&#xff1a;通常选票系统应该允许用户选择电影、场次、座位&#xff0c;然后显示总价和生成票据。好的&#xff0c;我得先规划一下界面布局。 首先&#xff0c;应该有一个电影选择的列表&#xff0c;可能用下拉菜单Combobox来实现。然后场次时间&#xff0c;可能用…

【全队项目】智能学术海报生成系统PosterGenius--图片布局生成模型LayoutPrompt(2)

&#x1f308; 个人主页&#xff1a;十二月的猫-CSDN博客 &#x1f525; 系列专栏&#xff1a; &#x1f3c0;大模型实战训练营_十二月的猫的博客-CSDN博客 &#x1f4aa;&#x1f3fb; 十二月的寒冬阻挡不了春天的脚步&#xff0c;十二点的黑夜遮蔽不住黎明的曙光 目录 1. 前…

Linux的时间同步服务器(附加详细实验案例)

一、计时方式的发展 1.古代计时方式​ 公元前约 2000 年&#xff1a;古埃及人利用光线留下的影子计时&#xff0c;他们修建高耸的大型方尖碑&#xff0c;通过追踪方尖碑影子的移动判断时间&#xff0c;这是早期利用自然现象计时的典型方式 。​商朝时期&#xff1a;人们开发并…

【无需docker】mac本地部署dify

环境安装准备 #安装 postgresql13 brew install postgresql13 #使用zsh的在全局添加postgresql命令集 echo export PATH"/usr/local/opt/postgresql13/bin:$PATH" >> ~/.zshrc # 使得zsh的配置修改生效 source ~/.zshrc # 启动postgresql brew services star…

(5)概述 QT 的元对象系统里的类的调用与联系,及访问接口

&#xff08;1&#xff09; QT 的元对象系统&#xff0c;这几个字大家都知道&#xff0c;那么 QT 的元对象系统里都包含哪些内容呢&#xff0c;其访问接口是如何呢&#xff1f; 从 QObject 类的实现里&#xff0c;从其数据成员里就可以看出来&#xff1a; QT 里父容器可以释放其…

打包 Python 项目为 Windows 可执行文件:高效部署指南

Hypackpy 是一款由白月黑羽开发的 Python 项目打包工具&#xff0c;它与 PyInstaller 等传统工具不同&#xff0c;通过直接打包解释器环境和项目代码&#xff0c;并允许开发者修改配置文件以排除不需要的内容&#xff0c;从而创建方便用户一键运行的可执行程序。以下是使用 Hyp…

MySQL JOIN详解:掌握数据关联的核心技能

一、为什么需要JOIN&#xff1f; 在关系型数据库中&#xff0c;数据通常被拆分到不同的表中以提高存储效率。当我们需要从多个表中组合数据时&#xff0c;JOIN操作就成为了最关键的技能。通过本文&#xff0c;您将全面掌握MySQL中7种JOIN操作&#xff0c;并学会如何在实际场景中…

Kdump 收集器及使用方式

以下是 Linux 系统中 Kdump 转储收集器的详细说明及其使用方法&#xff0c;涵盖核心工具、配置方法及实际示例&#xff1a; 一、Kdump 收集器分类及作用 Kdump 的核心功能是通过 捕获内核 生成内存转储文件&#xff08;vmcore&#xff09;&#xff0c;其核心收集器包括&#…

Error: error:0308010C:digital envelope routines::unsupported 高版本node启动低版本项目运行报错

我的问题就是高版本node启动旧版本项目引起的问题&#xff0c;单独在配置 package.json文件中配置并运行就可以&#xff0c;大概意思就是设置node的openssl "scripts": {"dev": "SET NODE_OPTIONS--openssl-legacy-provider && vue-cli-servi…

松下机器人快速入门指南(2025年更新版)

松下机器人快速入门指南&#xff08;2025年更新版&#xff09; 松下机器人以其高精度、稳定性和易用性在工业自动化领域广泛应用。本文将从硬件配置、参数设置、手动操作、编程基础到维护保养&#xff0c;全面讲解松下机器人的快速入门方法&#xff0c;帮助新手快速掌握核心操…

【CISCO】Se2/0, Se3/0:串行口(Serial) 这里串口的2/0 和 3/0分别都是什么?

在 Cisco IOS 设备上&#xff0c;接口名称通常遵循这样一个格式&#xff1a; <类型><槽号>/<端口号>类型&#xff08;Type&#xff09;&#xff1a;表示接口的物理或逻辑类型&#xff0c;比如 Serial&#xff08;串行&#xff09;、FastEthernet、GigabitEt…

开源无人机地面站QGroundControl安卓界面美化与逻辑优化实战

QGroundControl作为开源无人机地面站软件,其安卓客户端界面美化与逻辑优化是提升用户体验的重要工程。 通过Qt框架的界面重构和代码逻辑优化,可以实现视觉升级与性能提升的双重目标。本文将系统讲解QGC安卓客户端的二次开发全流程,包括开发环境搭建、界面视觉升级、多分辨率…

基于DDPG的自动驾驶小车绕圈任务

1.任务介绍 任务来源: DQN: Deep Q Learning &#xff5c;自动驾驶入门&#xff08;&#xff1f;&#xff09; &#xff5c;算法与实现 任务原始代码: self-driving car 在上一篇使用了DQN算法完成自动驾驶小车绕圈任务之后&#xff0c;学习了DDPG算法&#xf…

缓存置换:用c++实现最近最少使用(LRU)算法

在计算机的世界里&#xff0c;缓存就像一个“快速仓库”&#xff0c;它存储着我们频繁访问的数据&#xff0c;大大提升了数据的读取速度。但这个 “仓库” 空间有限&#xff0c;当它被装满时&#xff0c;就得决定舍弃一些数据&#xff0c;为新数据腾出位置&#xff0c;这个决策…

【YOLO11改进】改进Conv、颈部网络STFEN、以及引入PIOU用于小目标检测!

改进后的整体网络架构 改进一:RFD模块(Conv) YOLOv11模型的跨步卷积下采样虽然快速聚合了局部特征,并且实现了较高的计算效率,但其固有的信息压缩机制会导致细粒度特征的不可逆丢失。针对特征保留与计算效率的平衡问题,本文采用RFD模块替换跨步卷积下采样模块。RFD模块通…

设计模式每日硬核训练 Day 18:备忘录模式(Memento Pattern)完整讲解与实战应用

&#x1f504; 回顾 Day 17&#xff1a;中介者模式小结 在 Day 17 中&#xff0c;我们学习了中介者模式&#xff08;Mediator Pattern&#xff09;&#xff1a; 用一个中介者集中管理对象之间的通信。降低对象之间的耦合&#xff0c;适用于聊天系统、GUI 控件联动、塔台调度等…