Spring Boot 3.x 开发中缓存击穿防护的分布式锁实现问题详解

张开发
2026/4/11 3:15:16 15 分钟阅读

分享文章

Spring Boot 3.x 开发中缓存击穿防护的分布式锁实现问题详解
目录Spring Boot 3.x 开发中缓存击穿防护的分布式锁实现问题详解引言1. 问题表现分布式锁防护缓存击穿的典型故障2. 原因分析分布式锁的复杂性与常见误区2.1 分布式锁的基本要求2.2 缓存击穿场景的特殊性2.3 常见分布式锁实现的问题2.4 与 Spring Cache 注解的冲突3. 解决方案健壮的分布式锁防护缓存击穿3.1 推荐使用 Redisson 分布式锁3.2 核心代码使用分布式锁加载缓存3.3 与 Spring Cache 注解的集成方案3.4 处理锁超时与业务执行时间3.5 高并发下的性能优化3.6 分布式锁在 Redis 集群/哨兵模式下的可靠性3.7 锁的可重入性3.8 监控与异常处理4. 完整示例Spring Boot 3.x Redisson 防护缓存击穿4.1 依赖pom.xml4.2 配置类4.3 缓存服务完整版4.4 测试高并发场景5. 最佳实践总结6. 结语Spring Boot 3.x 开发中缓存击穿防护的分布式锁实现问题详解引言缓存击穿Cache Breakdown是指某个热点数据在缓存中过期失效的瞬间大量并发请求同时穿透缓存直接访问数据库导致数据库压力骤增甚至崩溃。与缓存穿透不同击穿针对的是存在但已过期的热点 Key。为了防护击穿常用手段是在查询数据库前加分布式锁确保只有一个线程去加载数据其他线程等待。然而分布式锁的实现并不简单在 Spring Boot 3.x 中开发者常常遇到锁超时、锁失效、死锁、重入、性能瓶颈、与 Spring Cache 注解冲突等一系列问题。本文将深入剖析这些疑难杂症并提供基于 Redisson 等成熟方案的完整解决方案。1. 问题表现分布式锁防护缓存击穿的典型故障现象 A使用synchronized或ReentrantLock在单机环境有效但部署多实例时锁失效多个节点同时加载数据击穿防护失败。现象 B分布式锁未设置超时时间导致线程异常退出后锁永远不释放形成死锁。现象 C锁超时时间设置过短业务线程还未完成数据库加载和缓存写入锁自动释放其他线程获得锁后重复加载。现象 D高并发下大量线程在锁上等待导致请求堆积响应延迟飙升。现象 E使用Cacheable和手动分布式锁混用导致锁未覆盖缓存查询逻辑依然有请求穿透。现象 F锁的重入问题同一线程在持有锁的情况下再次请求同一锁可能死锁取决于实现。现象 GRedis 分布式锁在哨兵或集群模式下主从切换时锁丢失导致多个客户端同时获得锁。2. 原因分析分布式锁的复杂性与常见误区2.1 分布式锁的基本要求互斥任何时刻只有一个客户端持有锁。无死锁最终总能释放锁。容错部分节点故障不影响锁的可用性。可重入可选同一线程可重复获取锁。2.2 缓存击穿场景的特殊性热点 Key 过期后大量请求同时到达。我们只需要一个线程去加载数据其他线程等待或直接返回旧数据如果允许。锁的粒度应该是每个 Key 独立而非全局锁。2.3 常见分布式锁实现的问题基于 Redis 的 SETNX EXPIRE 非原子操作容易导致锁未设置过期时间就宕机。不设置锁超时业务异常时锁永不释放。锁超时设置不合理业务执行时间 锁超时导致锁被自动释放其他线程获得锁后又重新加载造成数据库重复查询。使用synchronized锁字符串JVM 内部锁多实例无效。Redisson 等框架使用不当未配置看门狗机制或未正确设置 leaseTime。2.4 与 Spring Cache 注解的冲突Cacheable注解的方法会在缓存未命中时执行方法体如果方法体内已经加了分布式锁但Cacheable本身的拦截器在锁之前执行可能造成重复查询。正确做法是将锁逻辑放在Cacheable方法内部或使用自定义CacheLoader。3. 解决方案健壮的分布式锁防护缓存击穿3.1 推荐使用 Redisson 分布式锁Redisson 提供了完善的分布式锁实现支持自动续期看门狗、可重入、公平锁等特性是 Spring Boot 3.x 中缓存击穿防护的首选。依赖dependencygroupIdorg.redisson/groupIdartifactIdredisson-spring-boot-starter/artifactIdversion3.27.0/version/dependency配置application.ymlspring:redis:host:localhostport:63793.2 核心代码使用分布式锁加载缓存ServicepublicclassProductService{AutowiredprivateRedissonClientredissonClient;AutowiredprivateRedisTemplateString,ProductredisTemplate;AutowiredprivateProductRepositoryproductRepository;privatestaticfinalStringCACHE_KEY_PREFIXproduct:;publicProductgetProduct(Longid){StringcacheKeyCACHE_KEY_PREFIXid;// 1. 先查缓存ProductproductredisTemplate.opsForValue().get(cacheKey);if(product!null){returnproduct;}// 2. 缓存未命中尝试获取分布式锁锁粒度每个商品独立RLocklockredissonClient.getLock(lock:product:id);try{// 尝试加锁最多等待 3 秒锁自动续期看门狗默认30秒booleanlockedlock.tryLock(3,TimeUnit.SECONDS);if(locked){// 双重检查再次查询缓存避免在等待锁期间其他线程已加载productredisTemplate.opsForValue().get(cacheKey);if(product!null){returnproduct;}// 从数据库加载productproductRepository.findById(id).orElse(null);if(product!null){redisTemplate.opsForValue().set(cacheKey,product,Duration.ofMinutes(30));}else{// 防止穿透缓存空对象设置较短过期时间redisTemplate.opsForValue().set(cacheKey,null,Duration.ofMinutes(5));}returnproduct;}else{// 未获得锁可以短暂等待后重试或直接返回 null/旧值Thread.sleep(100);returngetProduct(id);// 递归重试注意栈溢出风险生产环境建议循环}}catch(InterruptedExceptione){Thread.currentThread().interrupt();thrownewRuntimeException(Lock interrupted,e);}finally{// 释放锁只有持有锁的线程才释放if(lock.isHeldByCurrentThread()){lock.unlock();}}}}关键点tryLock(3, TimeUnit.SECONDS)等待锁超时避免无限阻塞。Redisson 看门狗机制默认每 30 秒自动续期即使业务执行超过锁超时时间也不会自动释放只要线程还活着。这避免了锁超时设置过短导致重复加载。双重检查在获得锁后再次查询缓存防止在等待锁期间其他线程已加载完成。缓存空值防止缓存穿透。3.3 与 Spring Cache 注解的集成方案如果希望继续使用Cacheable可以通过自定义CacheManager和Cache实现在内部集成分布式锁。或者使用 Spring 的Cacheable的sync属性仅对本地缓存有效不适用于分布式。推荐手动控制锁更清晰。另一种方式使用 Spring Cache 的Cache.get(key, Callable)方法该方法本身在加载时使用了本地锁synchronized。但无法跨 JVM。可继承RedisCache并重写get方法加入分布式锁。3.4 处理锁超时与业务执行时间Redisson 看门狗默认自动续期无需担心业务执行超时。若业务确实可能超过 30 秒可在创建锁时指定leaseTime并自行管理续期。避免长时间持有锁数据库查询应尽量快速如使用索引、连接池锁内操作越少越好。可以将数据加载分为先快速判断是否存在再加载详情。3.5 高并发下的性能优化使用锁的粒度尽量小按 key 粒度而非全局锁。避免大量线程阻塞对于未获得锁的线程可立即返回一个默认值或提示稍后重试而非阻塞等待。或者使用tryLock非阻塞模式快速失败。异步加载将缓存加载任务提交到线程池当前请求返回旧值或默认值。3.6 分布式锁在 Redis 集群/哨兵模式下的可靠性Redisson 支持 Redis 哨兵、集群、单机模式其分布式锁基于 Redis 的 Lua 脚本保证原子性。但在主从切换时可能短暂出现多个客户端获得锁因为主节点锁数据未同步到从节点从节点升为主后丢失锁。Redisson 提供了RedissonRedLock红锁算法解决此问题但性能较差。根据业务对一致性的要求对数据一致性要求极高如金融可使用红锁。一般场景下主从切换概率低且缓存击穿防护允许极短暂重复加载最终一致使用普通锁即可。3.7 锁的可重入性Redisson 锁默认支持可重入同一线程多次获取同一锁会自动计数不会死锁。3.8 监控与异常处理记录锁获取失败次数、等待时长用于调优。如果数据库加载失败应释放锁并可能缓存失败标记如空对象防止反复击穿。确保finally中释放锁并使用isHeldByCurrentThread()判断避免释放不属于自己的锁。4. 完整示例Spring Boot 3.x Redisson 防护缓存击穿4.1 依赖pom.xmldependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-data-redis/artifactId/dependencydependencygroupIdorg.redisson/groupIdartifactIdredisson-spring-boot-starter/artifactIdversion3.27.0/version/dependency4.2 配置类ConfigurationpublicclassRedissonConfig{BeanpublicRedissonClientredissonClient(){ConfigconfignewConfig();config.useSingleServer().setAddress(redis://127.0.0.1:6379);returnRedisson.create(config);}}4.3 缓存服务完整版ServiceSlf4jpublicclassProductService{AutowiredprivateRedissonClientredissonClient;AutowiredprivateStringRedisTemplateredisTemplate;AutowiredprivateProductRepositoryproductRepository;privatestaticfinalDurationCACHE_TTLDuration.ofMinutes(30);privatestaticfinalDurationNULL_TTLDuration.ofMinutes(5);publicProductgetProduct(Longid){StringcacheKeyproduct:id;// 快速查缓存StringcachedredisTemplate.opsForValue().get(cacheKey);if(cached!null){if(null.equals(cached)){returnnull;}returnJsonUtils.fromJson(cached,Product.class);}RLocklockredissonClient.getLock(lock:product:id);booleanlockedfalse;try{lockedlock.tryLock(5,TimeUnit.SECONDS);// 等待5秒if(!locked){// 未获得锁快速返回或重试log.warn(Get lock timeout for product {},id);returnnull;// 或抛出友好提示}// 双重检查cachedredisTemplate.opsForValue().get(cacheKey);if(cached!null){returnnull.equals(cached)?null:JsonUtils.fromJson(cached,Product.class);}// 从数据库加载ProductproductproductRepository.findById(id).orElse(null);if(product!null){redisTemplate.opsForValue().set(cacheKey,JsonUtils.toJson(product),CACHE_TTL);}else{redisTemplate.opsForValue().set(cacheKey,null,NULL_TTL);}returnproduct;}catch(InterruptedExceptione){Thread.currentThread().interrupt();thrownewRuntimeException(e);}finally{if(lockedlock.isHeldByCurrentThread()){lock.unlock();}}}}4.4 测试高并发场景使用 JMeter 同时发起 1000 个请求访问同一个不存在的商品 ID观察数据库只被查询一次其余请求快速返回 null。5. 最佳实践总结优先使用 Redisson它解决了锁超时、自动续期、可重入等痛点避免手写 Lua 脚本。锁粒度必须细化按缓存 Key 独立加锁避免全局锁拖垮性能。双重检查获取锁后再次检查缓存防止重复加载。合理设置等待时间tryLock等待时间不宜过长避免请求堆积也不宜过短避免大量请求失败。缓存空值防止不存在的数据反复击穿数据库。监控锁状态通过日志或监控平台记录锁获取失败次数、锁等待时长及时发现性能瓶颈。测试覆盖模拟并发场景验证锁是否正常工作以及异常情况下锁能否正确释放。与 Spring Cache 集成时谨慎手动锁更可靠或自定义Cache实现。6. 结语缓存击穿是高并发系统中常见的性能杀手而分布式锁是防护击穿的利器。然而分布式锁的实现细节繁多稍有不慎就会引入新的问题。通过使用 Redisson 这类成熟的客户端结合细粒度锁、双重检查、缓存空值等策略可以在 Spring Boot 3.x 中高效、可靠地防止缓存击穿。希望本文的深入剖析与实战代码能帮助开发者在面临热点 Key 过期问题时从容构建坚固的防护体系。

更多文章