
做后端这些年每年最紧张的其实不是双十一那种提前一个月备战的大促反而是跨年这一秒。2026年跨年我们内部就有一个代号叫“26跨年查询场景处理”的项目。跨年查询这个事表面看就是用户零点刷新页面实际上背后是一场“时间型热点读”的极端压力测试——所有流量在23:59:59到00:00:00之间瞬间打进来来自动化测试脚本根本模拟不出那种真实的尖峰感。这篇文章就是我对这次场景处理的完整复盘从容量评估、缓存分层、降级预案到当天现场盯指标、流量回落后的排查记录全部摊开来讲。如果你负责的是直播、秒杀、年度报告、跨年活动这类高并发读多写少的业务这篇文章应该能帮你少踩很多坑。1. 跨年查询场景到底难在哪里很多人第一反应是跨年不就是一次普通的活动吗怎么还要专门立项等你看到流量曲线就明白了。平时我们业务核心接口大概5000 QPS2026年跨年那晚23:59:40开始倒计时页面流量就开始爬坡到00:00:00那一瞬间直接冲到了平台侧入口网关接近20万 QPS是日常的40倍。关键是这个峰值不是慢慢涨上来的是秒级拉满的。1.1 那一秒发生了什么跨年查询场景和普通秒杀有个非常本质的区别。秒杀是先抢资格再下单流量虽然也集中但大体上可以被前端按钮置灰、排队页挡住一部分。跨年查询不是用户不做任何操作只是把页面挂着零点一到页面自动刷新或者用户手动疯狂刷新就为了看到倒计时归零、年度报告生成、新一年的权益解锁那一瞬间。从系统视角看这个请求链路上每一个环节都在同一时刻被打满CDN回源、网关转发、应用服务器线程池、Redis连接、数据库连接池。流量特征就是“瞬时读放大”读放大的比例可以达到写操作的50倍甚至100倍。查询请求占绝大多数写操作比如领取奖励、生成报告记录反而是少数但这少数写入分布在多数查询后面如果查询链路被打挂写操作也跟着完蛋。我见过不少团队在这个场景上翻车核心原因是拿平时的性能数据去预估跨年的极限值。平时接口平均响应时间50ms日常QPS 5000单机扛500 QPS觉得绰绰有余结果跨年那一秒所有用户同时查询响应时间变成500ms单机实际吞吐量直接掉到不到200 QPS整个服务雪崩也就是几十秒的事。1.2 三类典型的查询场景与对应数据特征跨年查询不是单一的接口查询拆开来看至少有三类特征完全不同的子场景。第一类是全局状态查询比如跨年倒计时、页面是否已经开闸、跨年主题是否切换。这类数据的特点是一份数据所有人读读多写少只在零点切换一次。这类查询对一致性要求不高完全可以放在CDN、浏览器本地缓存和接入层本地缓存里最忌讳直接打到数据库。我们当时把倒计时状态做成了版本号机制前端每隔30秒拉一次版本号版本号变了才重新拉详情零点前后版本号切换一次压力就小很多。第二类是个人维度状态查询比如用户积分、优惠券数量、年度成就是否解锁、个人跨年报告是否生成。这类查询的特点是多租户、数据量大、每个用户只读自己的数据。看似没有热点实际上存在“冷热不均”——老用户、活跃用户和普通用户访问频率天差地别一旦有运营活动把某个人推上页面这个人的详情接口就会被打爆。这类场景适合“本地缓存 Redis缓存 数据库”三级结构缓存Key必须带用户维度过期时间需要打散。第三类是聚合排名查询比如跨年许愿榜、年度消费排行、跨年祝福热度榜。这类查询的特点是实时性要求高、单Key数据量巨大、读写并存。榜单数据如果直接实时Rank计算跨年那一下数据库和Redis都扛不住必须提前预计算生成一个只读榜单快照零点发布查询走快照延迟控制在秒级是可以接受的。1.3 为什么日常方案撑不住跨年查询日常方案默认假设流量是均匀分布的缓存过期时间均匀分散数据库连接池足够服务实例可以水平扩展。但跨年查询场景把这三个假设全部打破了。第一流量分布极度不均匀是典型的“脉冲式流量”。20万QPS集中在一秒内之后迅速回落到2万甚至更低。如果按峰值去扩容机器跨年过后全是浪费成本上不划算如果按均值去部署峰值那一下必死无疑。所以必须靠缓存挡流量而不是靠机器扛流量。第二缓存过期时间如果不打散零点前预热的数据会同时失效。假设我们把跨年数据都设置了“0点过期”那么00:00:00之后所有请求同时发现缓存Miss全部穿透到数据库数据库一次被打穿。缓存雪崩在跨年场景几乎是必然发生的除非你提前做了随机化处理。第三热点问题被放大。全局状态查询就是一个超级热点Key所有用户查询同一个Redis Key。单分片Redis能扛的QPS大约在8万到10万单Key访问如果所有请求都绕过本地缓存直接打Redis哪怕Redis集群有100个分片实际只有一个分片在扛压力这就是典型的热Key问题。跨年查询场景必须提前解决“单Key热点”否则Redis再大也没用。2. 方案选型分层缓存与降级设计我见过很多团队一提到高并发就是堆机器、Redis集群搞大一点、数据库升级一下结果跨年那天还是挂了因为负载都压在最脆弱的链路底层。跨年查询场景正确的做法是把流量一级一级挡在外面能让边缘扛的就不要让应用扛能让本地内存扛的就不要发网络请求。2.1 四级缓存结构从边缘到数据库我们最终采用的四级结构从外到内分别是CDN与浏览器缓存、应用本地缓存、分布式缓存、数据库兜底。每一级都有自己适合的数据类型和缓存时间。第一级CDN与浏览器缓存承接的是静态资源比如跨年活动页面的HTML、JS、CSS、背景图。这一层能挡住至少60%的静态请求。需要注意动态接口的响应不能整体放进CDN但可以把“全局状态”这类变更频率极低的数据接口做成CDN可缓存设置短TTL比如30秒CDN节点会自动回源刷新。动态请求和静态资源分离是基础如果静态资源还走应用服务器流量根本扛不住。第二级应用本地缓存是最关键的一层也是很多团队容易忽略的一层。每个应用实例用CaffeineJava或者groupcacheGo维护一份本地缓存热点Key直接本机内存返回连网络请求都省了。跨年那晚我们统计过本地缓存命中率达到了91.3%也就是说20万QPS打到应用层实际只有大约1.7万个请求需要继续访问Redis压力直接降低了一个数量级。这一层的配置核心是缓存容量和过期策略热点Key的过期时间不能太长分级设置。第三级Redis承接的是本地缓存未命中的请求以及需要分布式一致性的个人数据。我们用了Codis代理集群你也可以理解为Redis Cluster负责路由和分片。这里必须关注两个问题一是单Key热点解决方式是在Redis前面再挡一层本地缓存二是大Key问题尤其是榜单数据单Key可能几十MB查询耗时严重必须拆分成多个分片Key再逻辑聚合。我们跨年榜单最终拆成了100个分片每个分片存放1%的用户排名查询时并发拉取100个分片再本地合并耗时从原来的80ms降到15ms。第四级数据库是兜底方案正常情况下不应该承担任何跨年查询流量。我们整个跨年期间数据库QPS峰值不到3000这还是在零点后大量写入生成年度报告、记录领取行为的情况下。数据库存在的意义不是抗压而是保证最终数据不丢所以连接池只配置了日常的1.5倍没有盲目扩大。2.2 数据预热与TTL打散的关键细节缓存设计里最容易翻车的就是TTL配置。如果所有Key都设置相同过期时间那么缓存Miss发生的时间点也是集中的等于人为制造缓存雪崩。我们的做法是给每个Key的TTL加上一个随机偏移量公式是TTL 基础TTL random(0, 60)秒。基础TTL根据数据类型分级全局状态类基础60秒个人状态类基础10分钟报告结果类基础30分钟。这样即使同一批数据同时预热过期时间也会散落在几分钟到十几分钟之间不会形成同时失效的波峰。预热动作不能等到零点再做必须在23:00之前完成。预热的方式有两种一种是遍历数据库生成缓存适合个人类数据比如把过去30天活跃用户的年度报告摘要预生成并写入Redis另一种是指定热点Key主动写入适合全局类数据比如零点要切换的跨年主题版本。我们的预热脚本是分批次并发执行的控制Redis写入速率在每秒5000以内避免预热本身把Redis打满。预热完成后需要验证命中率用压测流量先打一遍确保缓存确实生效而不是热了个寂寞。这里还有一个容易被忽略的细节本地缓存的过期时间不能全部一样。我们遇到过某个实例的本地缓存提前过期导致这个实例的Redis流量比别的实例高出5倍的情况原因是这个实例在预热阶段流量不够缓存根本没建立起来。解决方式是在应用启动或流量上涨前主动做一次本地缓存预加载不能等用户请求来触发缓存填充。2.3 一致性取舍多久能容忍不一致跨年查询场景要学会和“缓存不一致”共处。全局状态类数据允许10秒级不一致个人状态类数据允许1分钟级不一致榜单类数据允许5分钟级不一致。这不是懒惰而是最基本的工程权衡——在同一时间点如果要做到缓存与数据库严格一致就必须等到写入完成再通知所有缓存更新跨年那一秒根本做不到反而会把压力全部转移到数据库。我们用了一个简单的版本号机制来控制全局状态更新。数据库里一张状态表记录当前版本号和生效时间。CDN回源时会去查“当前版本号”版本号没变就直接返回缓存的旧页面版本号变了才重新拉取详情。零点更新流程是先写数据库再更新Redis版本号最后等CDN自然过期。用户端看到的跨年页面延迟最多10秒业务层面完全可接受。个人状态类数据我们采用了“失效而非更新”的策略。用户零点后领取了跨年礼包如果不删缓存用户看到的数据还是“未领取”会反复重试甚至产生资损投诉。我们的做法是写操作完成后主动删除该用户的Redis Key和本地缓存Key而不是更新缓存值。下次查询时自动回源数据库并重建缓存。主动失效的代价是删除操作可能偶尔失败所以同时在“写操作”和“查询操作”之间做了1秒的缓存穿透容忍——如果查询时发现Key刚被删除但数据已有写入标记就直接读数据库构建新缓存。3. 容量评估与参数推演实录跨年查询场景处理最核心的环节就是提前做容量评估。很多团队不做评估或者评估得过于粗糙导致当天资源不够或者资源浪费。我以我们核心倒计时页面的真实数据为例把评估过程完整推演一遍。3.1 20万QPS的容量估算过程第一步是估算峰值QPS。我们参考了去年跨年的流量数据和今年的活动宣传力度预计平台入口峰值QPS为20万其中大约50%会落到倒计时页面详情接口也就是10万QPS。这10万QPS中假设静态资源请求占60%由CDN承接那么动态请求为4万QPS。动态请求中再经过应用本地缓存命中率按90%算真实打到Redis的请求为4000 QPS打到数据库的请求为400 QPS4万 × 10% × 10%这是保守估计。第二步是估算Redis容量。Redis单实例单Key访问QPS大约在8万到10万我们集群20个分片理论上可承载160万QPS远超4000 QPS的需求。但上面说了热点Key会集中在某一个分片上所以不能只算总量还要算单分片压力。我们通过本地缓存把热点Key的透传率降到10%以下4000 QPS中大约有3000 QPS是个人数据Key均匀分散1000 QPS是热点Key经过本地缓存再过滤90%后只有100 QPS会打到热点分片上单分片压力完全可控。第三步是估算带宽。跨年那晚最大的隐患其实是带宽而不是CPU。我们的活动页面有背景图和动态特效单个页面大小约1.5MB如果CDN没有生效20万用户同时加载带宽需求就是20万 × 1.5MB ≈ 300GB瞬间把入口带宽打满。我们提前和CDN厂商确认了边缘节点容量并把页面拆分为首屏2KB的HTML和内联懒加载的静态资源确保首屏请求极小。这个细节很关键很多人只盯着应用层容量忘了网络层才是真正的第一道卡点。3.2 限流、熔断与降级开关配置参考容量评估不只是算资源还要做“假如超出预期怎么办”的预案。我们当时配置了三个层级的保护机制具体参数如下。网关层使用令牌桶限流。入口网关的限流阈值设为预期峰值的80%即16万QPS超过部分直接返回“系统繁忙请稍后再试”的提示页。这样做的原因是即使流量超出预期也不能让所有请求都穿透到应用层保留20%的余量给系统缓冲。令牌桶的rate设为每秒16万burst设为20万保证短时突发可以容忍但持续超高流量会被拦截。应用层配置了熔断器。用的是Sentinel阿里开源的流量防卫组件规则是接口错误率超过5%时开启熔断熔断持续10秒10秒后半开试探允许少量请求通过。跨年当晚我们观察到一个有趣的现象错误率并不是从0直接跳到100%而是先在1%~3%之间波动然后瞬间冲到20%如果不加熔断等到错误率肉眼可见时服务已经接近雪崩了。熔断器的价值就在于“提前断臂求生”牺牲一两个请求保住了整体服务。降级开关是最后一道防线。我们把所有非核心查询接口都配置了可降级的开关比如个人年度报告详情、跨年祝福列表这类不影响主流程的功能一旦Redis或数据库压力过大直接返回静态兜底数据或提示“稍后查看”。降级开关通过配置中心动态下发不用重启服务开关粒度精确到接口级别。跨年当天我们在00:00:05左右手动降级了两个非核心接口瞬间释放了约3000 QPS压力。3.3 压测数据与真实预期差距光算不算数有条件一定要做全链路压测。我们提前两周做了三轮压测第一轮是单接口压测第二轮是核心链路压测第三轮是全链路混合压测。压测结果和真实跨年数据有一些差距这些差距很有参考价值。单接口压测时倒计时详情接口在应用层4线程并发下稳定输出8000 QPS响应时间平均35ms。全链路混合压测时加上网关、Redis、数据库后同样的应用配置只能扛到5000 QPS响应时间拉到65ms。原因很简单全链路压测会同时打到Redis、数据库和日志系统这些组件之间存在资源竞争尤其是日志异步刷盘在高峰时会抢占CPU和磁盘IO。真实跨年当天的数据又和压测不一样。00:00:00那一刻接口吞吐量达到了预期峰值但响应时间从压测的65ms飙升到220ms主要原因是CDN回源请求和动态请求混在一起入口网关的连接数远超压测时的模拟值。我们在压测时是用固定QPS持续打压真实场景是QPS瞬间拉满大量连接同时建立TCP握手和TLS握手开销被放大。所以压测模型一定要加入“瞬间脉冲”波形而不是只用恒定QPS。4. 跨年当天的执行过程与现场处置方案再好落地执行才是关键。跨年当天我们采用的是“一字排开、分岗盯防”的模式前端、网关、应用、Redis、数据库各安排一位同学盯着监控每两分钟在群里同步一次核心指标。4.1 时间线复盘从21点到00点10分21:00 各岗位就位。检查项包括CDN刷新任务是否完成、Redis集群各分片内存与连接数、应用实例数量与健康状态、配置中心降级开关是否处于预期状态。此时线上流量与日常持平约5000 QPS。23:00 预热脚本执行完毕。Redis中热点Key缓存命中率98.5%个人状态类数据预生成了最近30天活跃用户的缓存总计约1200万个Key。内存占用约35GB均在计划范围内。23:30 进行一次突发性流量模拟利用压测平台发送了2万QPS持续30秒的脉冲流量验证限流、熔断、降级开关流程都能正常工作。这次模拟还意外发现了一个问题应用层某个实例的本地缓存命中率明显低于其他实例排查原因是因为该实例最近发布过一次配置本地缓存被清空后没有主动预热。立刻执行了手动预热这里值得记一笔。23:55 流量开始爬升从2万QPS涨到5万QPS。此时监控显示Redis CPU使用率22%应用层平均RT 45ms一切正常。23:59:00 流量涨到12万QPS应用层RT开始上升至80ms。我盯着监控屏心跳开始加速。00:00:00 流量瞬间冲到19.6万QPS。监控大屏上Redis CPU使用率跳到58%应用层RT冲到180ms但还在可控范围。这一刻所有技术同学都屏住呼吸盯着三个数字错误率、RT、Redis CPU。00:00:05 个人年度报告查询接口RT异常升高到350ms错误率从0.1%跳到1.8%。虽然离熔断阈值5%还有距离但趋势不对。快速检查后发现这个接口的Redis Key在零点被大量请求穿透原因是一部分老用户的缓存预热没有覆盖到我们只预热了最近30天活跃用户但实际用户覆盖到了90天查询Miss后全部回源数据库构建缓存数据库连接池线程繁忙。立即降级该接口到静态兜底文案同时手动预热缺失的热点用户数据。00:00:10 流量开始回落到8万QPSRT稳定在120ms。错误率恢复正常。00:01:30 核心链路全部恢复平稳数据库连接池活跃数回落到正常区间完成状态发布验证。4.2 现场出现的问题与应急操作现场有两个问题值得单独记录。第一个是个人年度报告查询接口的缓存穿透。原因在上面提过就是预热策略的数据覆盖范围不够。我们做的应急操作是先降级该接口避免继续拖垮数据库然后写了一个扫描脚本找出零点后前5分钟内查询过但缓存未命中的所有用户ID批量预热这些用户的数据。这个操作20分钟完成之后接口恢复。事后反思预热范围不应该只看最近30天活跃用户还应该结合往年跨年当天的活跃数据把“大概率会在零点查询”的用户全部覆盖。第二个问题是Redis连接数接近上限。00:00:00那一刻应用层到Redis的连接数从平时的8000飙升到2.2万接近我们设置的上限2.5万。原因是本地缓存命中率虽然高达91%但剩余9%的请求基数太大约4000 QPS而且查询RT上升导致连接池中的连接被占用时间变长需要创建更多新连接。应急操作是临时调高了Redis连接池上限并启动了一个备用Redis集群将部分个人数据查询分流过去。事后我们优化了Redis客户端连接池的蹲守策略使用空闲连接检测和预创建机制避免高峰期连接数出现爬坡式的突增。4.3 结果数据与经验沉淀跨年那晚最终的数据是峰值19.6万QPS应用层整体平均RT 132ms错误率0.06%数据库峰值QPS 2800Redis峰值CPU 62%没有出现完全不可用的服务中断。整体来说是一次平稳的跨年。但我们并没有因为平稳就放松第二天立刻启动了复盘会议把所有监控数据、操作记录、异常事件整理成了一份详细的复盘文档。复盘文档里最有价值的内容是一份“跨年查询场景准备清单”包括容量估算表、缓存预热脚本、流量模拟方案、限流熔断参数、降级开关列表、监控看板模板、应急操作手册。这份清单沉淀到团队内部后后来的直播大场、年度报告等大流量活动都直接复用了这套模板节省了至少两周的准备工作。5. 常见问题与避坑速查跨年查询场景处理过程中踩过的坑和排查出的问题整理成速查表供大家直接参考。5.1 高频问题对照表问题现象可能根因解决方案缓存穿透查询一个不存在的Key每次都打到数据库恶意请求或业务数据未生成布隆过滤器拦截不存在的Key或缓存空值并设置极短TTL缓存击穿某个热点Key过期瞬间大量请求穿透热点数据过期时间未打散热点Key永不过期由后台定时更新或设置互斥锁只允许一个请求回源缓存雪崩大量Key同时失效TTL设置相同且没有加随机偏移量TTL 基础TTL random(0, 60)秒预热时分批进行热Key单Redis分片CPU 100%全局状态被所有请求访问本地缓存兜住90%流量热点Key拆分或使用Redis的本地读写分离连接池耗尽应用报获取连接超时流量突增导致连接占用时间变长预创建连接、调大最大连接数、设置更短的空闲回收时间每个问题在跨年当晚都有对应的实战案例。比如缓存穿透那个问题我们后来加了一个“空值短TTL”策略如果数据库查询结果为空仍然缓存一个空值TTL设置为30秒防止同一个不存在的用户ID被反复查询打穿数据库。这个策略对非法请求和正常但无数据的请求都很有效代价是最多延迟30秒的数据一致性在查询场景完全可接受。5.2 复盘时的关键决策记录每一次复盘我都会把“为什么这么做”记下来防止后人踩同样的坑。决策一为什么没有升级数据库规格因为跨年查询场景的QPS绝大部分是读而且可以通过缓存挡掉数据库只承受少量写入和缓存未命中的回源。升级数据库规格的成本远高于多加一层缓存且数据库高可用切换复杂不如把精力花在缓存上。决策二为什么没有引入消息队列削峰跨年查询是同步请求用户刷新页面就是为了立刻看到结果不可能把请求丢进MQ里异步返回。MQ适合削写峰、做解耦不适合削读峰。读请求只能用缓存和本地内存来挡没有别的捷径。决策三为什么本地缓存没有做成分布式一致性因为查询场景允许短暂不一致本地缓存过期时间控制在1分钟以内对用户无感知。做分布式缓存更新反而引入复杂度和一致性问题得不偿失。决策四为什么没有用Redis Cluster自带的高可用我们用的Codis代理集群原因是跨年场景更看重路由稳定性和运维可观测性。Redis Cluster虽然去中心化但增加节点时需要处理slot迁移跨年当天不希望在集群变更上冒风险。5.3 几个值得记住的踩坑心得第一时间格式问题一定要提前统一。跨年场景大量涉及时间戳、日期边界、时区转换。我们踩过一个坑性能监控系统显示的跨年时间是23:59:59业务日志里出现的是00:00:00之后的时间结果排查问题时发现两边差了8个小时原因是一边用UTC一边用本地时区。这种问题本身不大但在跨年那种紧张氛围下会浪费宝贵的排查时间。建议在日志、监控、缓存Key里面统一使用服务器本地时间并在Key里显式带上日期避免跨天时Key混淆。第二压测一定要模拟真实的脉冲波形而不仅仅是固定QPS。固定QPS压测可以让系统稳步进入高负载状态但真实跨年是瞬间打到峰值连接的建立、线程的创建、连接的分配都会在几秒内同时发生很多系统就是在这个阶段崩溃的。我们在第二轮压测后就发现应用启动时线程池初始化太慢通过预启动核心线程解决。第三跨年当天不要做任何计划外的改动哪怕是一个看似无害的配置修改。跨年之前所有参数应该已经通过压测验证过临时改配置等于拿线上稳定性冒险。我们有同学在23:00左右想调大某个接口的限流阈值被负责人拦下了理由是“为了10%的余量去改变验证过的参数不值得”。第四监控大屏上的数字只是结果真正的过程指标才是排查问题时的关键。不要只盯QPS和RT还要盯线程池活跃数、垃圾回收频率、数据库连接池水位、Redis慢查询数。很多问题在QPS还没有变化时这些指标已经在预警了。我们对线程池活跃数设置了90%的告警阈值跨年当晚因为这个告警提前发现了某个实例的流量倾斜问题。第五保留完整的压测数据、监控曲线和操作记录。跨年结束后这些数据是优化系统的最好依据也是明年写方案时最可靠的参考。我们做了一套自动化的复盘报告生成脚本每次大促或活动结束后自动拉取监控数据和操作日志生成HTML报告大大减少了复盘时整理数据的时间。跨年查询场景处理这个项目给我最大的感受是跨年跨的是“秒”但准备要提前几周甚至一个月。把容量评估、缓存分层、限流降级、压测预案一个一个落实到位再加上当天团队的沉稳执行跨年那一下就真的只是“秒过”。如果你明年也要做跨年活动建议现在就把这套思路拿去过一遍提前把热点Key找出来把预热脚本写好把压测模型建好把降级开关测通。真到了零点那一刻你就能像我一样看着曲线冲上去再安全落下来心里默默松一口气然后踏踏实实地开始写复盘文档。