缓存-Redis-常见问题-缓存击穿-永不过期+逻辑过期(全面 易理解)

缓存击穿(Cache Breakdown) 是在高并发场景下,当某个热点数据在缓存中失效或不存在时,瞬间大量请求同时击中数据库,导致数据库压力骤增甚至崩溃的现象。为了解决这一问题,“永不过期” + “逻辑过期” 的策略是一种有效的解决方案。这种方法通过将缓存数据设为永不过期,同时在数据内部维护一个逻辑过期时间,从而控制何时更新缓存,避免大量请求直接访问数据库。

本文将详细介绍这一解决方案,并提供完整的 Java 实现示例,使用 Redis 作为缓存存储。

一、“永不过期” + “逻辑过期” 策略概述

1. 永不过期

将缓存数据设置为永不过期(即不依赖 Redis 的 TTL),这样缓存项本身不会因时间原因自动失效。所有的过期逻辑由应用程序内部控制。

2. 逻辑过期

每个缓存数据项内部包含一个逻辑过期时间(如时间戳)。当应用程序读取数据时,会检查当前时间与逻辑过期时间的关系:

  • 未过期:直接返回缓存数据。
  • 已过期
    • 触发后台线程(或异步任务)刷新缓存数据。
    • 立即返回旧的缓存数据,保持应用响应性。

通过这种方式,可以避免大量请求同时刷新缓存,减轻数据库压力,同时确保数据在逻辑上是最新的。

二、实现步骤

  1. 定义缓存数据结构:将数据与逻辑过期时间一起存储在 Redis 中。
  2. 读取数据时检查逻辑过期时间
    • 如果未过期,直接返回数据。
    • 如果已过期,异步刷新缓存,并返回旧数据。
  3. 刷新缓存数据
    • 仅允许一个线程进行数据刷新,避免多线程同时刷新。
    • 更新 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. 运行流程说明

  1. 首次访问

    • 调用 getCacheData 方法。
    • 缓存可能不存在或已逻辑过期。
    • 触发异步刷新缓存,通过 refreshCacheAsync 方法。
    • 如果缓存不存在,调用 refreshCache 方法进行同步刷新。
    • 从数据库获取数据并更新缓存。
    • 返回获取到的数据。
  2. 后续访问

    • 调用 getCacheData 方法。
    • 检查逻辑过期时间。
    • 如果未过期,直接返回缓存数据。
    • 如果已过期,触发异步刷新缓存,同时返回旧数据,保持高响应性。

7. 优点与注意事项

优点
  • 防止缓存击穿:通过锁机制和异步刷新,避免高并发下大量请求同时触发数据库访问。
  • 高响应性:即使缓存已逻辑过期,也能立即返回旧数据,不会造成请求阻塞。
  • 灵活性:逻辑过期时间可根据业务需求动态调整。
注意事项
  • 数据一致性:旧数据可能与数据库中的最新数据存在一定的时间差,需要根据业务需求权衡。
  • 锁的可靠性:确保分布式锁机制的可靠性,避免死锁或锁丢失。
  • 线程池管理:合理配置线程池大小,避免过多异步任务导致资源竞争。
  • 异常处理:完善异常处理机制,确保在数据刷新失败时系统稳定。

四、扩展与优化

1. 使用 Redis Lua 脚本优化原子性

为了进一步确保操作的原子性,可以考虑使用 Redis 的 Lua 脚本,将读取和写入操作合并为一个原子操作。

2. 引入消息队列进行异步刷新

对于大规模分布式系统,可以引入消息队列(如 Kafka、RabbitMQ)来异步处理缓存刷新任务,提升系统的可扩展性和可靠性。

3. 监控与报警

建立完善的监控机制,实时监控缓存命中率、数据库访问量、缓存刷新失败次数等指标,及时发现并处理异常情况。

五、总结

通过 “永不过期” + “逻辑过期” 的策略,可以有效防止缓存击穿问题,确保系统在高并发下的稳定性和高可用性。本文详细介绍了该策略的原理及其 Java 实现,包括数据结构设计、缓存读取与逻辑过期检查、异步刷新机制等关键环节。根据实际业务需求,开发者可以进一步优化和扩展这一策略,以构建高性能、高可靠性的分布式系统。

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

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

相关文章

Hadoop 实战笔记(一) -- Windows 安装 Hadoop 3.x

环境准备 安装 JAVA 1.8 Java环境搭建之JDK下载及安装下载 Hadoop 3.3.5 安装包 Hadoop 下载&#xff1a;https://archive.apache.org/dist/hadoop/common/ 一、JAVA JDK 环境检查 二、Hadoop(HDFS)环境搭建 1. 解压安装文件 hadoop-3.3.5.tar 2. 配置环境变量 HADOOP_HO…

个人博客搭建(二)—Typora+PicGo+OSS

个人博客站—运维鹿: http://www.kervin24.top CSDN博客—做个超努力的小奚&#xff1a; 做个超努力的小奚-CSDN博客 一、前言 博客搭建完一直没有更新&#xff0c;因为WordPress自带的文档编辑器不方便&#xff0c;以前用CSDN写作的时候&#xff0c;习惯了Typora。最近对比了…

【向量数据库】搜索算法

最近几年&#xff0c;一种叫做向量数据库的产品&#xff0c;正趁着AI的热潮开始崭露头角。伴随着AI时代的到来&#xff0c;向量将成为一种重要的数据形式&#xff0c;而传统数据库并不适合用来存储和检索向量数据&#xff0c;因此我们大约需要一种专门设计的数据库来处理这些问…

ARM CCA机密计算安全模型之安全生命周期管理

安全之安全(security)博客目录导读 目录 一、固件启用的调试 二、CCA系统安全生命周期 三、重新供应 四、可信子系统与CCA HES 启用 CCA&#xff08;机密计算架构&#xff09;的安全系统是指 CCA 平台的实现处于可信状态。 由于多种原因&#xff0c;CCA 启用系统可能处于不…

k8s排错集:zk集群的pod报错 Init:CrashLoopBackOff无法启动

zk三节点集群&#xff0c;zk-0无法启动 statefulset 进到该node节点上查看容器的报错日志&#xff0c;发现在初始化container的时候一个命令有问题 查看正常zk集群的pod的资源配置文件 解决办法&#xff1a; 修改资源配置文件 应该修改为 chown -R 1000:1000 /zkenv kubec…

Golang的并发编程框架比较

# Golang的并发编程框架比较 中的并发编程 在现代软件开发中&#xff0c;处理高并发的能力愈发重要。Golang作为一门支持并发编程的编程语言&#xff0c;提供了丰富的并发编程框架和工具&#xff0c;使得开发者能够更轻松地处理并发任务。本文将介绍Golang中几种常用的并发编程…

【Web】软件系统安全赛CachedVisitor——记一次二开工具的经历

明天开始考试周&#xff0c;百无聊赖开了一把CTF&#xff0c;还顺带体验了下二开工具&#xff0c;让无聊的Z3很开心&#x1f642; CachedVisitor这题 大概描述一下&#xff1a;从main.lua加载一段visit.script中被##LUA_START##(.-)##LUA_END##包裹的lua代码 main.lua loca…

单纯形法的学习笔记

文章目录 A. 单纯形法概述1. 优化模型示例 B. 理论基础C. 算法思想D. 实现算法1. 线性规划的标准型2. 顶点解的理解及表示2.1 在标准型中变量取值为零的意义2.2 顶点解的表示 3. 最优性判断4. 解的更新5. 完成迭代过程 E. 单纯形法的基本概念与本文对照F. 文档源码 前言&#x…

【VBA】【EXCEL】将某列内容横向粘贴到指定行

Sub CopyRowToColumn()On Error GoTo ErrorHandler 添加错误处理Application.ScreenUpdating FalseApplication.Calculation xlCalculationManualApplication.EnableEvents False 禁用事件处理Dim lastCol As LongDim lastRow As LongDim i As Long, colCount As LongDim …

JS进阶--JS听到了不灭的回响

作用域 作用域&#xff08;scope&#xff09;规定了变量能够被访问的“范围”&#xff0c;离开了这个“范围”变量便不能被访问 作用域分为局部和全局 局部作用域 局部作用域分为函数和块 那 什么是块作用域呢&#xff1f; 在 JavaScript 中使用 { } 包裹的代码称为代码块…

计算机网络 (26)互联网的路由选择协议

一、路由选择协议的基本概念 路由选择协议是计算机网络中用于确定数据包在网络中传输路径的一种协议。它帮助路由器构建和维护路由表&#xff0c;以便根据目的地址将数据包转发到正确的下一跳路由器。路由选择协议分为静态路由选择协议和动态路由选择协议两大类。 二、静态路由…

Spring项目创建流程及配置文件bean标签参数简介

1. 项目搭建流程 1. pom.xml中引入依赖Spring-webMVC <!-- https://mvnrepository.com/artifact/org.springframework/spring-webmvc --><dependency><groupId>org.springframework</groupId><artifactId>spring-webmvc</artifactId><…

左神算法基础巩固--2

文章目录 稳定性选择排序冒泡排序插入排序归并排序快速排序堆排序 哈希表链表解题 稳定性 稳定性是指算法在排序过程中保持相等元素之间相对顺序的特性。具体来说&#xff0c;如果一个排序算法是稳定的&#xff0c;那么对于任意两个相等的元素&#xff0c;在排序前它们的相对顺…

UART串口数据分析

串口基础知识详细介绍&#xff1a; 该链接详细介绍了串并行、单双工、同异步、连接方式 https://blog.csdn.net/weixin_43386810/article/details/127156063 该文章将介绍串口数据的电平变化、波特率计算、脉宽计算以及数据传输量的计算。 捕获工具&#xff1a;逻辑分析仪&…

机器学习模型评估指标

模型的评估指标是衡量一个模型应用于对应任务的契合程度&#xff0c;常见的指标有&#xff1a; 准确率&#xff08;Accuracy&#xff09;: 正确预测的样本数占总样本数的比例。适用于类别分布均衡的数据集。 精确率&#xff08;Precision&#xff09;: 在所有被预测为正类的样…

面试题解,JVM中的“类加载”剖析

一、JVM类加载机制说一下 其中&#xff0c;从加载到初始化就是我们的类加载阶段&#xff0c;我们逐一来分析 加载 “加载 loading”是整个类加载&#xff08;class loading&#xff09;过程的一个阶段&#xff0c;加载阶段JVM需要完成以下 3 件事情&#xff1a; 1&#xff0…

腾讯云AI代码助手编程挑战赛-古诗词学习

一、作品介绍 在科技与文化深度交融的当下&#xff0c;“腾讯云 AI 代码助手编程挑战赛 - 每日古诗词” 宛如一颗璀璨的新星&#xff0c;闪耀登场。它绝非一场普通的赛事&#xff0c;而是一座连接编程智慧与古典诗词韵味的桥梁。 这项挑战赛以独特的视角&#xff0c;将每日古…

GelSight Mini视触觉传感器凝胶触头升级:增加40%耐用性,拓展机器人与触觉AI 应用边界

马萨诸塞州沃尔瑟姆-2025年1月6日-触觉智能技术领军企业Gelsight宣布&#xff0c;旗下Gelsight Mini视触觉传感器迎来凝胶触头的更新。经内部测试&#xff0c;新Gel凝胶触头耐用性提升40%&#xff0c;外观与触感与原凝胶触头保持一致。此次升级有效满足了客户在机器人应用中对设…

【C++入门】详解(上)

目录 &#x1f495;1.C中main函数内部———变量的访问顺序 &#x1f495;2.命名空间域namespace &#x1f495;3.命名空间域&#xff08;代码示例&#xff09;&#xff08;不要跳&#xff09; &#x1f495;4.多个命名空间域的内部重名 &#x1f495;5.命名空间域的展开 …

Ungoogled Chromium127 编译指南 MacOS篇(八)- 开始编译

1. 引言 完成了所有依赖包的安装后&#xff0c;我们终于来到了最关键的编译阶段。在开始编译之前&#xff0c;有一些重要的配置信息需要了解。本文将指导您完成整个编译过程。 2. 签名相关说明 虽然在我们的测试编译中不需要进行签名操作&#xff0c;但了解官方的签名要求仍…