当前位置: 首页 > news >正文

分布式场景下防止【缓存击穿】的不同方案

在高并发的业务场景中,缓存作为缓解数据库压力的关键组件,其稳定性直接影响整个系统的性能。而缓存击穿,作为缓存体系中常见的问题之一,始终是开发者需要重点攻克的难关。当缓存未命中时,大量请求会瞬间涌向数据库,极易造成数据库过载甚至宕机。本文将介绍三种方案来有效应对缓存击穿。

Redission RLock

Redission的RLock就是分布式场景下的ReentrantLock。

用 ReentrantLock 当锁组件时,底层是AQS,锁的作用只在一台JVM主机里。在分布式微服务场景下,可能存在多台JVM同时调用一个服务,那么锁将不再起作用。因此需要RLock来作为分布式场景下的全局锁,它的底层是Redis,可以做到跨 JVM、跨服务器共享资源控制,因此这是分布式场景锁的最好选择。

以 “热点综艺节目的详情查询” 为例:

  • 某热门综艺更新后,短时间内有 10 万用户同时查询该节目的嘉宾、播放时间等信息。
  • 该节目的缓存因设置了 24 小时过期时间,恰好在此刻失效。
  • 10 万请求瞬间穿透缓存,全部涌向节目数据库的program表,导致数据库连接池耗尽、查询超时,最终引发 “服务不可用”。

此时,若不做任何防护,数据库将成为整个系统的 “单点故障点”。接下来,我们从基础方案开始,逐步优化这一问题。

基础有两种方案:

  • 设置热点数据永不过期:虽然缓存不会失效导致缓存击穿,但缓存数据不会得到及时更新,存在数据不一致的问题
  • 分布式锁:当检测到缓存失效时,先加上分布式锁,获取锁成功后再进入操作数据库、重新构建缓存的流程。这样之后其他请求访问时能够直接获取到缓存数据返回。

以下是分布式锁的代码示例

public String getData(String id){RedisTemplate<String,String> redisTemplate = redisCache.getInstance();String cachedValue = redisTemplate.opsForValue().get(id);if (StringUtil.isEmpty(cachedValue)) {//分布式锁RLock lock = serviceLockTool.getLock(LockType.Reentrant, id);lock.lock();try {Program program = programMapper.selectById(id);if (Objects.nonNull(program)) {redisTemplate.opsForValue().set(id,JSON.toJSONString(program));cachedValue = JSON.toJSONString(program);}} finally {lock.unlock();}}return cachedValue;
}

Redission RLock + 双重检测

但上述方法也存在很大的问题,当大量请求进行锁竞争,只有一个请求能获取到锁并重新构建缓存,其他阻塞的请求获取锁后依然会访问数据库,并没有用到新构建好的缓存。数据库的压力依旧很大。

其实完全没有必要都去查询数据库的,当第一个请求从数据库查询出来放入缓存后,之后的请求都应该从缓存中查询。

因此就需要在原有的基础上加上双重检测,在获取锁后请求访问数据库前在检测一次缓存中是否存在想要的数据,如果有则直接返回。

public String getDataV3(String id){RedisTemplate<String,String> redisTemplate = redisCache.getInstance();String cachedValue = redisTemplate.opsForValue().get(id);if (StringUtil.isEmpty(cachedValue)) {RLock lock = serviceLockTool.getLock(LockType.Reentrant, id);lock.lock();try {cachedValue = redisTemplate.opsForValue().get(id);if (StringUtil.isEmpty(cachedValue)) {Program program = programMapper.selectById(id);if (Objects.nonNull(program)) {redisTemplate.opsForValue().set(id,JSON.toJSONString(program));cachedValue = JSON.toJSONString(program);}}} finally {lock.unlock();}}return cachedValue;
}

同样的想法也可以见单例模式的构建。

public class DCLSingleton {// 单例private static volatile Singleton singleton = null;// 私有构造方法private Singleton() {}public static Singleton getInstance() {if (null == singleton) {synchronized (Singleton.class) {if (null == singleton) {singleton = new Singleton();}}}return singleton;}
}

Redission RLock + 双重检测 + tryLock

但上面的方法依旧存在问题,按理说当第一个请求成功构建完缓存后,其他请求就不应该竞争锁了,他们可以直接访问缓存来获取数据。Redission RLock + 双重检测之后的请求依然要锁竞争、串行执行,浪费了很多时间。

假设某热门节目的并发请求有 100 万:

  • 第一个请求获取锁,查库、重建缓存(耗时约 50ms)。
  • 剩余 999,999 个请求会阻塞在lock.lock()处,等待锁释放。
  • 即使 Redis 处理锁请求的耗时仅 1ms,最后一个请求也需要等待约 999,999ms(约 16 分钟)才能获取锁 —— 这显然无法满足高并发场景的低延迟需求。

问题的核心在于:lock.lock()是 “无限等待” 的,即使缓存已重建,后续请求仍需排队获取锁,而不是直接从缓存读取数据。

tryLock可以帮助解决问题。tryLock是 Redisson RLock 的核心方法之一,其逻辑是:

  • 尝试在指定时间内(如 1 秒)获取锁;
  • 若在超时前获取到锁,则执行后续逻辑(查缓存、查库);
  • 若超时仍未获取到锁,则直接返回 “获取失败”,此时请求可再次尝试查缓存(因第一个请求可能已重建缓存),无需继续阻塞。

通过tryLock,我们将 “无限阻塞” 改为 “有限等待”,让大部分请求在等待一小段时间后,直接从缓存获取数据,大幅降低延迟。

因此这个方法就是针对减少锁竞争做的优化。

private ProgramVo getById(Long programId,Long expireTime,TimeUnit timeUnit) {//先从缓存中查询ProgramVo programVo = redisCache.get(RedisKeyBuild.createRedisKey(RedisKeyManage.PROGRAM, programId), ProgramVo.class);//如果存在直接返回数据if (Objects.nonNull(programVo)) {return programVo;}//加锁RLock lock = serviceLockTool.getLock(LockType.Reentrant, GET_PROGRAM_LOCK, new String[]{String.valueOf(programId)});boolean lockResult;try {//等待锁的时间为1s,超过1s后,继续执行lockResult = lock.tryLock(1, TimeUnit.SECONDS);} catch (InterruptedException e) {throw new DaMaiFrameException("线程中断异常",e);}try {//再从缓存中查询,如果缓存不存在则从数据库中查询再放入到缓存中return redisCache.get(RedisKeyBuild.createRedisKey(RedisKeyManage.PROGRAM,programId),ProgramVo.class,() -> createProgramVo(programId),expireTime,timeUnit);}finally {if (lockResult) {lock.unlock();}}
}

这里是将锁的竞争方式从lock改为了tryLock等待时间为1s意思也就是说请求等待锁的时间为1s,如果1s后还没有获得到锁的后,就不再继续等待,直接返回获得锁的结果,程序接着往下执行.

但这种方法依然存在着问题,如何判断第一个请求构建缓存的时间?如果1s没有构建完,将要会有大量请求打入数据库,依然会有缓存击穿的问题。如果构建时间过长,又会造成不必要的资源浪费。

这就需要根据业务来设置等待锁的时间,设置一个比较底限的值,保证不会超出的一个时间,但想设置一个百分之百不能超过的一个值,确实需要经过大量时间的业务运算来进行估算。

http://www.dtcms.com/a/456797.html

相关文章:

  • 《Cargo 参考手册》第二章:工作区(Workspaces)
  • 2025山西旅游攻略(个人摩旅版-国庆从北京到山西)
  • 博弈论——一些概念
  • 注册安全工程师资源合集
  • C++ 位运算 高频面试考点 力扣 面试题 17.19. 消失的两个数字 题解 每日一题
  • 深圳著名设计网站wordpress 目录配置
  • Benders 文献推荐
  • 【C语言基础详细版】08. 结构体、共用体、枚举详解:从定义到内存管理
  • 整理 tcp 服务器的设计思路
  • 域名备案未做网站个人做广播网站需要注意什么
  • https私人证书 PKIX path building failed 报错解决
  • 在线点餐收银系统会员卡管理系统模板餐饮收银充值积分时卡储值预约小程序
  • [嵌入式embed]Keil5-STM32F103C8T6(江协科技)+移植RT-Thread v3.15模版
  • 苹果(Apple)发展史:用创新重塑科技与生活的传奇征程
  • 网站开发零基础培训学校wordpress主题开发编辑器
  • OAuth2.0与CSP策略在SPA应用中的联合防御模型
  • 面向院区病房的空间智能体新范式:下一代病房系统研究(中)
  • Postman 请求前置脚本
  • 前端学AI:如何写好提示词(prompt)
  • Typescript》》TS》》Typescript 3.8 import 、import type
  • Python全栈(基础篇)——Day07:后端内容(函数的参数+递归函数+实战演示+每日一题)
  • 对抗样本:深度学习的隐秘挑战与防御之道
  • 通用:MySQL-InnoDB事务及ACID特性
  • 重庆江津网站建设企业专业网站设计公
  • 天津市武清区住房建设网站临沂天元建设集团网站
  • MySQL 锁机制深度解析:原理、场景、排查与优化​
  • Spring 的统一功能
  • 忘记php网站后台密码wordpress 医院模板下载
  • asp 网站卡死网站域名解析ip
  • Linux小课堂: 在 VirtualBox 虚拟机中安装 CentOS 7 的完整流程与关键技术详解