java经典场景题 (重要)
1.热点数据处理。
场景:流量明星发送微博信息,一分钟内涌入5000万人。
首先我们要对数据的流动进行充分的理解,用户点击微博,点开热点信息,点赞,评论,转发,客户端传入数据库,进行缓存,缓存拦截大量重复。结合微服务,就还要加一个网关。
考察:
1.能不能对并发流量问题拆解多个层次。
2.知不知道对核心业务的取舍
3.,理解高并发的过程
分析:热点流量特点是来的快去的也快,大量的读写操作,并发。核心业务,点赞,评论。
解决组合:
所有操作都要围绕,不要让数据库崩掉,让少数的数据进入数据库,大量的数据被拦截,围绕这个核心可以得出。
拆分四个层次。
第一层,多级缓存抗流量。事件特点是读多写少
1.redis缓存进行抗读,大量的数据需要在redis层被拦截。
最外层,cdn静态资源,比如图片视频,推到边缘节点,用户刷微博,用不上服务器。
第二层,本地缓存,将热点数据放在内存里面,连redis都不用差。
第三层redis 保证高可用(这是又涉及redis的穿透,雪崩,击穿,持久化,)
【redis】缓存穿透 缓存击穿 缓存雪崩-CSDN博客 链接失效去主页找。
第二层,当流量太大缓存也抗不住,就要进行服务降级操作。保证核心业务,边缘业务先放一放。
如刷微博,写微博,评论是核心业务,个性推荐,是边缘业务。
读可以降级为过期数据,点赞延迟更新,评论去显示部分评论,
写可以异步写入,使用消息队列异步处理。
第三层限流保护
两个核心算法,令牌桶算法,漏桶算法。
为什么限流?一个核心,保证系统不被大量数据冲爆。假设有五万张票,但是你不能让50万人抢,所以得限流,让他们排队。
限流可以分层,如果有网关层,网关层可以进行分流,单台服务器每秒最多10万请求,超出返回服务层繁忙,应用层利用算法,对每个接口进行单独限流,比如发微博接口每秒一万,点赞每秒五万,数据采用连接池限制,最多启用1000个连接,保护数据库安全。
第四层,增加硬实力,动态扩容。
,服务器进行扩容,但是要区分情况,如热点数据来的快去的也快,进行扩容就太浪费资源了。
使用云原能力,kubernetes自动伸缩,Docker镜像。
但是记住,阔再多,数据库扛不住,在多也没有,日活高,像微信,qq等数据库一定是分库分表的,按用户id哈希,将数据打散到库里面。
最后还可以加消息队列削峰,争对大量猛增的数据,一会突然下降的数据,使用kafka削峰高吞吐,用户发微博,点赞评论,仍进队列,后端慢慢处理。
总结
缓存扛读,降级保护核心,限流防打死数据库,扩容补资源,队列削峰填谷,
2.发红包场景
多层防御架构(从外到内,层层兜底)
第一层:缓存层核心防护(第一道主力防线)
1. 分布式互斥锁:串行重建缓存,禁止并发查库
核心思路:缓存失效时,只允许一个线程去 DB 查数据并重建缓存,其余线程等待 / 重试。
- 落地实现:
Redisson分布式锁(Redis 官方推荐,自带锁续期、防死锁) - 优化:自旋重试 + 超时控制,避免大量线程阻塞占用连接
- 适用场景:强一致性要求、热点 Key 更新频率低的场景(如商品详情)
2. 逻辑过期:永不主动删 Key,后台异步刷新
核心思路:缓存 Value 里存过期时间戳,业务线程只判断时间戳是否过期,不阻塞;过期后立即返回旧值,同时用异步线程去 DB 拉新数据更新缓存。
- 优点:无锁、无阻塞、吞吐量拉满,完全不打崩 DB
- 缺点:短时间会读到旧数据(最终一致性)
- 适用场景:高并发秒杀、热搜、榜单等允许短暂脏读的场景
3. 热点 Key 永久不过期 + 主动异步刷新
核心思路:
- 标记热点 Key,设置
TTL=-1永不过期 - 后台定时任务 / 消息队列异步更新缓存
- 配合缓存预热,服务启动时提前加载热点数据
- 适用场景:超级热点 Key(如爆款商品、首页 Banner)
4. 多级缓存:本地缓存兜底,避免穿透到 Redis
架构:JVM本地缓存(Caffeine) + Redis分布式缓存
- 热点 Key 先放 Caffeine(本地内存,O (1) 访问,比 Redis 快 10~100 倍)
- 本地缓存过期后再查 Redis,Redis 过期再走锁重建
- 关键:本地缓存设置短 TTL,避免数据不一致 + 内存溢出
- 优势:即使 Redis 宕机 / 抖动,本地缓存仍能扛住流量
第二层:应用层流量管控(第二道防线,削峰填谷)
1. 热点 Key 自动探测与标记
- 实时统计 QPS、访问频次,自动识别热点 Key
- 对识别出的热点 Key:提前预热、永久不过期、放入本地缓存
- 落地:Redis 客户端热 Key 统计、网关层埋点、日志分析
2. 接口限流 + 熔断降级
- 网关 / 服务层对热点接口做单机限流 + 分布式限流
- 超出阈值直接熔断,返回兜底数据(静态页、默认值)
- 配合 Sentinel/Hystrix,自动降级,防止流量压垮服务
3. 请求合并 / 批处理
高并发下,将同一 Key 的并发请求合并,只执行一次 DB 查询,结果复用给所有请求
- 落地:Guava Cache、自定义合并器、消息队列攒批
第三层:数据库层抗压(第三道防线,最后兜底)
1. 数据库限流 & 读写分离
- 读请求走从库,主库只负责写入,避免主库被打挂
- DB 层配置连接池限流、SQL 限流,禁止无限制查询
2. 索引优化 + 轻量查询
- 热点查询必须命中主键 / 唯一索引,禁止慢查询
- 只查必要字段,减少 DB IO 与 CPU 开销
- 禁止事务 + 长查询,避免锁等待
3. 分库分表 / 冷热数据分离
热点数据单独分片,避免与冷数据争抢资源,提升查询效率
第四层:架构兜底与一致性保障(终极防线)
1. 静态兜底数据
- 预生成静态页面 / 默认 JSON,缓存击穿时直接返回兜底数据
- 对用户无感知,保证服务可用
2. 缓存 + DB 双写一致性
避免重建缓存时出现数据不一致:
- 方案:
先更DB,再删缓存(Cache Aside Pattern) - 异步更新:通过 Canal 订阅 MySQL binlog,自动同步 Redis
第五层:监控预警 & 主动治理(防患于未然)
- 监控指标
- Redis 热 Key 数量、命中率、过期 Key 分布
- DB 连接数、慢查询、CPU 负载
- 接口 QPS、熔断次数、降级比例
- 自动预警
- 缓存命中率低于阈值告警
- 热点 Key 即将过期自动提醒
- 主动扩容
- 热点时段自动扩容 Redis / 应用节点
- 临时提升 DB 读能力
资深程序员生产级「最佳组合拳」
根据业务一致性要求,分两套落地方案:
方案 A:强一致性(商品详情、订单)
多级缓存(Caffeine+Redis) + Redisson分布式锁 + DB读写分离 + 限流熔断
方案 B:高并发最终一致性(秒杀、热搜、榜单)
本地缓存永久热点Key + 逻辑过期 + 异步刷新 + 熔断降级 + 静态兜底
必避坑点(资深踩坑总结)
- 不要用本地锁(synchronized/Lock) 应对集群部署,会出现多节点同时查库
- 互斥锁必须加超时时间,防止线程崩溃导致死锁
- 逻辑过期不要和强一致性业务混用,避免客诉
- 热点 Key 不要设置随机 TTL 打散,会人为制造击穿
- 禁止缓存重建时做复杂业务计算,拖慢响应、占用线程
一、整体流程概览
核心思想:缓存未命中 → 抢分布式锁 → 只允许一个线程查库更新缓存 → 其他线程等待重试 → 缓存更新后全部命中返回
关键设计点
- 双检缓存:抢锁前后都查一次缓存,避免重复查库
- 锁自动续期:防止查询 DB 过慢导致锁提前释放
- 有限重试:避免线程无限自旋占用资源
- 空值缓存:顺便解决缓存穿透
- finally 释放锁:绝对防止死锁
二、详细执行步骤(逐行可落地)
假设有一个热点 Key:product:detail:1001
步骤 1:接收请求,先查 Redis 缓存
plaintext
请求 → get(product:detail:1001)
- 命中:直接返回数据,流程结束
- 未命中:进入锁逻辑
步骤 2:【第一次双检】再次查缓存
为什么?高并发下,可能刚有其他线程更新完缓存,你没必要再抢锁。
plaintext
if (cache.exist(key)) {
return cache.get(key);
}
步骤 3:尝试获取分布式锁
锁 Key 建议:lock:cache:product:detail:1001
- 使用 Redisson
RLock lock = redisson.getLock(lockKey); - 加锁:
lock.lock(5, TimeUnit.SECONDS);- 5s 是锁超时时间,Redisson 会自动续期(watchdog 机制)
步骤 4:【第二次双检】再次查缓存
这一步极其重要!可能你在等待锁的过程中,其他线程已经把缓存重建好了。
plaintext
if (cache.exist(key)) {
return cache.get(key);
}
步骤 5:只有抢到锁 & 缓存仍不存在 → 查询数据库
plaintext
Data data = dbMapper.selectById(id);
步骤 6:将数据写回 Redis
- 数据非空:写入缓存 + 设置正常 TTL(如 30min)
- 数据为空:写入空值 / 默认值 + 设置短 TTL(如 1min),防穿透
plaintext
redis.set(key, data, 30, TimeUnit.MINUTES);
步骤 7:释放分布式锁
必须放在 finally 里,保证任何异常都能释放
plaintext
finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
步骤 8:返回数据
未抢到锁的线程处理逻辑:
- 线程 sleep 一小段时间(如 50ms)
- 重试查缓存(最多 3~5 次)
- 重试成功:返回缓存数据
- 重试失败:返回兜底数据 / 抛出限流异常
三、标准伪代码(Java 风格,可直接改写成业务代码)
java
运行
public ProductDetail getProductDetail(Long productId) {
String cacheKey = "product:detail:" + productId;
String lockKey = "lock:cache:" + cacheKey;
// 1. 查缓存
ProductDetail cache = redisTemplate.get(cacheKey);
if (cache != null) {
return cache;
}
RLock lock = redissonClient.getLock(lockKey);
try {
// 尝试加锁,最多等待 1s,锁持有 5s(Redisson自动续期)
boolean lockSuccess = lock.tryLock(1, 5, TimeUnit.SECONDS);
if (!lockSuccess) {
// 未抢到锁:短暂休眠重试
Thread.sleep(50);
// 最多重试2次
for (int i = 0; i < 2; i++) {
cache = redisTemplate.get(cacheKey);
if (cache != null) return cache;
Thread.sleep(50);
}
// 重试失败:返回兜底或抛异常
return getDefaultDetail(productId);
}
// 2. 抢到锁后【再次双检】
cache = redisTemplate.get(cacheKey);
if (cache != null) {
return cache;
}
// 3. 只有真正需要,才查DB
ProductDetail dbData = productMapper.selectById(productId);
// 4. 写回缓存(空值处理)
if (dbData != null) {
redisTemplate.set(cacheKey, dbData, 30, TimeUnit.MINUTES);
} else {
// 防穿透:缓存空值,短过期
redisTemplate.set(cacheKey, EMPTY_DETAIL, 1, TimeUnit.MINUTES);
}
return dbData;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return getDefaultDetail(productId);
} finally {
// 必须释放锁
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
四、为什么必须这么设计?(资深要点)
1. 为什么要两次查缓存?
- 第一次:过滤掉大部分并发
- 第二次:防止你等待锁期间,别人已经重建好缓存避免:1000 个并发同时抢锁,都去查 DB
2. 为什么用 Redisson 而不是手写 SETNX?
- Redisson 自带 watchdog 锁续期:查询 DB 慢也不会锁超时释放
- 可重入、防误删、防死锁
- 手写
SETNX + DEL极易在异常时造成死锁
3. 为什么未抢到锁要重试而不是直接报错?
热点 Key 重建极快(毫秒级),重试 2~3 次基本都能命中缓存,用户无感知。
4. 锁超时设多久合适?
- 锁等待时间:1s 以内
- 锁持有时间:5s(Redisson 会自动续期)
- 避免锁时间太长,导致故障时阻塞太久
五、极端异常场景兜底
- Redis 挂了→ 关闭缓存逻辑,直接限流 + 降级走 DB
- DB 查询超时→ 锁自动释放,下一个线程重试
- 应用突然宕机→ Redisson 锁超时自动释放,无死锁
- 大量线程同时重试→ 配合接口限流,防止雪崩
六、总结最简流程口诀
先查缓存,未命中抢锁抢到再查,避免重复查库查库写缓存,空值也要存finally 释放锁,重试不死锁
更多推荐


所有评论(0)