高并发下缓存失效问题-缓存穿透、缓存击穿、缓存雪崩、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、缓存命中便直接return
        return JSON.parseObject(categoryListFromCache, new TypeReference<>() {});
    }

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

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

缓存失效问题—缓存穿透

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

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

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

缓存失效问题—缓存击穿

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

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

一、单机锁

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

代码实现如下所示:

    @Override
    public 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、缓存命中便直接return
        return 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.mfbz.cn/a/209068.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

zookeeper心跳检测 (实操课程)

本系列是zookeeper相关的实操课程&#xff0c;课程测试环环相扣&#xff0c;请按照顺序阅读来学习和测试zookeeper。 阅读本文之前&#xff0c;请先阅读----​​​​​​zookeeper 单机伪集群搭建简单记录&#xff08;实操课程系列&#xff09;zookeeper 客户端常用命令简单记录…

nodejs微信小程序+python+PHP学科建设管理信息系统的设计与实现-计算机毕业设计推荐

目 录 摘 要 I ABSTRACT II 目 录 II 第1章 绪论 1 1.1背景及意义 1 1.2 国内外研究概况 1 1.3 研究的内容 1 第2章 相关技术 3 2.1 nodejs简介 4 2.2 express框架介绍 6 2.4 MySQL数据库 4 第3章 系统分析 5 3.1 需求分析 5 3.2 系统可行性分析 5 3.2.1技术可行性&#xff1a;…

11.28~11.29基本二叉树的性质、定义、复习;排序算法;堆

完全二叉树&#xff08;Complete Binary Tree&#xff09;是一种特殊的二叉树结构&#xff0c;它具有以下特点&#xff1a; 所有的叶子节点都集中在树的最后两层&#xff1b;最后一层的叶子节点都靠左排列&#xff1b;除了最后一层&#xff0c;其他层的节点数都达到最大值。 …

如何快速看懂市场行情?

一、看大盘指数 咱们平时所说的大盘其实指的就是上证指数&#xff0c;它是整个市场的晴雨表。大盘涨了&#xff0c;个股跟着上涨的概率就大&#xff0c;大盘跌了&#xff0c;个股被拖累下跌的概率也大。所以&#xff0c;要想在股市中尝到甜头&#xff0c;大盘分析是少不了滴&am…

Django HMAC 请求签名校验与 Vue.js 实现安全通信

概要 在 Web 应用的开发过程中&#xff0c;确保数据传输的安全性和完整性是一个不容忽视的问题。使用 HMAC&#xff08;Hash-based Message Authentication Code&#xff09;算法对请求内容进行签名校验&#xff0c;是一种常见且有效的安全策略。本文将详细介绍如何在 Django …

[1] AR Tag 在ros中的使用

1.定义 AR Tag 是一种用于增强现实&#xff08;AR&#xff09;应用中的视觉标记&#xff0c;用于跟踪和定位虚拟物体在现实世界中的位置。 AR Tag由黑白正方形图像表示&#xff0c;图像内部有黑色边框中的某些图案。它与我们经常用到的二维码长得类似&#xff0c;原理其实也一…

STM32内部温度传感器使用方法详解

STM32内部温度传感器使用方法详解 前言 STM32内部集成了一个片上温度传感器&#xff0c;可以用来测量MCU及周围的温度。测量范围&#xff1a;-40~125&#xff0c;精度1.5℃。虽然精度不高&#xff0c;但在某些应用场景下是够了的&#xff0c;相比于外部接入传感器&#xff0c…

nodejs微信小程序+python+PHP金融产品销售系统的设计与实现-计算机毕业设计推荐

目 录 摘 要 I ABSTRACT II 目 录 II 第1章 绪论 1 1.1背景及意义 1 1.2 国内外研究概况 1 1.3 研究的内容 1 第2章 相关技术 3 2.1 nodejs简介 4 2.2 express框架介绍 6 2.4 MySQL数据库 4 第3章 系统分析 5 3.1 需求分析 5 3.2 系统可行性分析 5 3.2.1技术可行性&#xff1a;…

Windows server 2016 FTP服务器的搭建

FTP&#xff08;File Transfer Protocol&#xff09;是一个用来在两台计算机之间传输文件的通信协议。这两台计算机中&#xff0c;一台是FTP服务器&#xff0c;另一台是FTP 客户端。 1.安装FTP服务与建立FTP站点 1.1 打开服务器管理器——单击仪表盘的添加角色和功能 1.2 持续…

【计算机网络笔记】PPP协议

系列文章目录 什么是计算机网络&#xff1f; 什么是网络协议&#xff1f; 计算机网络的结构 数据交换之电路交换 数据交换之报文交换和分组交换 分组交换 vs 电路交换 计算机网络性能&#xff08;1&#xff09;——速率、带宽、延迟 计算机网络性能&#xff08;2&#xff09;…

C语言:写一个函数,输入一个十六进制数,输出相应的十进制数

分析&#xff1a; 当用户运行该程序时&#xff0c;程序会提示用户输入一个十六进制数。用户需要在命令行中输入一个有效的十六进制数&#xff0c;例如&#xff1a;"1A3F"。 接下来&#xff0c;程序调用了名为 xbed 的函数&#xff0c;并将用户输入的十六进制数作…

Linux 权限管理

1 Linux 安全模型 AAA认证资源分派&#xff1a; 当用户登录时&#xff0c;系统会自动分配令牌 token&#xff0c;包括用户标识和组成员等等信息 1.1 用户 Linux 中每个用户是通过 User ID&#xff08;UID&#xff09;来唯一标识的。 1.2 用户组 Linux 中可以将一个或者多个…

limit 10和limit 10000 10效率相同吗

先说结论&#xff1a;不相同&#xff0c;差异很大。 set profiling 1;select * from xiatui order by id limit 10000,10;select * from xiatui order by id limit 10;show profiles; select * from xiatui order by name limit 90000,10;select * from xiatui order by name…

使用 NRF24L01 无线收发模块进行远程控制

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

k8s报错

报错&#xff1a; 这个错误信息表明你的容器运行时&#xff08;container runtime&#xff09;没有正常运行&#xff0c;具体是因为CRI&#xff08;容器运行时接口&#xff09;v1版本的API没有为特定的端点实现。这通常发生在使用containerd作为容器运行时时。错误信息中提到的…

@RequestMapping处理请求异常

使用RequestMapping不指定请求方式&#xff0c;多种请求方式都支持。 Get格式FORM_URLENCODED Content-Typeapplication/x-www-form-urlencoded URL形式传参&#xff0c;请求体里面的内容是&#xff1a;usernamejohnexample.com&passwordsecretpassword&grant_type…

1.2 Ubauntu 使用

一、完成VMware Tools安装 双击 VMwareTool 打开 Ubuntu 终端快捷键 AltControlT 切换汉语的快捷键是Alt空格 ls 打印出当前所在目录中所有文件和文件夹 cd 桌面 进入桌面文件夹 sudo ./vmware-install.pl 安装tool&#xff0c;输入之前设置的密码。 地址默认&#xff0c;按…

论文阅读——Img2LLM(cvpr2023)

arxiv&#xff1a;[2212.10846] From Images to Textual Prompts: Zero-shot VQA with Frozen Large Language Models (arxiv.org) 一、介绍 使用大语言模解决VQA任务的方法大概两种&#xff1a;multi-modal pretraining and language-mediated VQA&#xff0c;即多模态预训练…

web:[NPUCTF2020]ReadlezPHP

题目 打开页面显示如下 没发现其他的线索&#xff0c;查看源代码 发现一个网址&#xff0c;访问这个页面查看 进行代码审计 这段代码是一个简单的 PHP 类&#xff0c;名为 HelloPhp。它有两个公共属性 $a 和 $b&#xff0c;并在构造函数中将它们分别初始化为字符串 "Y-m-…

Linux命令与shell脚本编程大全【读书笔记 + 思考总结】

Linux命令与shell脚本编程大全 第 1 章 初识Linux shellLinux的组成及关系结构图是什么&#xff1f;Linux系统内核的作用是什么&#xff1f;内核的主要功能是什么&#xff1f;&#xff08;4点&#xff09;物理内存和虚拟内存是什么关系&#xff1f;内核如何实现虚拟内存&#x…