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

news/2025/3/21 5:58:57/

目录

引言

解决办法

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

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

设计逻辑过期时间


引言

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

解决办法

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

                

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

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

/*** 尝试获取锁** @param key* @return*/
private boolean tryLock(String key) {Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(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.判断是否为空值 这里过滤 " "的情况,不用担心会一直触发这个条件因为他有TTLif (shopJson != null) {//返回一个空值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) {//将空值写入RedisstringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);//为什么这里要存一个" "这是因为如果后续DB中有数据补充的话还可以去重建缓存//return Result.fail("暂无该商铺信息");return null;}//6.存在,写入RedisstringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shopById), CACHE_SHOP_TTL, TimeUnit.MINUTES);} catch (InterruptedException e) {throw new RuntimeException(e);} finally {//7.释放互斥锁unlock(lockKey);}return shopById;}

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

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

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

/*** 添加逻辑过期时间** @param id* @param expireTime*/
public void saveShopRedis(Long id, Long expireTime) {//查询店铺信息Shop shop = getById(id);//封装逻辑过期时间RedisData redisData = new RedisData();redisData.setData(shop);redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireTime));//将封装过期时间和商铺数据的对象写入RedisstringRedisTemplate.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.config.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();}
}

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

但是

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

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

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


http://www.ppmy.cn/news/1580790.html

相关文章

网络编程之客户端通过服务器与另外一个客户端交流

服务器使用select模型搭建&#xff0c;客户端1使用线程搭建&#xff0c;客户端2使用poll模型搭建&#xff0c; 使用时需要先运行服务器&#xff0c;具体编译可看我最后的图片 head.h头文件 #ifndef __HEAD_H_ #define __HEAD_H_ #include <stdio.h> #include <string…

基于 Python 爬取 TikTok 搜索数据 Tiktok爬虫(2025.3.17)

1. 前言 在数据分析和网络爬虫的应用场景中&#xff0c;我们经常需要获取社交媒体平台的数据&#xff0c;例如 TikTok。本篇文章介绍如何使用 Python 爬取 TikTok 用户搜索数据&#xff0c;并解析其返回的数据。 结果截图 2. 项目环境准备 在正式运行代码之前&#xff0c;我…

K8s集群的环境部署

1.测试环境所需要的主机名和IP和扮演的角色 harbor 172.25.254.200 harbor仓库 k8s-master 172.25.254.100 k8s集群控制节点 k8s-node1 172.25.254.10 k8s集群工作节点 k8s-node2 172.25.254.20 k8集群工作节点 注意&#xff1a;所有节点禁用selinux和防火墙 所有节点同步…

【Linux我做主】基础命令完全指南上篇

Linux基础命令完全指南【上篇】 Linux基础命令完全指南github地址前言命令行操作的引入Linux文件系统树形结构的根文件系统绝对路径和相对路径适用场景Linux目录下的隐藏文件 基本指令目录和文件相关1. ls2. cd和pwdcdpwd 3. touch4. mkdir5. cp6. mv移动目录时覆盖写入的两种特…

React第三十章(css原子化)

原子化 css 什么是原子化 css 原子化 CSS 是一种现代 CSS 开发方法&#xff0c;它将 CSS 样式拆分成最小的、单一功能的类。比如一个类只负责设置颜色&#xff0c;另一个类只负责设置边距。这种方式让样式更容易维护和复用&#xff0c;能提高开发效率&#xff0c;减少代码冗余…

Python----计算机视觉处理(Opencv:形态学变换)

一、形态学变化 形态学变换&#xff08;Morphological Transformations&#xff09;是一种基于形状的图像处理技术&#xff0c;主要处理的对象为二值化图像。 形态学变换有两个输入和一个输出&#xff1a;输入为原始图像和核&#xff08;即结构化元素&#xff09;&#xff0c;输…

【Leetcode刷题随笔】206.反转链表

1.题目简介 翻转一个单链表&#xff0c;示例: 输入: 1->2->3->4->5->NULL 输出: 5->4->3->2->1->NULL。 原题链接&#xff1a;206.反转链表. 2.解法思路 要反转一个链表&#xff0c;可以定义一个新的链表来实现反转&#xff0c;但是内存空间消…

区块链加密技术公司DApp开发指南:从零开始到上线

随着区块链技术的普及&#xff0c;去中心化应用&#xff08;DApp&#xff09;成为加密技术公司探索的核心领域。本文结合行业实践与最新技术趋势&#xff0c;系统梳理DApp从需求分析到上线的完整开发流程&#xff0c;并融入关键工具、安全策略与案例解析&#xff0c;助力企业高…