【Redis笔记】缓存穿透、缓存击穿的Java代码大致解决方案

2024-02-01 04:04

本文主要是介绍【Redis笔记】缓存穿透、缓存击穿的Java代码大致解决方案,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

关于缓存穿透、缓存击穿是什么,可以去我的博客专栏 Redis 下查看 【Redis笔记】缓存——缓存分类、更新策略、缓存穿透、缓存雪崩、缓存击穿

为了能够使用Java操作Redis,我们首先需要先给SpringBoot加入Redis的依赖坐标:

		<!--redis依赖及其连接池--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><dependency><groupId>org.apache.commons</groupId><artifactId>commons-pool2</artifactId></dependency>

application.yml 文件中配置数据源信息(也可以是application.properties),此处我使用的是application.yml,其他配置文件格式不太一样。

spring:datasource:driver-class-name: com.mysql.jdbc.Driverurl: jdbc:mysql://127.0.0.1:3306/数据库名称?useSSL=false&serverTimezone=UTC # 数据库urlusername: rootpassword: 1234redis:host: 192.168.1.1 # redis服务器地址port: 6379 # redis默认端口password: 123456 # redis服务器密码lettuce:pool: # redis连接池信息max-active: 10 max-idle: 10min-idle: 1time-between-eviction-runs: 10s

缓存穿透

关于缓存穿透的解决方案,之前提到过 缓存空对象布隆过滤两种。那么对于缓存空对象,业务逻辑上如何实现查询和更新呢?

缓存空对象部分代码

在一次查询操作过程中,先去请求Redis,如果查到了数据,也不用管是否为空值(“”),直接返回该数据即可;如果未在Redis中查询到数据,那么就去数据库进行查询,。如果数据库中也未能查到想要查询到的目标,则在Redis中设置该键值为空字符串(“”)。
可能有人有疑问,如果后面数据库中有这个数据了怎么办?那么,由于我们的Redis设置了过期时间,所以当一段时间之后,Reids中的值失效了,那么再次查询时就会去数据库中更新。
那这样就会存在一段时间的间隙,数据库与Redis不一致的情况,不过这也是没办法的问题。为了避免有人恶意或者无意使用不存在信息对数据库查询,从而瘫痪数据库系统,设置空值,能够有效的缓解不断查询数据库不存在的信息对数据库造成的压力。

	// Redis查询数据public void set(String key, Object value, Long time, TimeUnit unit) {// 业务调用时传入键值,过期时间stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);}// 解决缓存穿透public <R, ID> R queryWithPassThrough(String keyPrefix, ID id, Class<R> type,Function<ID, R> dbFallback, Long time, TimeUnit unit) {String key = keyPrefix + id;// 从redis查询缓存String json = stringRedisTemplate.opsForValue().get(key);// 判断是否存在if (StrUtil.isNotBlank(json)) {// 存在即返回return JSONUtil.toBean(json, type);}// 判断是否为""空值if (json != null) {return null;}// 不存在根据id查询数据库// 使用Function,由调用方法传入数据库查询逻辑R r = dbFallback.apply(id);// 不存在返回错误if (r == null) {// 写入空值,CACHE_NULL_TTL自定义常量,设置空值的过期时间stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);return null;}// 存在,写入redisthis.set(key, r, time, unit);// 返回return r;}

布隆过滤

待更新…

缓存击穿

逻辑过期时间

如果是采用逻辑过期时间策略来避免缓存击穿,那么就需要在给Redis存入数据的时候,将原始数据和我们设置的逻辑时间一并打包存入。这需要我们新增一个实体对象:

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

其中 expireTime 这里记录的是我们设置的过期时间,data 存的就是我们想要存入的数据。JSON数据的解析,我使用的是hutool的工具,需要添加依赖:

		<!--hutool工具--><dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.7.17</version></dependency>

逻辑过期时间详细代码:

	public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit) {RedisData redisData = new RedisData();redisData.setData(value);redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));}// 异步线程池private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);public <R, ID> R queryWithLogicalExpire(String keyPrefix, ID id, Class<R> type,Function<ID, R> dbFallback, Long time, TimeUnit unit) {// 从redis查询商铺信息缓存String key = keyPrefix + id;String json = stringRedisTemplate.opsForValue().get(key);// 判断是否命中if (StrUtil.isBlank(json)) {// 未命中直接返回nullreturn null;}// 命中需要先反序列化JSONRedisData redisData = JSONUtil.toBean(json, RedisData.class);// 判断是否过期JSONObject data = (JSONObject) redisData.getData();R r = JSONUtil.toBean(data, type);LocalDateTime expireTime = redisData.getExpireTime();// 未过期,直接返回if (expireTime.isAfter(LocalDateTime.now())) {// 过期时间是否在当前之后return r;}// 已过期,开始缓存重建// 申请互斥锁boolean isLock = tryLock(key);// 判断是否获取锁成功if (isLock) {// 成功,开启线程执行缓存重建CACHE_REBUILD_EXECUTOR.submit(() -> {try {// 重建缓存// 查询数据库R r1 = dbFallback.apply(id);// 写入Redisthis.setWithLogicalExpire(key, r1, time, unit);} catch (Exception e) {throw new RuntimeException(e);} finally {// 释放锁unlock(key);}});}// 返回店铺过期信息return r;}private boolean tryLock(String key) {// 异步线程的时间,这里设置的是10s,大家根据自己业务需要可以更改Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);return BooleanUtil.isTrue(flag);}private void unlock(String key) {stringRedisTemplate.delete(key);}

互斥锁

待更新…

Redis工具类

为了方便所有业务进行直接使用,我们最好把这一类的解决方案放在一个工具类中,供所有业务直接调用

import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;@Slf4j
@Component
public class CacheClient {private final StringRedisTemplate stringRedisTemplate;public CacheClient(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;}// Redis插入数据public void set(String key, Object value, Long time, TimeUnit unit) {// 业务调用时传入键值,过期时间stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);}// 解决缓存穿透public <R, ID> R queryWithPassThrough(String keyPrefix, ID id, Class<R> type,Function<ID, R> dbFallback, Long time, TimeUnit unit) {String key = keyPrefix + id;// 从redis查询缓存String json = stringRedisTemplate.opsForValue().get(key);// 判断是否存在if (StrUtil.isNotBlank(json)) {// 存在即返回return JSONUtil.toBean(json, type);}// 判断是否为""空值if (json != null) {return null;}// 不存在根据id查询数据库// 使用Function,由调用方法传入数据库查询逻辑R r = dbFallback.apply(id);// 不存在返回错误if (r == null) {// 写入空值,CACHE_NULL_TTL自定义常量,设置空值的过期时间stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);return null;}// 存在,写入redisthis.set(key, r, time, unit);// 返回return r;}public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit) {RedisData redisData = new RedisData();redisData.setData(value);redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));}// 异步线程池private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);public <R, ID> R queryWithLogicalExpire(String keyPrefix, ID id, Class<R> type,Function<ID, R> dbFallback, Long time, TimeUnit unit) {// 从redis查询商铺信息缓存String key = keyPrefix + id;String json = stringRedisTemplate.opsForValue().get(key);// 判断是否命中if (StrUtil.isBlank(json)) {// 未命中直接返回nullreturn null;}// 命中需要先反序列化JSONRedisData redisData = JSONUtil.toBean(json, RedisData.class);// 判断是否过期JSONObject data = (JSONObject) redisData.getData();R r = JSONUtil.toBean(data, type);LocalDateTime expireTime = redisData.getExpireTime();// 未过期,直接返回if (expireTime.isAfter(LocalDateTime.now())) {// 过期时间是否在当前之后return r;}// 已过期,开始缓存重建// 申请互斥锁boolean isLock = tryLock(key);// 判断是否获取锁成功if (isLock) {// 成功,开启线程执行缓存重建CACHE_REBUILD_EXECUTOR.submit(() -> {try {// 重建缓存// 查询数据库R r1 = dbFallback.apply(id);// 写入Redisthis.setWithLogicalExpire(key, r1, time, unit);} catch (Exception e) {throw new RuntimeException(e);} finally {// 释放锁unlock(key);}});}// 返回店铺过期信息return r;}private boolean tryLock(String key) {// 异步线程的时间,这里设置的是10s,大家根据自己业务需要可以更改Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);return BooleanUtil.isTrue(flag);}private void unlock(String key) {stringRedisTemplate.delete(key);}
}

这篇关于【Redis笔记】缓存穿透、缓存击穿的Java代码大致解决方案的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



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

相关文章

javax.net.ssl.SSLHandshakeException:异常原因及解决方案

《javax.net.ssl.SSLHandshakeException:异常原因及解决方案》javax.net.ssl.SSLHandshakeException是一个SSL握手异常,通常在建立SS... 目录报错原因在程序中绕过服务器的安全验证注意点最后多说一句报错原因一般出现这种问题是因为目标服务器

Java实现删除文件中的指定内容

《Java实现删除文件中的指定内容》在日常开发中,经常需要对文本文件进行批量处理,其中,删除文件中指定内容是最常见的需求之一,下面我们就来看看如何使用java实现删除文件中的指定内容吧... 目录1. 项目背景详细介绍2. 项目需求详细介绍2.1 功能需求2.2 非功能需求3. 相关技术详细介绍3.1 Ja

springboot项目中整合高德地图的实践

《springboot项目中整合高德地图的实践》:本文主要介绍springboot项目中整合高德地图的实践,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一:高德开放平台的使用二:创建数据库(我是用的是mysql)三:Springboot所需的依赖(根据你的需求再

spring中的ImportSelector接口示例详解

《spring中的ImportSelector接口示例详解》Spring的ImportSelector接口用于动态选择配置类,实现条件化和模块化配置,关键方法selectImports根据注解信息返回... 目录一、核心作用二、关键方法三、扩展功能四、使用示例五、工作原理六、应用场景七、自定义实现Impor

SpringBoot3应用中集成和使用Spring Retry的实践记录

《SpringBoot3应用中集成和使用SpringRetry的实践记录》SpringRetry为SpringBoot3提供重试机制,支持注解和编程式两种方式,可配置重试策略与监听器,适用于临时性故... 目录1. 简介2. 环境准备3. 使用方式3.1 注解方式 基础使用自定义重试策略失败恢复机制注意事项

SpringBoot整合Flowable实现工作流的详细流程

《SpringBoot整合Flowable实现工作流的详细流程》Flowable是一个使用Java编写的轻量级业务流程引擎,Flowable流程引擎可用于部署BPMN2.0流程定义,创建这些流程定义的... 目录1、流程引擎介绍2、创建项目3、画流程图4、开发接口4.1 Java 类梳理4.2 查看流程图4

一文详解如何在idea中快速搭建一个Spring Boot项目

《一文详解如何在idea中快速搭建一个SpringBoot项目》IntelliJIDEA作为Java开发者的‌首选IDE‌,深度集成SpringBoot支持,可一键生成项目骨架、智能配置依赖,这篇文... 目录前言1、创建项目名称2、勾选需要的依赖3、在setting中检查maven4、编写数据源5、开启热

C++高效内存池实现减少动态分配开销的解决方案

《C++高效内存池实现减少动态分配开销的解决方案》C++动态内存分配存在系统调用开销、碎片化和锁竞争等性能问题,内存池通过预分配、分块管理和缓存复用解决这些问题,下面就来了解一下... 目录一、C++内存分配的性能挑战二、内存池技术的核心原理三、主流内存池实现:TCMalloc与Jemalloc1. TCM

Java对异常的认识与异常的处理小结

《Java对异常的认识与异常的处理小结》Java程序在运行时可能出现的错误或非正常情况称为异常,下面给大家介绍Java对异常的认识与异常的处理,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参... 目录一、认识异常与异常类型。二、异常的处理三、总结 一、认识异常与异常类型。(1)简单定义-什么是

Redis Cluster模式配置

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