news 2026/6/2 7:46:19

Spring Boot 线程池拒单引发的缓存雪崩?多级缓存与防穿透架构实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring Boot 线程池拒单引发的缓存雪崩?多级缓存与防穿透架构实战

Spring Boot 线程池拒单引发的缓存雪崩?多级缓存与防穿透架构实战

前言

凌晨三点,电话响了。
监控报警,CPU 飙到 100%。
线程池满了,任务被拒绝。
缓存没更新,数据库被打死。
这就是典型的连锁反应。
那天我负责的系统,突然变得极慢。
用户反馈页面转圈,后台日志全是异常。
排查发现,线程池核心线程数设少了。
大量请求堆积,触发了拒绝策略。
任务被丢弃,缓存更新失败。
旧数据还在,新请求却查不到。
数据库直接暴露在流量下。
这就是所谓的“缓存穿透”加“线程池拒单”。
单一故障,引发了系统雪崩。
今天就把这个坑,彻底填平。

一、底层原理

1.1 核心机制

线程池不是简单的容器。
它是系统流量的闸门。
状态转换决定了生死。
RUNNING 状态正常接收任务。
SHUTDOWN 不再接收新任务。
STOP 会中断正在执行的任务。
TIDYING 是清理工作的阶段。
TERMINATED 代表彻底结束。
缓存一致性是另一个难题。
先改库还是先删缓存?
双写不一致会导致脏数据。
高并发下,竞争更激烈。
防穿透是为了保护数据库。
查询不存在的数据,会穿透缓存。
直接打到数据库,压力巨大。
我们需要多层防御体系。

graph TD A["用户请求"] --> B["网关层"] B --> C["线程池(核心)"] C --> D["一级缓存(Local)"] D --> E["二级缓存(Redis)"] E --> F["数据库(DB)"] C --> G["拒绝策略处理"] G --> H["降级服务"] E -.->|穿透检测 | I["布隆过滤器"] I -->|不存在 | J["直接返回"] I -->|存在 | E

上图展示了流量走向。
请求先进入线程池。
线程池控制并发度。
一级缓存速度最快。
二级缓存容量大。
数据库是最后防线。
拒绝策略触发时。
系统进入降级模式。
布隆过滤器拦截无效请求。
保护数据库不被穿透。

1.2 与同类方案的对比

不同场景需要不同策略。
直接创建线程太浪费。
使用 Executors 工厂类。
默认配置往往有坑。
固定大小池适合 IO 密集。
缓存策略影响一致性。
强一致性牺牲性能。
最终一致性提升吞吐。
防穿透手段各有优劣。
布隆过滤器节省空间。
空对象缓存占用内存。

方案线程池配置缓存一致性防穿透手段适用场景
默认工厂固定大小先删缓存空对象缓存低并发内部系统
自定义池动态调整延时双删布隆过滤器高并发核心业务
响应式流背压控制事件驱动多级熔断实时数据流处理

二、快速上手

先写个最小可运行示例。
配置一个自定义线程池。
设置核心线程数为 10。
最大线程数设为 50。
队列容量限制在 100。
拒绝策略选择调用者运行。
这样不会直接丢弃任务。
缓存使用 Spring Cache。
注解驱动,简单方便。
三步即可跑通流程。

@Configuration public class ThreadPoolConfig { /** * 自定义线程池 Bean * 避免使用默认工厂类 * 防止队列无界导致 OOM */ @Bean("customExecutor") public Executor customExecutor() { // 核心线程数,日常流量够用 int corePoolSize = 10; // 最大线程数,应对突发流量 int maxPoolSize = 50; // 队列容量,限制堆积数量 long queueCapacity = 100; // 线程名前缀,方便排查问题 String threadNamePrefix = "biz-worker-"; // 构建线程池工厂 ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(corePoolSize); executor.setMaxPoolSize(maxPoolSize); executor.setQueueCapacity((int) queueCapacity); executor.setThreadNamePrefix(threadNamePrefix); // 设置拒绝策略,调用者运行避免任务丢失 executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); // 等待任务完成后关闭 executor.setWaitForTasksToCompleteOnShutdown(true); // 初始化 executor.initialize(); return executor; } }

三、核心 API / 深水区

3.1 核心方法速查

线程池有很多方法。
submit 提交有返回值任务。
execute 提交无返回值任务。
shutdown 平滑关闭线程池。
shutdownNow 立即中断任务。
getQueue 获取等待队列。
getPoolSize 获取当前大小。
这些方法生产都要用。
尤其是关闭时机。
容器销毁时必须关闭。
防止资源泄露。
缓存 API 也很关键。
put 存入数据。
get 获取数据。
evict 删除数据。
配合注解使用。
@Cacheable 查询时生效。
@CacheEvict 更新时生效。

3.2 生产级配置

异常处理不能少。
任务内部必须 try-catch。
防止单个任务异常。
导致整个线程池挂掉。
超时控制要设置。
任务不能无限期运行。
使用 Future.get 时。
必须指定超时时间。
线程池监控要开启。
指标上报到监控系统。
队列长度实时可见。
拒绝次数需要报警。
配置中心动态调整。
不用重启即可生效。

3.3 高级定制

拒绝策略可以自定义。
记录日志并持久化。
后续可以重试处理。
线程池可以分层。
IO 密集型单独一组。
CPU 密集型单独一组。
避免相互影响。
缓存可以分级。
本地缓存做热点。
分布式缓存做兜底。
一致性靠消息队列。
异步更新保证最终一致。

四、实战演练

场景是用户信息查询。
高并发读取用户资料。
先查本地缓存。
再查 Redis 缓存。
最后查数据库。
更新时先删缓存。
再更新数据库。
防止旧数据覆盖。
线程池处理异步日志。
避免阻塞主流程。
如果线程池满了。
日志直接丢弃。
保证核心业务可用。
查询不存在用户。
布隆过滤器拦截。
直接返回空结果。
不查数据库。

@Service public class UserService { @Autowired @Qualifier("customExecutor") private Executor executor; @Autowired private RedisTemplate<String, Object> redisTemplate; /** * 查询用户信息 * 多级缓存 + 防穿透 */ public User getUserInfo(String userId) { // 1. 尝试从本地缓存获取 (模拟) User localCacheUser = getLocalCache(userId); if (localCacheUser != null) { return localCacheUser; } // 2. 尝试从 Redis 获取 String cacheKey = "user:" + userId; User redisUser = (User) redisTemplate.opsForValue().get(cacheKey); if (redisUser != null) { // 回填本地缓存 setLocalCache(userId, redisUser); return redisUser; } // 3. 防穿透:检查布隆过滤器 (模拟) // 如果布隆过滤器说没有,直接返回 null // 避免缓存穿透打穿 DB if (!bloomFilterExists(userId)) { // 缓存空对象,防止穿透 redisTemplate.opsForValue().set(cacheKey, new User(), 300, TimeUnit.SECONDS); return null; } // 4. 查数据库 User dbUser = userMapper.selectById(userId); // 5. 异步更新缓存 if (dbUser != null) { executor.execute(() -> { try { redisTemplate.opsForValue().set(cacheKey, dbUser, 1, TimeUnit.HOURS); setLocalCache(userId, dbUser); } catch (Exception e) { // 记录日志,不影响主流程 log.error("缓存更新失败", e); } }); } return dbUser; } private boolean bloomFilterExists(String userId) { // 模拟布隆过滤器检查 return true; } private User getLocalCache(String userId) { return null; } private void setLocalCache(String userId, User user) { // 模拟设置本地缓存 } }

五、避坑指南与最佳实践

💡 技巧

线程名前缀一定要设。
排查问题时能一眼认出。
队列最好用有界队列。
防止内存溢出。
拒绝策略选 CallerRuns。
给系统喘息机会。

⚠️ 警告

千万不要用无界队列。
流量突增直接 OOM。
缓存更新不要同步做。
拖慢主接口响应。
布隆过滤器会有误判。
允许少量穿透发生。
线程池不要共用。
不同业务隔离开。

✅ 推荐

核心业务独立线程池。
互不影响更稳定。
监控指标必须上。
知道什么时候满。
配置中心动态化。
紧急情况下可调整。
空对象缓存要设 TTL。
防止占用过多内存。

六、综合实战演示

下面是一套闭环代码。
包含配置、服务、异常处理。
直接复制可用。
注意变量名已汉化。
注释极其详细。
生产环境请按需调整。

/** * 综合实战:线程池 + 缓存 + 防穿透 * 包含完整的异常处理逻辑 */ @Component public class HighConcurrentService { @Autowired @Qualifier("customExecutor") private Executor taskExecutor; @Autowired private StringRedisTemplate redisTemplate; /** * 处理高并发查询任务 * @param orderId 订单 ID * @return 订单详情 */ public OrderDTO queryOrder(String orderId) { // 1. 参数校验 if (orderId == null || orderId.isEmpty()) { throw new IllegalArgumentException("订单 ID 不能为空"); } // 2. 尝试从缓存获取 String key = "order:" + orderId; String cachedJson = redisTemplate.opsForValue().get(key); if (cachedJson != null) { // 解析 JSON 返回 return JsonUtil.parse(cachedJson, OrderDTO.class); } // 3. 防穿透检查 (模拟布隆过滤器) // 如果 ID 格式都不对,直接拒绝 if (!orderId.matches("\\d{10,}")) { // 记录异常日志 log.warn("非法订单 ID 请求: {}", orderId); return null; } // 4. 提交异步任务查询 DB // 使用 submit 获取 Future,便于控制超时 Future<OrderDTO> future = taskExecutor.submit(() -> { // 模拟数据库查询耗时 Thread.sleep(100); return orderMapper.selectDtoById(orderId); }); try { // 设置超时时间,防止无限等待 OrderDTO order = future.get(2, TimeUnit.SECONDS); if (order != null) { // 异步回填缓存 redisTemplate.opsForValue().set(key, JsonUtil.toJson(order), 24, TimeUnit.HOURS); } else { // 缓存空对象,防穿透 redisTemplate.opsForValue().set(key, "NULL", 5, TimeUnit.MINUTES); } return order; } catch (TimeoutException e) { // 超时处理 log.error("查询超时,订单 ID: {}", orderId); future.cancel(true); // 取消任务 return null; } catch (Exception e) { // 其他异常处理 log.error("查询异常,订单 ID: {}", orderId, e); return null; } } }

七、总结

线程池是流量的闸门。
缓存是系统的盾牌。
防穿透是最后的防线。
三者缺一不可。
配置要合理,监控要跟上。
异常要处理,超时要控制。
不要相信默认配置。
生产环境必须自定义。
把复杂技术讲清楚。
是为了更好地解决问题。
今晚可以睡个安稳觉了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/2 7:43:24

Prompt 结构设计:拆解一个可复用的模板引擎

系列导读 你现在看到的是《Prompt Engineering 生产级实战:从零构建可落地的提示工程体系》的第 2/10 篇,当前这篇会重点解决:将 Prompt 当作代码管理,提升团队协作和系统稳定性。 上一篇回顾:第 1 篇《Prompt Engineering 入门:为什么你的提示词总是不靠谱?》主要聚焦…

作者头像 李华