Redis缓存击穿问题及解决思路

2023-11-07

一.什么是缓存击穿

缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。

逻辑分析:假设线程1在查询缓存之后,本来应该去查询数据库,然后把这个数据重新加载到缓存的,此时只要线程1走完这个逻辑,其他线程就都能从缓存中加载这些数据了,但是假设在线程1没有走完的时候,后续的线程2,线程3,线程4同时过来访问当前这个方法, 那么这些线程都不能从缓存中查询到数据,那么他们就会同一时刻来访问查询缓存,都没查到,接着同一时间去访问数据库,同时的去执行数据库代码,对数据库访问压力过大

二.解决缓存击穿的方法

常见的解决方案有两种:

  • 互斥锁

  • 逻辑过期

2.1互斥锁

解决方案一、使用锁来解决:

因为锁能实现互斥性。假设线程过来,只能一个人一个人的来访问数据库,从而避免对于数据库访问压力过大,但这也会影响查询的性能,因为此时会让查询的性能从并行变成了串行,我们可以采用tryLock方法 + double check来解决这样的问题。

假设现在线程1过来访问,他查询缓存没有命中,但是此时他获得到了锁的资源,那么线程1就会一个人去执行逻辑,假设现在线程2过来,线程2在执行过程中,并没有获得到锁,那么线程2就可以进行到休眠,直到线程1把锁释放后,线程2获得到锁,然后再来执行逻辑,此时就能够从缓存中拿到数据了。

2.2逻辑过期

解决方案二、逻辑过期方案

方案分析:我们之所以会出现这个缓存击穿问题,主要原因是在于我们对key设置了过期时间,假设我们不设置过期时间,其实就不会有缓存击穿的问题,但是不设置过期时间,这样数据不就一直占用我们内存了吗,我们可以采用逻辑过期方案。

我们把过期时间设置在 redis的value中,注意:这个过期时间并不会直接作用于redis,而是我们后续通过逻辑去处理。

假设线程1去查询缓存,然后从value中判断出来当前的数据已经过期了,此时线程1去获得互斥锁,那么其他线程会进行阻塞,获得了锁的线程他会开启一个 线程去进行 以前的重构数据的逻辑,直到新开的线程完成这个逻辑后,才释放锁, 而线程1直接进行返回,假设现在线程3过来访问,由于线程线程2持有着锁,所以线程3无法获得锁,线程3也直接返回数据,只有等到新开的线程2把重建数据构建完后,其他线程才能走返回正确的数据。

这种方案巧妙在于,异步的构建缓存,缺点在于在构建完缓存之前,返回的都是脏数据。

2.3两种方案进行对比

互斥锁方案:由于保证了互斥性,所以数据一致,且实现简单,因为仅仅只需要加一把锁而已,也没其他的事情需要操心,所以没有额外的内存消耗,缺点在于有锁就有死锁问题的发生,且只能串行执行性能肯定受到影响

逻辑过期方案: 线程读取过程中不需要等待,性能好,有一个额外的线程持有锁去进行重构数据,但是在重构数据完成前,其他的线程只能返回之前的数据,且实现起来麻烦

三.利用互斥锁解决缓存击穿问题

核心思路:相较于原来从缓存中查询不到数据后直接查询数据库而言,现在的方案是 进行查询之后,如果从缓存没有查询到数据,则进行互斥锁的获取,获取互斥锁后,判断是否获得到了锁,如果没有获得到,则休眠,过一会再进行尝试,直到获取到锁为止,才能进行查询

如果获取到了锁的线程,再去进行查询,查询后将数据写入redis,再释放锁,返回数据,利用互斥锁就能保证只有一个线程去执行操作数据库的逻辑,防止缓存击穿

操作锁的代码:

核心思路:利用redis的setnx方法来表示获取锁。

该方法含义是redis中如果没有这个key,则插入成功,返回1,在stringRedisTemplate中返回true; 如果有这个key则插入失败,则返回0,在stringRedisTemplate返回false。

我们可以通过true,或者是false,来表示是否有线程成功插入key,成功插入的key的线程我们认为他就是获得到锁的线程。

//加锁
private boolean tryLock(String key) {
    Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
    return BooleanUtil.isTrue(flag);
}
//释放锁
private void unlock(String key) {
    stringRedisTemplate.delete(key);
}

操作代码:

public Shop queryWithMutex(Long id)  {
        //Redis前缀+id
        String key = CACHE_SHOP_KEY + id;
        // 1、从redis中查询商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get("key");
        // 2、判断是否存在
        if (StrUtil.isNotBlank(shopJson)) {
            // 存在,直接返回
            return JSONUtil.toBean(shopJson, Shop.class);
        }
        //判断命中的值是否是空值
        if (shopJson != null) {
            //返回一个错误信息
            return null;
        }
        // 4.实现缓存重构
        //4.1 获取互斥锁
        String lockKey = "lock:shop:" + id;
        Shop shop = null;
        try {
            boolean isLock = tryLock(lockKey);
            // 4.2 判断否获取成功
            if(!isLock){
                //4.3 失败,则休眠重试
                Thread.sleep(50);
                return queryWithMutex(id);
            }
            //4.4 成功,根据id查询数据库
             shop = getById(id);
            // 5.不存在,返回错误
            if(shop == null){
                 //将空值写入redis
                stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
                //返回错误信息
                return null;
            }
            //6.写入redis
            stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),CACHE_NULL_TTL,TimeUnit.MINUTES);

        }catch (Exception e){
            throw new RuntimeException(e);
        }
        finally {
            //7.释放互斥锁
            unlock(lockKey);
        }
        return shop;
    }

四.利用逻辑过期解决缓存击穿问题

需求:修改根据id查询商铺的业务,基于逻辑过期方式来解决缓存击穿问题

思路分析:当用户开始查询redis时,判断是否命中,如果没有命中则直接返回空数据,不查询数据库,而一旦命中后,将value取出,判断value中的过期时间是否满足,如果没有过期,则直接返回redis中的数据,如果过期,则在开启独立线程后直接返回之前的数据,独立线程去重构数据,重构完成后释放互斥锁。

步骤一、

新建一个实体类,我们采用第二个方案,这个方案,对原来代码没有侵入性。

@Data
public class RedisData {
    private LocalDateTime expireTime;
    private Object data;
}

步骤二、

ShopServiceImpl 新增此方法,利用单元测试进行缓存预热

在测试类中

步骤三:正式代码

ShopServiceImpl

private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
public Shop queryWithLogicalExpire( Long id ) {
    String key = CACHE_SHOP_KEY + id;
    // 1.从redis查询商铺缓存
    String json = stringRedisTemplate.opsForValue().get(key);
    // 2.判断是否存在
    if (StrUtil.isBlank(json)) {
        // 3.存在,直接返回
        return null;
    }
    // 4.命中,需要先把json反序列化为对象
    RedisData redisData = JSONUtil.toBean(json, RedisData.class);
    Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
    LocalDateTime expireTime = redisData.getExpireTime();
    // 5.判断是否过期
    if(expireTime.isAfter(LocalDateTime.now())) {
        // 5.1.未过期,直接返回店铺信息
        return shop;
    }
    // 5.2.已过期,需要缓存重建
    // 6.缓存重建
    // 6.1.获取互斥锁
    String lockKey = LOCK_SHOP_KEY + id;
    boolean isLock = tryLock(lockKey);
    // 6.2.判断是否获取锁成功
    if (isLock){
        CACHE_REBUILD_EXECUTOR.submit( ()->{

            try{
                //重建缓存
                this.saveShop2Redis(id,20L);
            }catch (Exception e){
                throw new RuntimeException(e);
            }finally {
                unlock(lockKey);
            }
        });
    }
    // 6.4.返回过期的商铺信息
    return shop;
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Redis缓存击穿问题及解决思路 的相关文章

  • Redis 块推送直到列表有空位

    我正在寻找类似的东西BLPUSH该命令将阻塞 直到列表的长度低于指定值max size 目的是防止生产者运行速度快于消费者时列表无限增长 功能与 python 非常相似Queue put https docs python org 3 li
  • python 3.5 中的 json.loads 和 Redis

    我使用 json dumps 创建了一个 JSON 对象 并在 Redis 列表中将其 RPUSH ed 当使用 LRANGE redis lrange 返回 JSON 时 我收到一个二进制字符串 b si 00 ff 所以 json lo
  • Stackexchange.redis 缺乏“WAIT”支持

    我在客户端应用程序正在使用的负载均衡器后面有 3 个 Web API 服务器 我正在使用这个库来访问具有一个主服务器和几个从服务器的 Redis 集群 目前不支持 WAIT 操作 我需要此功能来存储新创建的用户会话并等待它复制到所有从属服务
  • 使用 AWS ElastiCache 请求中的 Airflow CROSSSLOT 密钥未散列到同一插槽错误

    我在 AWS ECS 上运行 apache airflow 1 8 1 并且有一个 AWS ElastiCache 集群 redis 3 2 4 运行 2 个分片 2 个启用多可用区的节点 集群 Redis 引擎 我已经验证气流可以毫无问题
  • 我的 Redis 自动生成的密钥

    我不知道我的 Redis 版本 4 0 9 到底发生了什么 我正在运行一个应用程序并使用 Redis 来存储我的数据库 但是 然后 Redis 自动创建 3 个新键 Backup1 Backup2 Backup3 并删除我的所有数据 这是我
  • 如何在多个Lua State(多线程)之间传递数据?

    我在中启动Redis连接池redis lua 通过从 C 调用 我得到了redis lua state 此 Lua 状态全局启动一次 仅在其他线程中启动get从中 当有一个 HTTP 请求 工作线程 时 我需要从redis lua stat
  • Redis键空间事件不触发

    我有两个 Redis 客户端 在一个文件中我有一个简单的脚本设置并删除了 Redis 键 var redis require redis var client redis createClient 6379 127 0 0 1 client
  • Redis hash写入速度非常慢

    我面临一个非常奇怪的问题 使用 Redis 时 我的写入速度非常糟糕 在理想的情况下 写入速度应该接近 RAM 上的写入速度 这是我的基准 package redisbenchmark import redis clients jedis
  • 如何将 ActionController::Live 与 Resque + Redis 一起使用(用于聊天应用程序)

    我正在尝试为我的 Rails 应用程序构建聊天功能 我在用ActionController Live Puma Resque Redis为了这 所以基本上在这种情况下 redissubscribe方法正在后台运行 使用resque 到目前为
  • 通过 StackExchange.Redis 连接到 Redis Servier

    我尝试使用以下方法制作一个测试项目Redis https redis io服务器 通过 Virtual Box 安装在 Linux Ubuntu 虚拟机上 Linux 机器通过 Virtual Box 的桥接适配器与本地网络连接 Virtu
  • 使用Redis从有限范围内生成唯一ID

    我有一些数据库项目 除了主键之外 还需要项目所属组的唯一索引 我们来调用属性nbr 以及将项目分组在一起并定义唯一范围的属性nbr 我们会打电话group This nbr必须在 1 N 范围内 并且may从外部源导入项目时进行设置 由于所
  • Java 将字节转换为二进制安全字符串

    我有一些以字节为单位的数据 我想将它们放入Redis中 但是Redis只接受二进制安全字符串 而我的数据有一些二进制非安全字节 那么如何将这些字节转换为二进制安全字符串以便将它们保存到 Redis 中呢 Base64 对我有用 但它使数据更
  • Redis、会话过期和反向查找

    我目前正在构建一个网络应用程序 并想使用 Redis 来存储会话 登录时 会话会使用相应的用户 ID 插入到 Redis 中 并且过期时间设置为 15 分钟 我现在想实现会话的反向查找 获取具有特定用户 ID 的会话 这里的问题是 由于我无
  • 有没有办法用Lettuce自动发现Redis集群中新的集群节点IP

    我有一个Redis集群 3主3从 运行在一个库伯内斯簇 该集群通过Kubernetes 服务 Kube 服务 我将我的应用程序服务器连接到 Redis 集群 使用Kube 服务作为 URI 通过 Redis 的 Lettuce java 客
  • Spring Data JPA Redis:无法编写基于自定义方法的查询

    我已经使用 Redis 配置了 Spring Data JPA 并使用RedisRepositorieswith 提供了类似的方法find findAll 所有这些方法似乎都工作得很好 但我无法编写我的自定义方法 RedisEntity f
  • 有没有办法让特定的key在集群模式下定位到特定的redis实例上?

    我想让我的多锁位于不同的redis实例上 我发现redission可以指定一个实例来执行命令 但是如果该命令与key相关 则指定的实例会将命令传输到另一个实例 你能给我一些建议吗 你可以 但这并不是微不足道的 首先 Redis 在键中使用大
  • StackExchange.Redis的正确使用方法

    这个想法是使用更少的连接和更好的性能 连接会随时过期吗 对于另一个问题 redis GetDatabase 打开新连接 private static ConnectionMultiplexer redis private static ID
  • Amazon Elasticache Redis 集群 - 无法获取端点

    我需要获取 Amazon Elasticache 中 Redis 集群的终端节点 以下代码适用于 Memcached 集群 但不适用于 Redis import com amazonaws auth AWSCredentials impor
  • Laravel 异常队列最大尝试次数超出

    我创建了一个应用程序来向多个用户发送电子邮件 但在处理大量收件人时遇到问题 该错误出现在failed jobs table Illuminate Queue MaxAttemptsExceededException App Jobs ESe
  • 如何延长 django-redis 中的缓存 ttl(生存时间)?

    我正在使用 django 1 5 4 和 django redis 3 7 1 我想延长缓存的 ttl 生存时间 当我取回它时 这是示例代码 from django core cache import cache foo cache get

随机推荐

  • main.c(16): warning: #223-D: function "led_init" declared implicitly

    编写了一个简单的stm32 的程序 比如led h和led c 在led c中定义了函数 void led init void 在main 函数中用的led init 除了要包含led h外 需要声明下函数才可以 extern void l
  • Linux开机自启动挂盘以及进入紧急模式的解决办法

    设置开机自启动挂盘经常会有挂载错误的操作 然后就进入了紧急模式 这俩孩子老是一起出现 就一起解决了吧 1 dev sr0已经挂载好了 现在需要把它重新挂载到另一个目录 media cdrom下 2 如果需要把它挂载到另一个目录 media
  • 【华为OD机试】找出两个整数数组中同时出现的整数(C++ Python Java)2023 B卷

    时间限制 C C 1秒 其他语言 2秒 空间限制 C C 262144K 其他语言524288K 64bit IO Format lld 题目描述 现有两个整数数组 需要你找出两个数组中同时出现的整数 并按照如下要求输出 有同时出现的整数时
  • 上传文件——FormData的格式

    let fd new FormData fd append file params file 上传文件时 上传fd即可 使用场景 一般是异步上传文件 需要formdata的形式 表单元素的集合 减少元素的拼接 提高效率
  • 第 19 课时:调度器的调度流程和算法介绍(木苏)

    本文将主要分享以下四个部分的内容 调度流程 调度算法 如何配置调度器 如何扩展调度器 调度流程 调度流程概览 首先来看一下调度器流程概览图 调度器启动时会通过配置文件 File 或者是命令行参数 或者是配置好的 ConfigMap 来指定调
  • 安卓7.0 在加载某些https资源的时会出现证书验证错误OS Error(handshake.cc:)解决方案

    今天收到反馈 Flutter库 CachedNetworkImage 3 2 3 在华为早期安卓版本7 0系统中加载某些https网站的图像时会出现类似这样的错误 I flutter The following HandshakeExcep
  • 生成静态页面的五种方案 收藏

    方案1 public static bool WriteFile string strText string strContent string strAuthor string path HttpContext Current Serve
  • 【知识图谱】知识图谱数据库将人类的思维路径转化为机器的路径思维

    前段时间被沙特阿拉伯授予公民身份的人形机器人 索菲亚 再一次颠覆了人们对人工智能技术的认知 索菲亚 多次与人类交锋并公开发表言论的过程中 我们感受到了基本的对答如流 有时甚至还可以做到妙语连珠 据了解 索菲亚的大脑存储在云端 通过连接WIF
  • 处理处理kdevtmpfsi挖矿病毒以及他的守护进程kinsing

    服务器CPU资源占用一直处于100 的状态 检查发现是kdevtmpfsi占用导致的 此进程为挖矿程序 处理步骤如下 kdevtmpfsi 进程处理 1 top 查看cpu占用情况 找到占用cpu的进程 最后是 kdevtmpfsi 2 n
  • 将文件间的编译依存关系降至最低——条款31

    假设你对C 程序的某个class实现文件做了些轻微修改 注意 修改的不是class接口 而是实现 而且只改private成分 然后重新建置这个程序 并预计只花数秒就好 毕竟只有一个class被修改 你按下 Build 按钮或键入make 或
  • 西门子200SMART(六)数据块

    数据块中的数据页可以插入 编辑 删除 查询 和之前讨论的程序块 符号快以及状态图标基本具备一样的功能 数据块最主要点作用就是对地址和数据赋值 如下图 这里需要注意点是 这里的赋值和之前我们说过的状态图表中的强制是有区别的 强制顾名思义不管你
  • 【Pandas学习】读、存excel数据

    目录 一 读数据 二 将df存为excel 1 pandas DataFrame to csv 函数语法 2 利用 import os 获取保存路径 3 产生新的数据 添加至上述csv文件中已有数据的后面 4 多sheet 指定存入的she
  • Leecode 每日一题 problem2 (03-23)

    给你两个 非空 的链表 表示两个非负的整数 它们每位数字都是按照 逆序 的方式存储的 并且每个节点只能存储 一位 数字 请你将两个数相加 并以相同形式返回一个表示和的链表 你可以假设除了数字 0 之外 这两个数都不会以 0 开头 来源 力扣
  • 2021-10-24

    Python 简介 Python 是一个高层次的结合了解释性 编译性 互动性和面向对象的脚本语言 Python 的设计具有很强的可读性 相比其他语言经常使用英文关键字 其他语言的一些标点符号 它具有比其他语言更有特色语法结构 Python
  • ERR_PNPM_NO_GLOBAL_BIN_DIR Unable to find the global bin directory

    错误提示 ERROR Unable to find the global bin directory Run pnpm setup to create it automatically or set the global bin dir s
  • (1)minikube玩转k8s集群之虚拟机支持嵌套虚拟化

    配套视频教程 1 Minikube介绍 简单说 创建k8s集群很麻烦 minikube可以让我们快速搭建一个k8s集群用于学习 Minikube 是一种可以让您在本地轻松运行 Kubernetes 的工具 Minikube 在笔记本电脑上的
  • openId和unionId的区别

    网友的解释 微信的用户隐私策略 每个接入微信的应用 公众号 APP 就像一个独立的商场 用户使用这些应用就像逛商场 商场用会员卡识别用户 类似的 我们根据商场名字为每个用户生成了一张专属会员卡 openid 每张会员卡只能在对应的商场才能够
  • Navicat for MySQL安装教程

    Navicat for MySQL是一款强大的 MySQL 数据库管理和开发工具 它为专业开发者提供了一套强大的足够尖端的工具 但对于新用户仍然易于学习 Navicat for MySQL 基于Windows平台 为 MySQL 量身订作
  • R语言logistic回归的细节解读

    本文首发于公众号 医学和生信笔记 完美观看体验请至公众号查看本文 医学和生信笔记 专注R语言在临床医学中的使用 R语言数据分析和可视化 文章目录 二项logistic回归 R语言中的 factor 函数可以把变量变为因子类型 默认是没有等级
  • Redis缓存击穿问题及解决思路

    一 什么是缓存击穿 缓存击穿问题也叫热点Key问题 就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了 无数的请求访问会在瞬间给数据库带来巨大的冲击 逻辑分析 假设线程1在查询缓存之后 本来应该去查询数据库 然后把这个数据重新加载