redis 使用互斥锁或逻辑过期两种方案解决缓存击穿,和缓存穿透(用缓存空值 或布隆过滤器)的解决方案

缓存穿透
        缓存穿透是指在缓存中查找一个不存在的值,由于缓存一般不会存储这种无效的数据,所以每次查询都会落到数据库上,导致数据库压力增大,严重时可能会导致数据库宕机。
解决方案:
缓存空值 (本文此方案)
2 布隆过滤器
3 增强id的复杂度
4 做好数据的基础格式校验
5 做好热点参数的限流


缓存击穿
        缓存击穿是指一个被频繁访问(高并发访问并且缓存重建业务较复杂)的缓存键因为过期失效,同时又有大量并发请求访问此键,导致请求直接落到数据库或后端服务上,增加了系统的负载并可能导致系统崩溃 
解决方案
互斥锁
逻辑过期


1 前提先好做redis与springboot的集成,redisson的集成【用于加锁解锁】【本文用的redisson】
   另外用到了hutool的依赖


2 缓存对象封装的类,这里只是逻辑过期方案可以用上,你也可以自己改

/*** 决缓存击穿--(设置逻辑过期时间)*/
@Data
public class RedisData {//逻辑过期时间private LocalDateTime expireTime;//缓存实际的内容private Object data;
}

3 相关的常量

public class Constant {//缓存空值的ttl时间public static final Long CACHE_NULL_TTL = 2L;//缓存时间,单位程序里参数传public static final Long CACHE_NEWS_TTL = 10L;//缓存前缀,根据模块来public static final String CACHE_NEWS_KEY = "cache:news:";//锁-前缀,根据模块来public static final String LOCK_NEWS_KEY = "lock:news:";//持有锁的时间public static final Long LOCK_TTL = 10L;
}

4 缓存核心类

import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;import static org.example.service_a.cache.Constant.CACHE_NULL_TTL;
import static org.example.service_a.cache.Constant.LOCK_NEWS_KEY;@Slf4j
@Component
//封装的将Java对象存进redis 的工具类
public class CacheClient {@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Autowiredprivate RedissonClient redissonClient;// 定义线程池private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 2);AtomicInteger atomicInteger = new AtomicInteger();/*** 设置TTL过期时间set** @param key* @param value* @param time* @param unit*/public void set(String key, Object value, Long time, TimeUnit unit) {// 需要把value序列化为string类型String jsonStr = JSONUtil.toJsonStr(value);stringRedisTemplate.opsForValue().set(key, jsonStr, time, unit);}/*** 缓存穿透功能封装** @param id* @return*/public <R, ID> R queryWithPassThrough(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {String key = keyPrefix + id;//1. 从Redis中查询缓存String Json = stringRedisTemplate.opsForValue().get(key);//2. 判断是否存在if (StrUtil.isNotBlank(Json)) {//3. 存在,直接返回return JSONUtil.toBean(Json, type);}// 这里要先判断命中的是否是null,因为是null的话也是被上面逻辑判断为不存在// 这里要做缓存穿透处理,所以要对null多做一次判断,如果命中的是null则shopJson为""if ("".equals(Json)) {return null;}//4. 不存在,根据id查询数据库R r = dbFallback.apply(id);log.error("查询数据库次数 {}",atomicInteger.incrementAndGet());if (r == null) {//5. 不存在,将null写入redis,以便下次继续查询缓存时,如果还是查询空值可以直接返回false信息stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);return null;}//6. 存在,写入Redisthis.set(key, r, time, unit);//7. 返回return r;}/*** 解决缓存击穿--(互斥锁)* @param keyPrefix* @param id* @param type* @param dbFallback* @param time* @param unit* @return* @param <R>* @param <ID>*/public <R, ID> R queryWithMutex(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit){String key = keyPrefix + id;// 1.从redis查询商铺缓存String shopJson = stringRedisTemplate.opsForValue().get(key);// 2.判断是否存在if (StrUtil.isNotBlank(shopJson)) {// 3.存在,直接返回return JSONUtil.toBean(shopJson, type);}// 判断命中的是否是空值if (shopJson != null) {// 返回一个错误信息return null;}log.error("缓存重建----");// 4.实现缓存重建// 4.1.获取互斥锁String lockKey = LOCK_NEWS_KEY + id;R r = null;try {boolean isLock = tryLock(lockKey);// 4.2.判断是否获取成功if (!isLock) {// 4.3.获取锁失败,休眠并重试Thread.sleep(10);return queryWithMutex(keyPrefix, id, type, dbFallback, time, unit);}// 4.4.获取锁成功,根据id查询数据库r = dbFallback.apply(id);log.info("查询数据库次数 {}",atomicInteger.incrementAndGet());// 5.不存在,返回错误if (r == null) {// 将空值写入redisstringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);// 返回错误信息return null;}// 6.存在,写入redisthis.set(key, r, time, unit);} catch (InterruptedException e) {throw new RuntimeException(e);}finally {// 7.释放锁unLock(lockKey);}// 8.返回return r;}/*** --------------注意 key 没有加过期时间,会一直存在,只是 缓存的内容里有个字段,标识了过期的时间----------------* 设置逻辑过期set** @param key* @param value* @param time* @param chronoUnit*/public void setWithLogicExpire(String key, Object value, Long time, ChronoUnit chronoUnit) {// 设置逻辑过期RedisData redisData = new RedisData();redisData.setData(value);redisData.setExpireTime(LocalDateTime.now().plus(time, chronoUnit));// 需要把value序列化为string类型String jsonStr = JSONUtil.toJsonStr(redisData);stringRedisTemplate.opsForValue().set(key, jsonStr);}/*** 解决缓存击穿--(设置逻辑过期时间)方式* 1. 组合键名,从Redis查询缓存。* 2. 缓存不存在,直接返回(预设热点数据已预热)。* 3. 解析缓存内容,获取过期时间。* 4. 若未过期,直接返回数据。* 5. 已过期,执行缓存重建流程:* a. 尝试获取互斥锁。* b. 二次检查缓存是否已重建且未过期,若是则返回数据。* c. 成功获取锁,异步执行:* i. 查询数据库获取最新数据。* ii. 重新写入Redis缓存,附带新的逻辑过期时间。* iii. 最终释放锁。* 6. 未能获取锁,直接返回旧数据。** @param id* @return*/public <R, ID> R queryWithLogicalExpire(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, ChronoUnit chronoUnit) throws InterruptedException {String key = keyPrefix + id;//1. 从Redis中查询缓存String Json = stringRedisTemplate.opsForValue().get(key);//2. 判断是否存在if (StrUtil.isBlank(Json)) {//3. 不存在,直接返回(这里做的是热点key,先要预热,所以已经假定热点key已经在缓存中)return null;}//4. 存在,需要判断过期时间,需要先把json反序列化为对象RedisData redisData = JSONUtil.toBean(Json, RedisData.class);R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);LocalDateTime expireTime = redisData.getExpireTime();//5. 判断是否过期if (expireTime.isAfter(LocalDateTime.now())) {//5.1 未过期,直接返回店铺信息return r;}log.error("缓存内容已逻辑过期-----------{}",LocalDateTime.now());//5.2 已过期,需要缓存重建//6. 缓存重建//6.1 获取互斥锁String lockKey = LOCK_NEWS_KEY + id;//6.2 判断是否获取锁成功boolean isLock = tryLock(lockKey);if (isLock) {// 二次验证是否过期,防止多线程下出现缓存重建多次String Json2 = stringRedisTemplate.opsForValue().get(key);// 这里假定key存在,所以不做存在校验// 存在,需要判断过期时间,需要先把json反序列化为对象RedisData redisData2 = JSONUtil.toBean(Json2, RedisData.class);R r2 = JSONUtil.toBean((JSONObject) redisData2.getData(), type);LocalDateTime expireTime2 = redisData2.getExpireTime();if (expireTime2.isAfter(LocalDateTime.now())) {// 未过期,直接返回店铺信息return r2;}//6.3 成功,开启独立线程,实现缓存重建CACHE_REBUILD_EXECUTOR.submit(() -> {try {// 重建缓存,这里设置的值小一点,方便观察程序执行效果,实际开发应该设为30min// 查询数据库R apply = dbFallback.apply(id);log.info("查询数据库次数 {}",atomicInteger.incrementAndGet());// 写入redisthis.setWithLogicExpire(key, apply, time, chronoUnit);} catch (Exception e) {throw new RuntimeException(e);} finally {// 释放锁unLock(lockKey);}});}//7. 返回,如果没有获得互斥锁,会直接返回旧数据return r;}/*** 加锁* @param lockKey* @return*/private boolean tryLock(String lockKey) {RLock lock = redissonClient.getLock(lockKey);try {// 尝试获取锁,最多等待10秒,获取到锁后自动 LOCK_SHOP_TTL 0秒后解锁return lock.tryLock(10, Constant.LOCK_TTL, TimeUnit.SECONDS);} catch (Exception e) {Thread.currentThread().interrupt();// 重新抛出中断异常log.error("获取锁时发生中断异常", e);return false;}}/*** 解锁* @param lockKey*/private void unLock(String lockKey) {RLock lock = redissonClient.getLock(lockKey);lock.unlock(); // 解锁操作}}

5 缓存预热和测试

import cn.hutool.json.JSONUtil;
import org.example.common.AppResult;
import org.example.common.AppResultBuilder;
import org.example.service_a.cache.CacheClient;
import org.example.service_a.cache.RedisData;
import org.example.service_a.domain.News;
import org.example.service_a.service.NewsService;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.SessionCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import javax.annotation.PostConstruct;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;import static org.example.service_a.cache.Constant.CACHE_NEWS_KEY;
import static org.example.service_a.cache.Constant.CACHE_NEWS_TTL;@RestController
@Validated()
@RequestMapping("/article")
public class News_Controller {@Autowiredprivate StringRedisTemplate redisTemplate;@Autowiredprivate CacheClient cacheClient;@Autowiredprivate NewsService newsService;@Autowiredprivate RedissonClient redissonClient;/*** @param id 编号*/@RequestMapping("/get/{id}")public AppResult<News> getGirl(@PathVariable("id") Long id) throws InterruptedException {//解决缓存穿透-------->News news = cacheClient.queryWithPassThrough(CACHE_NEWS_KEY, id, News.class,newsService::getById,CACHE_NEWS_TTL, TimeUnit.MINUTES);//(互斥锁)解决缓存击穿---------->
//        News news = cacheClient.queryWithMutex(CACHE_NEWS_KEY, id, News.class,
//                (x) -> {
//                    return newsService.getById(id);
//                }
//                , CACHE_NEWS_TTL, TimeUnit.MINUTES);//(设置逻辑过期时间)解决缓存击穿---------->
//        News news = cacheClient.queryWithLogicalExpire(
//                CACHE_NEWS_KEY,
//                id,
//                News.class,
//                (x)->{
//                    return newsService.getById(id);
//                },
//                CACHE_NEWS_TTL,
//                ChronoUnit.SECONDS);System.out.println("news = " + news);//判断返回值是否为空
//        if (news == null) {
//            return Result.fail("信息不存在");
//        }
//        //返回
//        return Result.ok(news);return AppResultBuilder.success(news);}/***缓存预热*/@PostConstruct()public void cache_init() {RLock lock = redissonClient.getLock("lock:cacheInit");lock.lock();try {List<News> list = newsService.list();redisTemplate.executePipelined(new SessionCallback<Object>() {HashMap<String, Object> objectObjectHashMap = new HashMap<>();@Overridepublic <K, V> Object execute(RedisOperations<K, V> operations) throws DataAccessException {list.forEach(news -> {//演示缓存击穿--逻辑过期 用这种方式
//                        RedisData redisData = new RedisData();
//                        redisData.setData(news);
//                        redisData.setExpireTime(LocalDateTime.now().plusSeconds(30));
//                        objectObjectHashMap.put(CACHE_NEWS_KEY +news.getId(),JSONUtil.toJsonStr(redisData));//演示缓存击穿--互斥锁 用这种方式objectObjectHashMap.put(CACHE_NEWS_KEY + news.getId(), JSONUtil.toJsonStr(news));});operations.opsForValue().multiSet((Map<? extends K, ? extends V>) objectObjectHashMap);return null;}});} catch (Exception e) {throw new RuntimeException(e);} finally {lock.unlock();}}
}

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

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

相关文章

【c++】----STL简介string

目录 1. 什么是STL 2. STL的版本 3. STL的六大组件 4.STL的缺陷 5.string类 1. 为什么学习string类&#xff1f; 6.string类的常用接口说明&#xff08;下面我们只讲解最常用的接口&#xff09; 1.string 常见构造 2.string类的遍历 iterator 迭代器遍历 &#xff08;…

初识BootStrap

目录 前言: 1.Bootstrap的特点包括&#xff1a; 1.1响应式设计&#xff1a; 1.2组件丰富&#xff1a; 1.3易于定制&#xff1a; 1.4兼容性良好&#xff1a; 1.5强大的社区支持&#xff1a; 1.6一致的样式和布局&#xff1a; 1.7 插件和扩展性 2.初识Ajax: 2.1同步请求…

CANopen学习笔记

1.CANopen的预定义报文ID分类 CANopen在设计时&#xff0c;对其定义为小网络、控制信号的实时通讯&#xff1a; 报文传输采用CAN标准帧格式。即11bit的ID域&#xff0c;以尽量减小传输时间。网络控制报均采用数据最小字节数。比如心跳报文&#xff0c;只有1个字节数据。实时更…

STM32应用开发教程进阶--Wi-Fi通信(ESP8266模块:STA、AP、STA+AP)

实现目标 1、熟悉Wi-F、ESP8266模块 2、掌握ESP8266模块共3种工作模式&#xff1a;STA、AP、STAAP的配置 3、具体实现目标&#xff1a;&#xff08;1&#xff09;AT固件烧录&#xff1b;&#xff08;2&#xff09;ESP8266模块STA、AP、STAAP的配置 一、Wi-Fi概述 1、Wi-Fi定…

【JavaScript】使用 AbortController 对象中断 fetch 的文本流传输

AbortController 正如我们所知道的&#xff0c;fetch 返回一个 promise。JavaScript 通常并没有“中止” promise 的概念。那么我们怎样才能取消一个正在执行的 fetch 呢&#xff1f;例如&#xff0c;如果用户在我们网站上的操作表明不再需要某个执行中的 fetch。 为此有一个…

【kettle006】kettle访问华为openGauss高斯数据库并处理数据至execl文件

1.一直以来想写下基于kettle的系列文章&#xff0c;作为较火的数据ETL工具&#xff0c;也是日常项目开发中常用的一款工具&#xff0c;最近刚好挤时间梳理、总结下这块儿的知识体系。 2.熟悉、梳理、总结下华为openGauss高斯数据库相关知识体系 3.欢迎批评指正&#xff0c;跪谢…

Spring Boot应用部署 - JAR包Docker部署

JAR包Docker部署 要使用Docker部署Spring Boot应用&#xff0c;需要创建一个Dockerfile来定义如何构建Docker镜像&#xff0c;并且可能还需要在Maven构建脚本中集成Docker插件以简化构建过程。以下是详细的步骤&#xff1a; 1. 创建Dockerfile 在Spring Boot项目的根目录下创…

【实时数仓架构】方法论(未完)

笔者不是专业的实时数仓架构&#xff0c;这是笔者从其他人经验和网上资料整理而来&#xff0c;仅供参考。写此文章意义&#xff0c;加深对实时数仓理解。 实时数仓背景和场景 一、实时数仓架构技术演进 1.1、四种架构演进 1&#xff09;离线大数据架构 一种批处理离线数据分…

胡写乱写哈哈哈

message.from() 获取消息的发送者名称 const bot new Wechaty() .on(message, async message > { //这条消息是谁发送的const contact message.from()//这条消息的文本内容const text message.text()//这条消息是哪个群聊中发送的const room message.room()if (room) {/…

电脑开机后卡在开机LOGO画面如何排查处理

当电脑开机后长时间停滞在开机LOGO画面,无法继续进入操作系统,这一现象常令用户困扰不已。本文将深入探讨导致此类问题的多种可能原因,并提供相应的解决方法,帮助你有效地诊断和排除故障。 硬件故障或接触不良 1. 硬盘问题:硬盘是系统启动的关键组件,其故障或数据线接触…

css如何去掉重叠部分的边框,CSS中nth-child不生效

css如何去掉重叠部分的边框 div使用负的margin&#xff0c;margin-right:-1px&#xff1b;table表格设置边框后的重叠&#xff0c;border-collapse: collapse CSS中nth-child不生效 <body><ul><li><a><span class"item"></span&…

CentOS 删除文件提示 Operation not permitted 的解决方法

1、阿里云服务器提示存在挖矿行为&#xff0c;路径在 /etc/zzh&#xff0c;我们做下删除动作&#xff0c;发现不能删除 [rootMSH etc]# rm -f zzh# 提示 rm: cannot remove ‘zzh’: Operation not permitted2、解决方法&#xff1a; (1)、查看文件权限 [rootMSH etc]# lsat…

xml,json和protobuffer

数据组织格式 xmljsonprotobuffer小结 xml 是以成对的方式,来表示"键值对"的信息,同时标签支持嵌套,可以构成更复杂的树形结构数据. 请求: <request> // 开始标签<username>zhangsan</username> // 表示的是键值对 key:username value: zhangsan&l…

Django项目之电商购物商城 -- 校验用户输入密码是否合法

Django项目之电商购物商城 – 校验用户输入密码是否合法 需要开发文档和前端资料的可私聊 一. 创建用户逻辑操作 1. 创建用户app – users python manage.py startapp users2.注册app users.apps.UsersConfig,3. 创建视图 from django.shortcuts import render from djan…

如何在小程序中添加图片和视频

在微信小程序中添加图片和视频可以通过特定的组件和属性来实现。 对于添加图片&#xff0c;你可以使用<image>组件。 <view> <image src"图片链接" style"width: 375rpx; height: 375rpx;"></image> </view> 这里&…

原生IP和住宅IP有什么区别?

原生IP和住宅IP在多个方面存在显著的区别。 从定义和来源来看&#xff0c;原生IP是指未经NAT&#xff08;网络地址转换&#xff09;处理的真实、公网可路由的IP地址&#xff0c;它直接从互联网服务提供商&#xff08;ISP&#xff09;获得&#xff0c;而不是通过代理服务器或VP…

Django初步了解

目录 一、什么是Django 二、Django的设计模式 三、涉及的英文缩写及其含义 四、安装&#xff08;官方教程&#xff09; 一、什么是Django Django是一个Python Web框架&#xff0c;可以快速开发网站&#xff0c;提供一站式的解决方案&#xff0c;包括缓存、数据库ORM、后台…

Postgresql从小白到高手 十:Linux服务器配置详解

Postgresql从小白到高手 第十章 Linux服务器配置详解 文章目录 Postgresql从小白到高手Postgresql Linux 服务器配置conf常用配置conf配置类别1. 连接相关配置2. 资源管理相关配置3. 日志和错误报告相关配置4. 安全性相关配置5 内存和查询优化6、复制和备份配置注意事项 Postg…

大象机器人开源协作机械臂myCobot 630 全面升级!

1. 开篇概述 在快速发展的机器人技术领域中&#xff0c;Elephant Robotics的myCobot 600已经证明了其在教育、科研和轻工业领域的显著适用性。作为一款具备六自由度的机械臂&#xff0c;myCobot 600以其600mm的工作半径和2kg的末端负载能力&#xff0c;满足了多样化的操作需求。…

中间件解析漏洞

1 、 apache 解析漏洞 漏洞环境搭建 下载 vulhub git clone https://github.com/vulhub/vulhub.git 进入对应漏洞目录、 cd vulhub/httpd/apache_parsing_vulnerability apt-get docker-compose 启动漏洞环境 docker-compose up -d 注&#xff1a;启动容器时&#xf…