高并发下缓存失效问题-缓存穿透、缓存击穿、缓存雪崩、Redis分布式锁简单实现、Redisson实现分布式锁

文章目录

  • 缓存基本使用范式暴露的几个问题
  • 缓存失效问题---缓存穿透
  • 缓存失效问题---缓存击穿
    • 一、单机锁
      • 正确的锁粒度
      • 不正确的锁粒度无法保证查询数据库次数是唯一
    • 二、分布式锁
      • getCatalogJsonData()
      • 分布式锁演进---基本原理
      • 分布式锁(加锁)演进一:删锁失败导致死锁
      • 分布式锁(加锁)演进二:给‘锁’设置过期时间防止死锁
      • 分布式锁(加锁)演进三:必须保证过期时间和占锁动作原子性
      • 分布式锁(解锁)演进一:业务逻辑执行时间大于‘锁’的过期时间
      • 分布式锁(解锁)演进二:UUID保证删除的是自己的‘锁’
      • 分布式锁(解锁)演进三:lua脚本保证删‘锁’原子性
    • 三、锁的自动续期
    • 四、Redis简单实现分布式锁的完整代码
  • 缓存失效问题---缓存雪崩
  • 分布式锁---Redisson

缓存基本使用范式暴露的几个问题

{1、先查询缓存2、if(缓存没有命中){2.1、查询数据库2.2、查询结果放入缓存2.3、同时return结果}3、缓存命中直接return缓存数据
}

如下;使用缓存高效的查询‘三级分类’数据,就完全遵循上面提到的范式

    public Map<Long, List<Catalog2VO>> getCatalogJsonBaseMethod() {String key = ProductConstant.RedisKey.INDEX_CATEGORY_JSON;// 1、从缓存中获取数据String categoryListFromCache = redisTemplate.opsForValue().get(key);if (!StringUtils.hasText(categoryListFromCache)) {// 2.1、缓存没有命中,查询数据库Map<Long, List<Catalog2VO>> catalogJsonFromDB = getCatalogJsonFromDB();// 2.2、将查询结果放入缓存redisTemplate.opsForValue().set(key,JSON.toJSONString(catalogJsonFromDB));return catalogJsonFromDB;}// 3、缓存命中便直接returnreturn JSON.parseObject(categoryListFromCache, new TypeReference<>() {});}

该范式在高并发、分布式下会暴露以下几个问题,这也是本章需要解决和讨论的点

  • 高并发缓存失效之缓存穿透
  • 高并发缓存失效之缓存击穿
  • 高并发缓存失效之缓存雪崩
  • 分布式架构下的分布式锁

缓存失效问题—缓存穿透

请求查询一个百分百不存在的数据

假设id=idooy这条记录在数据库中压根不存在;按照请求处理逻辑先查询缓存,但因为这本就是一条不存在的记录(假设成立),因此缓存也不可能命中,缓存不命中接着就会查询数据库;如果没有将这一次请求查询的null写入缓存,这将导致id=idooy这条请求每次都要去数据库,直接失去了缓存的意义

风险: 利用不存在的数据发送大量请求,数据库瞬时压力增大,最终导致数据库崩溃
解决: 将null结果进行缓存,并加入短暂的过期时间;有时查询固定的值,不需要请求携带参数,这种情况本身就不会出现缓存穿透

缓存失效问题—缓存击穿

某一个Key在高并发请求期间刚好过期失效

对于一个设置了过期时间的Key,如果这个Key在将来的某个时间被高并发访问期间刚好过期失效,那么高并发的请求压力直接给到数据库
解决: 加锁;对同一个Key的高并发请求保证只有一个请求打给数据库;其他请求等待并最终从缓存中获取;下面讨论单机锁分布式锁

一、单机锁

单机锁是指在单体应用中或同一个进程中利用锁的排他性保证高并发期间某个Key失效时只有一个请求去数据库进行查询来避免缓存击穿

代码实现如下所示:

    @Overridepublic Map<Long, List<Catalog2VO>> getCatalogJson() {String key = ProductConstant.RedisKey.INDEX_CATEGORY_JSON;// 1、从缓存中获取数据String categoryListFromCache = redisTemplate.opsForValue().get(key);if (!StringUtils.hasText(categoryListFromCache)) {// 2、缓存没有命中,查询数据库,加锁保证数据库只查询一次// 因为当前this实例为单例,故可以作为锁资源使用synchronized (this) {// 2.1、高并发下必然有N个请求同时等待竞争锁,所以竞争到锁的第一件事就是再查一遍缓存String result = redisTemplate.opsForValue().get(key);if (StringUtils.hasText(result)) {return JSON.parseObject(result, new TypeReference<>() {});}// 2.2、缓存依旧没有命中的情况下查询数据库Map<Long, List<Catalog2VO>> catalogJsonFromDB = getCatalogJsonFromDB();// 2.3、将查询结果放入缓存redisTemplate.opsForValue().set(key,JSON.toJSONString(catalogJsonFromDB),2,TimeUnit.HOURS);return catalogJsonFromDB;}}// 3、缓存命中便直接returnreturn JSON.parseObject(categoryListFromCache, new TypeReference<>() {});}

正确的锁粒度

在这里插入图片描述

不正确的锁粒度无法保证查询数据库次数是唯一

在这里插入图片描述
在这里插入图片描述

二、分布式锁

上面单机锁本质就是使用当前进程中的某个单例对象充当锁资源;在微服务架构分布式部署下,同一个商品服务可能部署N多个,此时每个服务进程之间相互隔离。

在这里插入图片描述
因此;本地锁,只能锁住当前进程,分布式架构下需要分布式锁

getCatalogJsonData()

    private Map<Long, List<Catalog2VO>> getCatalogJsonData() {String key = ProductConstant.RedisKey.INDEX_CATEGORY_JSON;String result = redisTemplate.opsForValue().get(key);if (!StringUtils.hasText(result)) {Map<Long, List<Catalog2VO>> catalogJsonFromDB = getCatalogJsonFromDB();redisTemplate.opsForValue().set(key,JSON.toJSONString(catalogJsonFromDB),2,TimeUnit.HOURS);return catalogJsonFromDB;}return JSON.parseObject(result, new TypeReference<>() {});}

分布式锁演进—基本原理

所有的‘商品服务’可以同时去一个地方“占坑”,如果占到就执行逻辑,否则就必须等待,直到释放锁。
“占坑”可以去Redis,也可以去数据库,可以去任何只要“商品服务”都能访问到的地方
在这里插入图片描述

分布式锁(加锁)演进一:删锁失败导致死锁

在这里插入图片描述
如上图;执行业务逻辑出现异常或者在删锁前系统宕机(kill -9);直接导致没有执行删锁操作。那么其他请求就无法"成功占锁",造成死锁。

接下来给"锁"设置过期时间防止死锁。即使删锁失败也会自动删除

分布式锁(加锁)演进二:给‘锁’设置过期时间防止死锁

在这里插入图片描述
所以,“占锁+设置过期时间”必须保证原子性

分布式锁(加锁)演进三:必须保证过期时间和占锁动作原子性

Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "ok",3,TimeUnit.SECONDS);

在这里插入图片描述

分布式锁(解锁)演进一:业务逻辑执行时间大于‘锁’的过期时间

业务逻辑执行时间超过‘锁’的过期时间;这也就意味着业务逻辑执行完毕以后删的就不是自己的锁。
试想如下高并发场景下, 假设‘锁’的过期时间为10s,业务的执行时间为15s;

①号请求执行到第10s,‘锁’自动过期;②号请求立马占锁成功执行业务逻辑。
在第15s①号业务逻辑执行完毕,成功删除锁。很显然此时①号删除的就不是自己的锁(自己的锁在第10s的时候已自动删除了),而是②号的锁。
同时在15s这一时刻①号删了②号的锁;接着3号占锁成功,如此情况下‘锁永久失效’

在这里插入图片描述
该况下暴露的问题本质就是锁删除了他人的锁;那么接下来就通过唯一ID保证线程删除的是自己的锁

分布式锁(解锁)演进二:UUID保证删除的是自己的‘锁’

在占锁的时候,值指定为uuid,每个人匹配是自己的锁才删除
在这里插入图片描述
如图;问题还是暴露了出来。get(“lock”)并且equals成立,此时锁刚好自动过期删除了,另一个线程占锁成功了,此时再执行delete删锁同样删除的不是自己的锁。
所以这个问题的本质就是删锁的过程不能保证原子性

分布式锁(解锁)演进三:lua脚本保证删‘锁’原子性

如下图;官方提供了‘解锁’的建议和保证解锁过程原子性的lua脚步

  • 锁的值不要设置固定字符串,而是设置一个不可猜测的大随机字符串,称为token。
  • 不是用DEL释放锁,而是发送一个脚本,仅在值匹配时才删除键
    在这里插入图片描述

根据官方提示;解锁的核心业务代码片段

// 解锁
redisTemplate.execute(new DefaultRedisScript<>(getLuaScript(),Long.class),Arrays.asList("lock"),uuid);private String getLuaScript(){return "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n" +"then\n" +"    return redis.call(\"del\",KEYS[1])\n" +"else\n" +"    return 0\n" +"end";}

在这里插入图片描述

三、锁的自动续期

业务执行时间超长;业务逻辑还未执行完毕‘锁’自动过期了,最简单的方式就是给‘锁’设置足够长的时间。
但完美的解决该问题,自己写代码实现还是很困难的,所以这个问题就抛出Redisson,它提供的分布式锁会解决上面提到的所有问题;包括锁的自动续期

四、Redis简单实现分布式锁的完整代码

  • 加锁原子性命令;保证’设置过期时间和占锁’是原子性操作
  • 解锁原子性命令;uuid保证删的是自己的锁;lua脚本保证了删锁的原子性
  • 设置‘锁’的过期时间足够长,确保业务逻辑执行时间不会超过过期时间这种简单粗暴的方式来解决‘锁’过期自动续期的问题
private Map<Long, List<Catalog2VO>> getCatalogJsonWithRedisLock() {// 所有的请求进来先占坑,即抢占锁String uuid = UUID.randomUUID().toString();// 原子性命令;保证'设置过期时间和占锁'是原子性操作Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid,3,TimeUnit.SECONDS);if (lock) {// "占坑"成功,执行业务逻辑Map<Long, List<Catalog2VO>> result;try {result = getCatalogJsonData();} finally {// 解锁:uuid保证删的是自己的锁;lua脚本保证了删锁的原子性redisTemplate.execute(new DefaultRedisScript<>(getLuaScript(),Long.class),Arrays.asList("lock"),uuid);}return result;}else {// "占坑"失败,自旋try {// 防止栈溢出TimeUnit.MILLISECONDS.sleep(200);} catch (InterruptedException e) {throw new RuntimeException(e);}return getCatalogJsonWithRedisLock();}}private String getLuaScript(){return "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n" +"then\n" +"    return redis.call(\"del\",KEYS[1])\n" +"else\n" +"    return 0\n" +"end";}private Map<Long, List<Catalog2VO>> getCatalogJsonData() {String key = ProductConstant.RedisKey.INDEX_CATEGORY_JSON;String result = redisTemplate.opsForValue().get(key);if (!StringUtils.hasText(result)) {Map<Long, List<Catalog2VO>> catalogJsonFromDB = getCatalogJsonFromDB();redisTemplate.opsForValue().set(key,JSON.toJSONString(catalogJsonFromDB),2,TimeUnit.HOURS);return catalogJsonFromDB;}return JSON.parseObject(result, new TypeReference<>() {});}

缓存失效问题—缓存雪崩

某一时刻大量的Key同时失效

假设缓存中大量的Key使用了相同过期时间,这直接导致在将来的某个时刻这些Key同时失效;此时再大量请求这些Key压力都来到了数据库,使数据库瞬时压力过大可能出现崩溃
解决: 再原有的失效时间上增加一个随机值,这样每个缓存的过期时间的重复率就会很低,也就很难出现Key大面积同时失效导致缓存雪崩问题

// 再原有的失效时间基础上添加随机时间片
// 这里没有增加随机时间片,因为Key的数量有限,足以保证失效时间的离散分布
redisTemplate.opsForValue().set(key,JSON.toJSONString(catalogJsonFromDB),2,TimeUnit.HOURS);

分布式锁—Redisson

上面基于Redis的setnx命令简单的实现了一个分布式锁,并在实现的过程中暴露出许多问题,也都一一的解决了。但是官方建议还是使用redlock来实现分布式锁

注意:Redlock算法实现起来稍微复杂一点,但提供了更好的保证和容错能力
在这里插入图片描述
这里边就存在针对Java的实现Redisson
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/207661.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

负电源电压转换-TP7660H

负电源电压转换-TP7660H 简介引脚说明典型应用电路倍压与反压的应用电路 简介 TP7660H 是一款 DC/DC 电荷泵电压反转器专用集成电路。芯片能将输入范围为 2.5V&#xff5e;11V 的电压转换成相应的-2.5V&#xff5e;-11V 的输出&#xff0c;电压转换精度可达99.9%&#xff0c;电…

Docker的常用基本命令(基础命令)

文章目录 1. Docker简介2. Docker环境安装Linux安装 3. 配置镜像加速4. Docker镜像常用命令列出镜像列表搜索镜像下载镜像查看镜像版本删除镜像构建镜像推送镜像 5. Docker容器常用命令新建并启动容器列出容器停止容器启动容器进入容器删除容器&#xff08;慎用&#xff09;查看…

概率论与数理统计中常见的随机变量分布律、数学期望、方差及其介绍

1 离散型随机变量 1.1 0-1分布 设随机变量X的所有可能取值为0与1两个值&#xff0c;其分布律为 若分布律如上所示&#xff0c;则称X服从以P为参数的(0-1)分布或两点分布。记作X~ B(1&#xff0c;p) 0-1分布的分布律利用表格法表示为: X01P1-PP 0-1分布的数学期望E(X) 0 *…

面向对象编程的艺术:构建高效可扩展的软件

&#x1f90d; 前端开发工程师&#xff08;主业&#xff09;、技术博主&#xff08;副业&#xff09;、已过CET6 &#x1f368; 阿珊和她的猫_CSDN个人主页 &#x1f560; 牛客高级专题作者、在牛客打造高质量专栏《前端面试必备》 &#x1f35a; 蓝桥云课签约作者、已在蓝桥云…

zabbix6.4.0配置邮件及企微机器人群聊告警

一、邮件告警 根据公司邮箱自行配置&#xff0c;电子邮件、用户账号密码填自己的邮箱账号密码 动作本次使用的默认的&#xff0c;如果为了更加美观可自行修改。 二、企业微信机器人告警 首先在企微上创建群聊&#xff0c;之后添加群聊机器人 将地址复制&#xff0c;后面用 …

使用NVM管理多个版本的node.js

1、nvm介绍&#xff1a; nvm全英文也叫node.js version management&#xff0c;是一个nodejs的版本管理工具。nvm是node.js版本管理工具&#xff0c;为了解决node.js各种版本存在不兼容现象可以通过它可以安装和切换不同版本的node.js 2、下载nvm地址&#xff1a; https://d…

测试用例设计方法六脉神剑——第一剑:入门试招,等价边界初探 | 京东物流技术团队

1 背景及问题 G.J.Myers在<软件测试技巧>中提出&#xff1a;测试是为了寻找错误而运行程序的过程&#xff0c;一个好的测试用例是指很可能找到迄今为止尚未发现的错误的测试&#xff0c; 一个成功的测试是揭示了迄今为止尚未发现的错误的测试。 对于新手来说&#xff0…

ChatGPT成为“帮凶”:生成虚假数据集支持未知科学假设

ChatGPT 自发布以来&#xff0c;就成为了大家的好帮手&#xff0c;学生党和打工人更是每天都离不开。 然而这次好帮手 ChatGPT 却帮过头了&#xff0c;莫名奇妙的成为了“帮凶”&#xff0c;一位研究人员利用 ChatGPT 创建了虚假的数据集&#xff0c;用来支持未知的科学假设。…

Flutter加固原理及加密处理

​ 引言 为了保护Flutter应用免受潜在的漏洞和攻击威胁&#xff0c;加固是必不可少的措施之一。Flutter加固原理主要包括代码混淆、数据加密、安全存储、反调试与反分析、动态加载和安全通信等多个方面。通过综合运用这些措施&#xff0c;可以提高Flutter应用的安全性&#xf…

从订阅式需求发展,透视凌雄科技DaaS模式增长潜力

订阅制&#xff0c;C端消费者早已耳熟能详&#xff0c;如今也凭借灵活、服务更新稳定的特点&#xff0c;逐渐成为B端企业服务的新热点。 比如对中小企业而言&#xff0c;办公IT设备等配套支出都必不可少&#xff0c;但收入本身并不稳定&#xff0c;购置大堆固定资产&#xff0…

利用 NRF24L01 无线收发模块实现传感器数据的无线传输

NRF24L01 是一款常用的无线收发模块&#xff0c;适用于远程控制和数据传输应用。本文将介绍如何利用 NRF24L01 模块实现传感器数据的无线传输&#xff0c;包括硬件的连接和配置&#xff0c;以及相应的代码示例。 一、引言 NRF24L01 是一款基于 2.4GHz 射频通信的低功耗无线收发…

Python实现FA萤火虫优化算法优化BP神经网络分类模型(BP神经网络分类算法)项目实战

说明&#xff1a;这是一个机器学习实战项目&#xff08;附带数据代码文档视频讲解&#xff09;&#xff0c;如需数据代码文档视频讲解可以直接到文章最后获取。 1.项目背景 萤火虫算法&#xff08;Fire-fly algorithm&#xff0c;FA&#xff09;由剑桥大学Yang于2009年提出 , …

RPG项目01_场景及人物动画管理器

基于“RPG项目01_UI登录”&#xff0c;新建一个文件夹名为Model&#xff08;模型&#xff09; 将资源场景拖拽至Model中 找到相应场景双击进入 红色报错部分Clear清掉即可&#xff0c;我们可以重做 接下来另存场景 起名为Game 点击保存 场景就保存至Scene中了 在文件夹下新创建…

Git修改远程仓库名称

1、先直接在远程点仓库名&#xff0c;然后左侧菜单栏找settings-general&#xff0c;然后直接修改工程名&#xff0c;保存即可。 2、还是在settings-general下&#xff0c;下拉找到Advanced点击Expand展开&#xff0c;然后下拉到最底部 在Change path里填入新的项目名称&#x…

Docker 环境中 Spring Boot 应用的 Arthas 故障排查与性能优化实战

&#x1f680; 作者主页&#xff1a; 有来技术 &#x1f525; 开源项目&#xff1a; youlai-mall &#x1f343; vue3-element-admin &#x1f343; youlai-boot &#x1f33a; 仓库主页&#xff1a; Gitee &#x1f4ab; Github &#x1f4ab; GitCode &#x1f496; 欢迎点赞…

LeetCode刷题---汉诺塔问题

个人主页&#xff1a;元清加油_【C】,【C语言】,【数据结构与算法】-CSDN博客 前言&#xff1a;这个专栏主要讲述递归递归、搜索与回溯算法&#xff0c;所以下面题目主要也是这些算法做的 我讲述题目会把讲解部分分为3个部分&#xff1a; 1、题目解析 2、算法原理思路讲解 …

TiDB 在咪咕云原生场景下的实践

导读 咪咕是中国移动旗下的视频科技公司&#xff0c;门户系统是其核心业务之一。 为满足用户的多样化需求&#xff0c;咪咕计划对其数据库进行升级。 经过对中国主流国产数据库的测试评估后&#xff0c;咪咕选择了 TiDB&#xff0c;并成功将其落地于门户系统云化项目。 TiDB 为…

HarmonyOS脚手架:UI组件之文本和图片

主要实现UI组件文本和图片的常见效果查看&#xff0c;本身功能特别的简单&#xff0c;其目的也是很明确&#xff0c;方便大家根据效果查看相关代码实现&#xff0c;可以很方便的进行复制使用&#xff0c;当然了&#xff0c;这些所谓的小功能都是开胃小菜&#xff0c;脚手架的最…

如何通过内网穿透实现远程访问Linux SVN服务

文章目录 前言1. Ubuntu安装SVN服务2. 修改配置文件2.1 修改svnserve.conf文件2.2 修改passwd文件2.3 修改authz文件 3. 启动svn服务4. 内网穿透4.1 安装cpolar内网穿透4.2 创建隧道映射本地端口 5. 测试公网访问6. 配置固定公网TCP端口地址6.1 保留一个固定的公网TCP端口地址6…

Python之Appium 2自动化测试(Android篇)

一、环境搭建及准备工作 1、Appium 2 环境搭建 请参考另一篇文章: Windows系统搭建Appium 2 和 Appium Inspector 环境 2、安装 Appium-Python-Client&#xff0c;版本要求3.0及以上 pip install Appium-Python-ClientVersion: 3.1.03、手机连接电脑&#xff0c;并在dos窗口…