Redis解决缓存击穿问题的两种方法

2025-03-21 13:50

本文主要是介绍Redis解决缓存击穿问题的两种方法,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

《Redis解决缓存击穿问题的两种方法》缓存击穿问题也叫热点Key问题,就是⼀个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击,本文给大家介绍了Re...

引言

缓存击穿:给某一个key设置了过期时间,当key过期的时候,恰好这个时间点对这个key有大量的并发请求过来,这些并发的请求可能会瞬间把DB压垮

解决办法

互斥锁(强一致,性能差)

Redis解决缓存击穿问题的两种方法

根据图片就可以看出,我们的思路就是只能让一个线程能够进行访问Redis,要想实现这个功能,我们也可以使用Redis自带的setnx

封装两个方法,一个写key来尝试获取锁另一个删key来释放锁

/**
 * 尝试获取锁
 *
 * @param key
 * @return
 */
private boolean tryLock(String key) {
    Boolean flag = stringRedisTemplate.opsForValue().sewww.chinasem.cntIfAbsent(key, "1", 10, TimeUnit.SECONDS);
    return BooleanUtil.isTrue(flag);
}
 
/**
 * 释放锁
 *
 * @param key
 */
private void unlock(String key) {
    stringRedisTemplate.delete(key);
}

在并行情况下每当其他线程想要获取锁,来访问缓存都要通过将自己的key写到tryLock()方法里,setIfAbsent()返回false则说明有线程在在更新缓存数据,锁未释放。若返回true则说明当前线程拿到锁了可以访问缓存甚至操作缓存。

我们在下面一个热门的查询场景中用代码用代码来实现互斥锁解决缓存击穿,代码如下:

    /**
     * 解决缓存击穿的互斥锁
     * @param id
     * @return
     */
    public Shop queryWithMutex(Long id) {
        String key = CACHE_SHOP_KEY + id;
        //1.从Redis查询缓存
        String shopjson = stringRedisTemplate.opsForValue().get(key);  //JSON格式
        //2.判断是否存在
        if (StrUtil.isNotBlank(shopJson)) { //不为空就返回 此工具类API会判断" "为false
            //存在则直接返回
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            //return Result.ok(shop);
            return shop;
        }
        //3.判断是否为空值 这里过滤 " "的情况,不用担心会一直触发这个条件因为他有TTL
        if (shopJson != null) {
            //返回一个空值
  android          return null;
        }
        //4.缓存重建 Redis中值为null的情况
        //4.1获得互斥锁
        String lockKey = "lock:shop"+id;
        Shop shopById=null;
        try {
            boolean isLock = tryLock(lockKey);
            //4.2判断是否获取成功
            if (!isLock){
                //4.3失败,则休眠并重试
                Thread.sleep(50);
               return queryWithMutex(id);
            }
            //4.4成功,根据id查询数据库
            shopById = getById(id);
            //5.不存在则返回错误
            if (shopById == null) {
                //将空值写入Redis
                stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
                //为什么这里要存一个" "这是因为如果后续DB中有数据补充的话还可以去重建缓存
                //return Result.fail("暂无该商铺信息");
                return null;
            }
            //6.存在,写入Redis
            stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shopById), CACHE_SHOP_TTL, TimeUnit.MINUTES);
 http://www.chinasem.cn       } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            //7.释放互斥锁
            unlock(lockKey);
        }
 
        return shopById;
    }

逻辑过期(高可用,性能优)

方案:用户查询某个热门产品信息,如果缓存未命中(即信息为空),则直接返回空,不去查询数据库。如果缓存信息命中,则判断是否逻辑过期,未过期返回缓存信息,过期则重建缓存,尝试获得互斥锁,获取失败则直接返回已过期缓存数据,获取成功则开启独立线程去重构缓存然后直接返回旧的缓存信息,重构完成之后就释放互斥锁。

Redis解决缓存击穿问题的两种方法

封装一个方法用来模拟更新逻辑过期时间与缓存的数据在测试类里运行起来达到数据与热的效果 

/**
 * 添加逻辑过期时间
 *
 * @param id
 * @param expireTime
 */
public void saveShopRedis(Long id, Long expireTime) {
    //查询店铺信息
    Shop shop = getById(id);
    //封装逻辑过期时间
    RedisData redisData = new RedisData();
    redisData.setData(shop);
    redisData.sethttp://www.chinasem.cnExpireTime(LocalDateTime.now().plusSeconds(expireTime));
    //将封装过期时间和商铺数据的对象写入Redis
    stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
}

查询接口:

/**
 * 逻辑过期解决缓存击穿
 *
 * @param id
 * @return
 */
public Shop queryWithLogicalExpire(Long id) throws InterruptedException {
    String key = CACHE_SHOP_KEY + id;
    Thread.sleep(200);
    //1.从Redis查询缓存
    String shopJson = stringRedisTemplate.opsForValue().get(key);  //JSON格式
    //2.判断是否存在
    if (StrUtil.isBlank(shopJson)) {
        //不存在则直接返回
        return null;
    }
    //3.判断是否为空值
    if (shopJson != null) {
        //返回一个空值
        //return Result.fail("店铺不存在!");
        return null;
    }
    //4.命中
    //4.1将JSON反序列化为对象
    RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
    Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
    LocalDateTime expireTime = redisData.getExpireTime();
    //4.2判断是否过期
    if (expireTime.isAfter(LocalDateTime.now())) {
        //5.未过期则返回店铺信息
        return shop;
    }
    //6.过期则缓存重建
    //6.1获取互斥锁
    String LockKey = LOCK_SHOP_KEY + id;
    boolean isLock = tryLock(LockKey);
    //6.2判断是否成功获得锁
    if (isLock) {
        //6.3成功,开启独立线程,实现缓存重建
        CACHE_REBUILD_EXECUTOR.submit(() -> {
            try {
                //重建缓存
                this.saveShop2Redis(id, 20L);
 
            } catch (Exception e) {
                throw new RuntimeException(e);
            } finally {
                //释放锁
                unlock(LockKey);
            }
        });
    }
    //6.4返回商铺信息
    return shop;
}

设计逻辑过期时间

可以用这个方法设置逻辑过期时间

import org.redisson.Redisson;
import org.redisson.api.RBucket;
import org.redisson.api.RedissonClient;
import org.redisson.copythonnfig.Config;
 
public class RedissonExample {
    public static void main(String[] args) {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");
 
        RedissonClient redisson = Redisson.create(config);
 
        String key = "exampleKey";
        String value = "exampleValue";
        int timeout = 10; // 过期时间(秒)
 
        // 获取RBucket对象
        RBucket<String> bucket = redisson.getBucket(key);
 
        // 设置值并指定过期时间
        bucket.set(value, timeout, TimeUnit.SECONDS);
 
        System.out.println("设置成功");
        redisson.shutdown();
    }
}

大家可以看到,逻辑过期锁就是可以实现并发,所以他的效率更快,性能更好

但是

  • 牺牲了数据的实时性,以保证高并发场景下的服务可用性和数据库的稳定性。

  • 在实际应用中,需要确保获取互斥锁的操作是原子的,并且锁具有合适的超时时间,以避免死锁的发生。

  • 逻辑过期策略适用于那些对数据实时性要求不高,但要求服务高可用性的场景。

到此这篇关于Redis解决缓存击穿问题的两种方法的文章就介绍到这了,更多相关Redis解决缓存击穿内容请搜索China编程(www.chinasem.cn)以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程China编程(www.chinasem.cn)!

这篇关于Redis解决缓存击穿问题的两种方法的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1153872

相关文章

qt5cored.dll报错怎么解决? 电脑qt5cored.dll文件丢失修复技巧

《qt5cored.dll报错怎么解决?电脑qt5cored.dll文件丢失修复技巧》在进行软件安装或运行程序时,有时会遇到由于找不到qt5core.dll,无法继续执行代码,这个问题可能是由于该文... 遇到qt5cored.dll文件错误时,可能会导致基于 Qt 开发的应用程序无法正常运行或启动。这种错

Python常用命令提示符使用方法详解

《Python常用命令提示符使用方法详解》在学习python的过程中,我们需要用到命令提示符(CMD)进行环境的配置,:本文主要介绍Python常用命令提示符使用方法的相关资料,文中通过代码介绍的... 目录一、python环境基础命令【Windows】1、检查Python是否安装2、 查看Python的安

Redis Cluster模式配置

《RedisCluster模式配置》:本文主要介绍RedisCluster模式配置,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧... 目录分片 一、分片的本质与核心价值二、分片实现方案对比 ‌三、分片算法详解1. ‌范围分片(顺序分片)‌2. ‌哈希分片3. ‌虚

Maven 配置中的 <mirror>绕过 HTTP 阻断机制的方法

《Maven配置中的<mirror>绕过HTTP阻断机制的方法》:本文主要介绍Maven配置中的<mirror>绕过HTTP阻断机制的方法,本文给大家分享问题原因及解决方案,感兴趣的朋友一... 目录一、问题场景:升级 Maven 后构建失败二、解决方案:通过 <mirror> 配置覆盖默认行为1. 配置示

SpringBoot排查和解决JSON解析错误(400 Bad Request)的方法

《SpringBoot排查和解决JSON解析错误(400BadRequest)的方法》在开发SpringBootRESTfulAPI时,客户端与服务端的数据交互通常使用JSON格式,然而,JSON... 目录问题背景1. 问题描述2. 错误分析解决方案1. 手动重新输入jsON2. 使用工具清理JSON3.

使用jenv工具管理多个JDK版本的方法步骤

《使用jenv工具管理多个JDK版本的方法步骤》jenv是一个开源的Java环境管理工具,旨在帮助开发者在同一台机器上轻松管理和切换多个Java版本,:本文主要介绍使用jenv工具管理多个JD... 目录一、jenv到底是干啥的?二、jenv的核心功能(一)管理多个Java版本(二)支持插件扩展(三)环境隔

SpringBoot服务获取Pod当前IP的两种方案

《SpringBoot服务获取Pod当前IP的两种方案》在Kubernetes集群中,SpringBoot服务获取Pod当前IP的方案主要有两种,通过环境变量注入或通过Java代码动态获取网络接口IP... 目录方案一:通过 Kubernetes Downward API 注入环境变量原理步骤方案二:通过

Springboot整合Redis主从实践

《Springboot整合Redis主从实践》:本文主要介绍Springboot整合Redis主从的实例,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录前言原配置现配置测试LettuceConnectionFactory.setShareNativeConnect

Java中Map.Entry()含义及方法使用代码

《Java中Map.Entry()含义及方法使用代码》:本文主要介绍Java中Map.Entry()含义及方法使用的相关资料,Map.Entry是Java中Map的静态内部接口,用于表示键值对,其... 目录前言 Map.Entry作用核心方法常见使用场景1. 遍历 Map 的所有键值对2. 直接修改 Ma

Mybatis Plus Join使用方法示例详解

《MybatisPlusJoin使用方法示例详解》:本文主要介绍MybatisPlusJoin使用方法示例详解,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,... 目录1、pom文件2、yaml配置文件3、分页插件4、示例代码:5、测试代码6、和PageHelper结合6