缓存-Redis-常见问题-缓存击穿-永不过期+逻辑过期(全面 易理解)
缓存击穿(Cache Breakdown) 是在高并发场景下,当某个热点数据在缓存中失效或不存在时,瞬间大量请求同时击中数据库,导致数据库压力骤增甚至崩溃的现象。为了解决这一问题,“永不过期” + “逻辑过期” 的策略是一种有效的解决方案。这种方法通过将缓存数据设为永不过期,同时在数据内部维护一个逻辑过期时间,从而控制何时更新缓存,避免大量请求直接访问数据库。
本文将详细介绍这一解决方案,并提供完整的 Java 实现示例,使用 Redis 作为缓存存储。
一、“永不过期” + “逻辑过期” 策略概述
1. 永不过期
将缓存数据设置为永不过期(即不依赖 Redis 的 TTL),这样缓存项本身不会因时间原因自动失效。所有的过期逻辑由应用程序内部控制。
2. 逻辑过期
每个缓存数据项内部包含一个逻辑过期时间(如时间戳)。当应用程序读取数据时,会检查当前时间与逻辑过期时间的关系:
- 未过期:直接返回缓存数据。
- 已过期:
- 触发后台线程(或异步任务)刷新缓存数据。
- 立即返回旧的缓存数据,保持应用响应性。
通过这种方式,可以避免大量请求同时刷新缓存,减轻数据库压力,同时确保数据在逻辑上是最新的。
二、实现步骤
- 定义缓存数据结构:将数据与逻辑过期时间一起存储在 Redis 中。
- 读取数据时检查逻辑过期时间:
- 如果未过期,直接返回数据。
- 如果已过期,异步刷新缓存,并返回旧数据。
- 刷新缓存数据:
- 仅允许一个线程进行数据刷新,避免多线程同时刷新。
- 更新 Redis 中的数据及其逻辑过期时间。
三、Java 实现示例
以下是一个基于 Java 和 Redis 的完整实现示例。我们将使用 Redisson 作为 Redis 客户端,它支持分布式锁和异步操作,适合实现“永不过期” + “逻辑过期” 策略。
1. 引入依赖
首先,在项目的 pom.xml 中添加 Redisson 依赖:
<dependencies><!-- Redisson --><dependency><groupId>org.redisson</groupId><artifactId>redisson</artifactId><version>3.23.6</version></dependency><!-- JSON 处理(如使用 Jackson) --><dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-databind</artifactId><version>2.15.0</version></dependency>
</dependencies>
2. 定义缓存数据结构
我们需要一个数据结构来存储实际数据和逻辑过期时间。以下是一个示例类:
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;public class CacheData<T> {@JsonProperty("data")private T data;@JsonProperty("expiryTime")private long expiryTime; // 逻辑过期时间,单位毫秒public CacheData() {}public CacheData(T data, long expiryTime) {this.data = data;this.expiryTime = expiryTime;}public T getData() {return data;}public void setData(T data) {this.data = data;}public long getExpiryTime() {return expiryTime;}public void setExpiryTime(long expiryTime) {this.expiryTime = expiryTime;}@JsonIgnorepublic boolean isExpired() {return System.currentTimeMillis() > expiryTime;}
}
3. Redis 配置与初始化
配置 Redisson 客户端以连接 Redis:
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;public class RedisConfig {private static RedissonClient redissonClient;static {Config config = new Config();// 配置单机模式config.useSingleServer().setAddress("redis://127.0.0.1:6379").setConnectionTimeout(10000).setRetryAttempts(3).setRetryInterval(1500);redissonClient = Redisson.create(config);}public static RedissonClient getRedissonClient() {return redissonClient;}
}
4. 缓存管理器实现
实现缓存读取、逻辑过期检查和异步刷新:
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;import java.io.IOException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;public class CacheManager {private RedissonClient redissonClient;private ObjectMapper objectMapper;private ExecutorService executorService;// 缓存逻辑过期时间,单位毫秒private final long LOGICAL_EXPIRY = 5 * 60 * 1000; // 5分钟public CacheManager() {this.redissonClient = RedisConfig.getRedissonClient();this.objectMapper = new ObjectMapper();// 创建固定线程池用于异步刷新this.executorService = Executors.newFixedThreadPool(10);}/*** 获取缓存数据** @param key Redis 键* @param dbQueryFunc 查询数据库的函数* @param <T> 数据类型* @return 缓存数据或旧数据*/public <T> T getCacheData(String key, DBQueryFunc<T> dbQueryFunc) {try {String json = redissonClient.getBucket(key).get().toString();if (json != null) {// 反序列化CacheData<T> cacheData = objectMapper.readValue(json, CacheData.class);if (!cacheData.isExpired()) {// 未过期,返回数据return cacheData.getData();} else {// 已过期,异步刷新refreshCacheAsync(key, dbQueryFunc);// 返回旧数据return cacheData.getData();}} else {// 缓存不存在,尝试刷新refreshCacheAsync(key, dbQueryFunc);// 返回 null 或者可以选择同步查询数据库return null;}} catch (IOException e) {e.printStackTrace();return null;}}/*** 异步刷新缓存** @param key Redis 键* @param dbQueryFunc 查询数据库的函数* @param <T> 数据类型*/private <T> void refreshCacheAsync(String key, DBQueryFunc<T> dbQueryFunc) {executorService.submit(() -> {RLock lock = redissonClient.getLock("lock:" + key);boolean isLockAcquired = false;try {// 尝试获取锁,防止多线程同时刷新isLockAcquired = lock.tryLock(500, 3000, TimeUnit.MILLISECONDS);if (isLockAcquired) {// 再次检查缓存是否过期,防止被其他线程刷新String json = redissonClient.getBucket(key).get().toString();CacheData<T> cacheData = objectMapper.readValue(json, CacheData.class);if (cacheData.isExpired()) {// 查询数据库T data = dbQueryFunc.query();// 更新缓存CacheData<T> newCacheData = new CacheData<>(data, System.currentTimeMillis() + LOGICAL_EXPIRY);String newJson = objectMapper.writeValueAsString(newCacheData);redissonClient.getBucket(key).set(newJson);}}} catch (InterruptedException | IOException e) {e.printStackTrace();} finally {if (isLockAcquired && lock.isHeldByCurrentThread()) {lock.unlock();}}});}/*** 刷新缓存数据(同步调用,用于缓存不存在时)** @param key Redis 键* @param dbQueryFunc 查询数据库的函数* @param <T> 数据类型*/public <T> T refreshCache(String key, DBQueryFunc<T> dbQueryFunc) {RLock lock = redissonClient.getLock("lock:" + key);boolean isLockAcquired = false;try {// 获取锁,等待最多 500 毫秒isLockAcquired = lock.tryLock(500, 3000, TimeUnit.MILLISECONDS);if (isLockAcquired) {// 查询数据库T data = dbQueryFunc.query();// 更新缓存CacheData<T> newCacheData = new CacheData<>(data, System.currentTimeMillis() + LOGICAL_EXPIRY);String newJson = objectMapper.writeValueAsString(newCacheData);redissonClient.getBucket(key).set(newJson);return data;} else {// 获取锁失败,可能由其他线程刷新,等待一段时间后尝试获取Thread.sleep(100);String json = redissonClient.getBucket(key).get().toString();if (json != null) {CacheData<T> cacheData = objectMapper.readValue(json, CacheData.class);return cacheData.getData();} else {// 最终未获取到数据,返回 null 或选择其他处理方式return null;}}} catch (InterruptedException | IOException e) {e.printStackTrace();return null;} finally {if (isLockAcquired && lock.isHeldByCurrentThread()) {lock.unlock();}}}/*** 关闭缓存管理器,释放资源*/public void shutdown() {executorService.shutdown();redissonClient.shutdown();}/*** 数据库查询函数接口** @param <T> 数据类型*/public interface DBQueryFunc<T> {T query();}
}
5. 使用示例
假设我们有一个 User 数据模型,并希望缓存用户信息:
public class User {private String id;private String name;private int age;// 构造方法、getter、setter等public User() {}public User(String id, String name, int age) {this.id = id;this.name = name;this.age = age;}// Getters and Setters// ...
}
模拟数据库查询方法:
public class UserService {/*** 模拟数据库查询** @param userId 用户 ID* @return 用户信息*/public User getUserFromDB(String userId) {// 模拟数据库延迟try {Thread.sleep(100); // 100ms 延迟} catch (InterruptedException e) {e.printStackTrace();}// 返回模拟数据return new User(userId, "User_" + userId, 25);}
}
主程序示例:
public class Main {public static void main(String[] args) {CacheManager cacheManager = new CacheManager();UserService userService = new UserService();String userId = "12345";String cacheKey = "user:" + userId;// 定义数据库查询函数CacheManager.DBQueryFunc<User> dbQueryFunc = () -> userService.getUserFromDB(userId);// 第一次访问,缓存可能不存在或已过期User user = cacheManager.getCacheData(cacheKey, dbQueryFunc);if (user == null) {// 缓存不存在,进行同步刷新user = cacheManager.refreshCache(cacheKey, dbQueryFunc);}System.out.println("User: " + user.getName() + ", Age: " + user.getAge());// 之后的访问,如果缓存未过期,直接返回缓存数据User cachedUser = cacheManager.getCacheData(cacheKey, dbQueryFunc);System.out.println("Cached User: " + cachedUser.getName() + ", Age: " + cachedUser.getAge());// 关闭缓存管理器cacheManager.shutdown();}
}
6. 运行流程说明
-
首次访问:
- 调用
getCacheData方法。 - 缓存可能不存在或已逻辑过期。
- 触发异步刷新缓存,通过
refreshCacheAsync方法。 - 如果缓存不存在,调用
refreshCache方法进行同步刷新。 - 从数据库获取数据并更新缓存。
- 返回获取到的数据。
- 调用
-
后续访问:
- 调用
getCacheData方法。 - 检查逻辑过期时间。
- 如果未过期,直接返回缓存数据。
- 如果已过期,触发异步刷新缓存,同时返回旧数据,保持高响应性。
- 调用
7. 优点与注意事项
优点
- 防止缓存击穿:通过锁机制和异步刷新,避免高并发下大量请求同时触发数据库访问。
- 高响应性:即使缓存已逻辑过期,也能立即返回旧数据,不会造成请求阻塞。
- 灵活性:逻辑过期时间可根据业务需求动态调整。
注意事项
- 数据一致性:旧数据可能与数据库中的最新数据存在一定的时间差,需要根据业务需求权衡。
- 锁的可靠性:确保分布式锁机制的可靠性,避免死锁或锁丢失。
- 线程池管理:合理配置线程池大小,避免过多异步任务导致资源竞争。
- 异常处理:完善异常处理机制,确保在数据刷新失败时系统稳定。
四、扩展与优化
1. 使用 Redis Lua 脚本优化原子性
为了进一步确保操作的原子性,可以考虑使用 Redis 的 Lua 脚本,将读取和写入操作合并为一个原子操作。
2. 引入消息队列进行异步刷新
对于大规模分布式系统,可以引入消息队列(如 Kafka、RabbitMQ)来异步处理缓存刷新任务,提升系统的可扩展性和可靠性。
3. 监控与报警
建立完善的监控机制,实时监控缓存命中率、数据库访问量、缓存刷新失败次数等指标,及时发现并处理异常情况。
五、总结
通过 “永不过期” + “逻辑过期” 的策略,可以有效防止缓存击穿问题,确保系统在高并发下的稳定性和高可用性。本文详细介绍了该策略的原理及其 Java 实现,包括数据结构设计、缓存读取与逻辑过期检查、异步刷新机制等关键环节。根据实际业务需求,开发者可以进一步优化和扩展这一策略,以构建高性能、高可靠性的分布式系统。
相关文章:
缓存-Redis-常见问题-缓存击穿-永不过期+逻辑过期(全面 易理解)
缓存击穿(Cache Breakdown) 是在高并发场景下,当某个热点数据在缓存中失效或不存在时,瞬间大量请求同时击中数据库,导致数据库压力骤增甚至崩溃的现象。为了解决这一问题,“永不过期” “逻辑过期” 的策略…...
137. 只出现一次的数字 II
137. 只出现一次的数字 II 题目-中等难度1. 位运算2. 位运算 题目-中等难度 给你一个整数数组 nums ,除某个元素仅出现 一次 外,其余每个元素都恰出现 三次 。请你找出并返回那个只出现了一次的元素。 你必须设计并实现线性时间复杂度的算法且使用常数…...
【力扣热题100】—— Day18.将有序数组转换为二叉搜索树
期末考试完毕,假期学习开始! —— 25.1.7 108. 将有序数组转换为二叉搜索树 给你一个整数数组 nums ,其中元素已经按 升序 排列,请你将其转换为一棵平衡二叉搜索树。 示例 1: 输入:nums [-10,-3,0,5,9] …...
PyTorch 官方文档 中文版本
文档来源 https://pytorch.cadn.net.cn 大多数机器学习工作流都涉及处理数据、创建模型、优化模型 参数,并保存经过训练的模型。本教程向您介绍完整的 ML 工作流 在 PyTorch 中实现,并提供了用于了解有关每个概念的更多信息的链接。 我们将使用 Fashion…...
电力智能问答RAG: 多问题生成、思维链提示生成;混合编码和重排序策略
电力智能问答RAG 目录 电力智能问答RAG文档转换、元信息抽取与增强及文档解析模块多问题生成、思维链提示生成和指令微调数据集构建模块混合编码和重排序策略文档转换、元信息抽取与增强及文档解析模块 在电力领域的知识处理中,文档转换、元信息抽取与增强及文档解析模块发挥…...
C#高级:递归4-根据一颗树递归生成数据列表
一、目的 该程序展示了如何将树形结构的数据(例如家庭成员信息)转化为一维列表形式,以便于存储、展示或操作。 二、流程思路 创建树:首先通过 GetDemoTree 创建一个简单的家庭树,树的根节点是“爸爸”,然…...
PDFelement 特别版
Wondershare PDFelement Pro 是一款非常强大的PDF编辑软件,它允许用户轻松地编辑、转换、创建和管理PDF文件。这个中文特别版的软件具有许多令人印象深刻的功能,PDFelement Pro 提供了丰富的编辑功能,可以帮助用户直接在PDF文件中添加、删除、…...
云计算在医疗行业的应用
云计算在医疗行业的应用广泛而深入,为医疗服务带来了前所未有的变革。以下是对云计算在医疗行业应用的详细解析: ### 一、医疗数据共享与整合 云计算平台具有强大的数据存储和处理能力,使得医疗数据共享与整合成为可能。通过云计算平台&…...
(转)rabbitmq怎么保证消息不丢失?
RabbitMQ 可以通过以下多种机制来保证消息不丢失: 生产阶段 - 持久化队列和交换器: - 在声明队列和交换器时,将 durable 参数设置为 true ,确保它们是持久化的。这样,即使 RabbitMQ 节点重新启动,队列和交…...
每日一题:链表中环的入口结点
文章目录 判断链表环的入口节点描述数据范围:复杂度要求:输入输出 示例代码实现思路解析注意事项: 判断链表环的入口节点 描述 给定一个链表,判断该链表是否存在环。如果存在环,返回环的入口节点;如果不存…...
k8s里面etcd的作用
etcd 是 Kubernetes 集群中一个至关重要的组件,它是一个开源的分布式键值存储系统,主要用于存储和管理 Kubernetes 集群的配置和状态信息。以下是 etcd 在 Kubernetes 中的具体作用和功能: ### 1. **集群状态存储** etcd 是 Kubernetes 集群的持久化存储后端,负责存储和管…...
使用 uniapp 开发微信小程序遇到的坑
0. 每次修改代码时,都会触发微信开发工具重新编译 终极大坑,暂未找到解决方案 1. input 无法聚焦问题 问题:在小程序开发工具中,input 会突然无法聚焦,重启也不行。但是真机调试可以正常聚焦。 解决办法:…...
AlphaPi相关硬件驱动提取
初涉硬件编程,在咸鱼上搞了几块AlphaPi和microbit的板鼓捣了一下,alphapi生态不完善,网上又无任何文档,搞封闭,可玩性实在有限,但貌似相关扩展板是可以插microbit的,于是想把这些扩展版用microb…...
【学习笔记】数据结构(十)
内部排序 文章目录 内部排序10.1 概述10.2 插入排序10.2.1 直接插入排序10.2.2 其他插入排序10.2.2.1 折半插入排序(Binary Insertion Sort)10.2.2.2 2-路插入排序(Two-Way Insertion Sort)10.2.2.3 表插入排序(Table Insertion Sort…...
Unity中 Xlua使用整理(二)
1.Xlua的配置应用 xLua所有的配置都支持三种方式:打标签;静态列表;动态列表。配置要求: 列表方式均必须是static的字段/属性 列表方式均必须放到一个static类 建议不用标签方式 建议列表方式配置放Editor目录(如果是H…...
刚体变换矩阵的逆
刚体运动中的变换矩阵为: 求得变换矩阵的逆矩阵为: opencv应用 cv::Mat R; cv::Mat t;R.t(), -R.t()*t...
高等数学-----极限、函数、连续
考研数学笔记...
ubuntu 创建服务、查看服务日志
1. 在 /etc/systemd/system/ 下创建文件,名称为 xxx.service [Unit] DescriptionYour Service Description Afternetwork.target[Service] Typesimple ExecStart/path/to/your/service/executable Restarton-failure[Install] WantedBymulti-user.target2. 配置服务…...
如何监控批量写入的性能瓶颈?
监控批量写入的性能瓶颈是优化数据写入过程的关键步骤。通过系统化的监控和分析,可以识别出影响性能的具体环节,并采取相应的优化措施。以下是详细的监控方法和步骤: ### 1. **数据库性能监控** #### a. **数据库内置监控工具** 大多数数据库系统都提供了内置的性能监控工…...
Ubuntu挂载Windows 磁盘,双系统
首先我们需要在终端输入这个命令,来查看磁盘分配情况 lsblk -f 找到需要挂载的磁盘,检查其类型( 我的/dev/nvme2n1p1类型是ntfs,名字叫3500winData) 然后新建一个挂载磁盘的目录,我的是/media/zeqi/3500wi…...
DeepSeek 赋能智慧能源:微电网优化调度的智能革新路径
目录 一、智慧能源微电网优化调度概述1.1 智慧能源微电网概念1.2 优化调度的重要性1.3 目前面临的挑战 二、DeepSeek 技术探秘2.1 DeepSeek 技术原理2.2 DeepSeek 独特优势2.3 DeepSeek 在 AI 领域地位 三、DeepSeek 在微电网优化调度中的应用剖析3.1 数据处理与分析3.2 预测与…...
AI Agent与Agentic AI:原理、应用、挑战与未来展望
文章目录 一、引言二、AI Agent与Agentic AI的兴起2.1 技术契机与生态成熟2.2 Agent的定义与特征2.3 Agent的发展历程 三、AI Agent的核心技术栈解密3.1 感知模块代码示例:使用Python和OpenCV进行图像识别 3.2 认知与决策模块代码示例:使用OpenAI GPT-3进…...
React Native在HarmonyOS 5.0阅读类应用开发中的实践
一、技术选型背景 随着HarmonyOS 5.0对Web兼容层的增强,React Native作为跨平台框架可通过重新编译ArkTS组件实现85%以上的代码复用率。阅读类应用具有UI复杂度低、数据流清晰的特点。 二、核心实现方案 1. 环境配置 (1)使用React Native…...
如何在看板中有效管理突发紧急任务
在看板中有效管理突发紧急任务需要:设立专门的紧急任务通道、重新调整任务优先级、保持适度的WIP(Work-in-Progress)弹性、优化任务处理流程、提高团队应对突发情况的敏捷性。其中,设立专门的紧急任务通道尤为重要,这能…...
LLM基础1_语言模型如何处理文本
基于GitHub项目:https://github.com/datawhalechina/llms-from-scratch-cn 工具介绍 tiktoken:OpenAI开发的专业"分词器" torch:Facebook开发的强力计算引擎,相当于超级计算器 理解词嵌入:给词语画"…...
【开发技术】.Net使用FFmpeg视频特定帧上绘制内容
目录 一、目的 二、解决方案 2.1 什么是FFmpeg 2.2 FFmpeg主要功能 2.3 使用Xabe.FFmpeg调用FFmpeg功能 2.4 使用 FFmpeg 的 drawbox 滤镜来绘制 ROI 三、总结 一、目的 当前市场上有很多目标检测智能识别的相关算法,当前调用一个医疗行业的AI识别算法后返回…...
智能AI电话机器人系统的识别能力现状与发展水平
一、引言 随着人工智能技术的飞速发展,AI电话机器人系统已经从简单的自动应答工具演变为具备复杂交互能力的智能助手。这类系统结合了语音识别、自然语言处理、情感计算和机器学习等多项前沿技术,在客户服务、营销推广、信息查询等领域发挥着越来越重要…...
return this;返回的是谁
一个审批系统的示例来演示责任链模式的实现。假设公司需要处理不同金额的采购申请,不同级别的经理有不同的审批权限: // 抽象处理者:审批者 abstract class Approver {protected Approver successor; // 下一个处理者// 设置下一个处理者pub…...
Web中间件--tomcat学习
Web中间件–tomcat Java虚拟机详解 什么是JAVA虚拟机 Java虚拟机是一个抽象的计算机,它可以执行Java字节码。Java虚拟机是Java平台的一部分,Java平台由Java语言、Java API和Java虚拟机组成。Java虚拟机的主要作用是将Java字节码转换为机器代码&#x…...
FFmpeg:Windows系统小白安装及其使用
一、安装 1.访问官网 Download FFmpeg 2.点击版本目录 3.选择版本点击安装 注意这里选择的是【release buids】,注意左上角标题 例如我安装在目录 F:\FFmpeg 4.解压 5.添加环境变量 把你解压后的bin目录(即exe所在文件夹)加入系统变量…...
