缓存-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…...
未来机器人的大脑:如何用神经网络模拟器实现更智能的决策?
编辑:陈萍萍的公主一点人工一点智能 未来机器人的大脑:如何用神经网络模拟器实现更智能的决策?RWM通过双自回归机制有效解决了复合误差、部分可观测性和随机动力学等关键挑战,在不依赖领域特定归纳偏见的条件下实现了卓越的预测准…...
网络六边形受到攻击
大家读完觉得有帮助记得关注和点赞!!! 抽象 现代智能交通系统 (ITS) 的一个关键要求是能够以安全、可靠和匿名的方式从互联车辆和移动设备收集地理参考数据。Nexagon 协议建立在 IETF 定位器/ID 分离协议 (…...
基于FPGA的PID算法学习———实现PID比例控制算法
基于FPGA的PID算法学习 前言一、PID算法分析二、PID仿真分析1. PID代码2.PI代码3.P代码4.顶层5.测试文件6.仿真波形 总结 前言 学习内容:参考网站: PID算法控制 PID即:Proportional(比例)、Integral(积分&…...
Appium+python自动化(十六)- ADB命令
简介 Android 调试桥(adb)是多种用途的工具,该工具可以帮助你你管理设备或模拟器 的状态。 adb ( Android Debug Bridge)是一个通用命令行工具,其允许您与模拟器实例或连接的 Android 设备进行通信。它可为各种设备操作提供便利,如安装和调试…...
Day131 | 灵神 | 回溯算法 | 子集型 子集
Day131 | 灵神 | 回溯算法 | 子集型 子集 78.子集 78. 子集 - 力扣(LeetCode) 思路: 笔者写过很多次这道题了,不想写题解了,大家看灵神讲解吧 回溯算法套路①子集型回溯【基础算法精讲 14】_哔哩哔哩_bilibili 完…...
【SpringBoot】100、SpringBoot中使用自定义注解+AOP实现参数自动解密
在实际项目中,用户注册、登录、修改密码等操作,都涉及到参数传输安全问题。所以我们需要在前端对账户、密码等敏感信息加密传输,在后端接收到数据后能自动解密。 1、引入依赖 <dependency><groupId>org.springframework.boot</groupId><artifactId...
汽车生产虚拟实训中的技能提升与生产优化
在制造业蓬勃发展的大背景下,虚拟教学实训宛如一颗璀璨的新星,正发挥着不可或缺且日益凸显的关键作用,源源不断地为企业的稳健前行与创新发展注入磅礴强大的动力。就以汽车制造企业这一极具代表性的行业主体为例,汽车生产线上各类…...
使用Matplotlib创建炫酷的3D散点图:数据可视化的新维度
文章目录 基础实现代码代码解析进阶技巧1. 自定义点的大小和颜色2. 添加图例和样式美化3. 真实数据应用示例实用技巧与注意事项完整示例(带样式)应用场景在数据科学和可视化领域,三维图形能为我们提供更丰富的数据洞察。本文将手把手教你如何使用Python的Matplotlib库创建引…...
【笔记】WSL 中 Rust 安装与测试完整记录
#工作记录 WSL 中 Rust 安装与测试完整记录 1. 运行环境 系统:Ubuntu 24.04 LTS (WSL2)架构:x86_64 (GNU/Linux)Rust 版本:rustc 1.87.0 (2025-05-09)Cargo 版本:cargo 1.87.0 (2025-05-06) 2. 安装 Rust 2.1 使用 Rust 官方安…...
Java求职者面试指南:Spring、Spring Boot、Spring MVC与MyBatis技术解析
Java求职者面试指南:Spring、Spring Boot、Spring MVC与MyBatis技术解析 一、第一轮基础概念问题 1. Spring框架的核心容器是什么?它的作用是什么? Spring框架的核心容器是IoC(控制反转)容器。它的主要作用是管理对…...
