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