缓存-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;
    }

    @JsonIgnore
    public 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 实现,包括数据结构设计、缓存读取与逻辑过期检查、异步刷新机制等关键环节。根据实际业务需求,开发者可以进一步优化和扩展这一策略,以构建高性能、高可靠性的分布式系统。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【TI毫米波雷达】DCA1000不使用mmWave Studio的数据采集方法,以及自动化实时数据采集

【TI毫米波雷达】DCA1000不使用mmWave Studio的数据采集方法&#xff0c;以及自动化实时数据采集 mmWave Studio提供的功能完全够用了 不用去纠结用DCA1000低延迟、无GUI传数据 速度最快又保证算力无非就是就是Linux板自己写驱动做串口和UDP 做雷达产品应用也不会采用DCA1000的…

MYSql------视图

什么是视图 定义&#xff1a;视图是一种虚拟的表&#xff0c;它是基于 SQL 查询语句的结果集而建立的。视图并不存储实际的数据&#xff0c;而是根据查询语句从一个或多个实际的表中提取数据&#xff0c;类似于存储在数据库中的预定义查询。作用&#xff1a; 简化复杂查询&…

基于Matlab的变压器仿真模型建模方法(13):单相升压自耦变压器的等效电路和仿真模型

1.单相升压自耦变压器的基本方程和等效电路 单相升压自耦变压器的接线原理图如图1所示。在建立自耦变压器的基本方程时,仍然把它看成是从双绕组变压器演变而来。在图1中,设节点a到节点b部分的绕组的匝数为,对应于双绕组变压器的原边绕组;节点c到节点a部分的绕组的绕组匝数为…

电脑之故障检测(Computer Fault Detection)

电脑之故障检测 在日常使用电脑的过程中&#xff0c;我们难免会遇到各种各样的故障。从简单的软件冲突到复杂的硬件损坏&#xff0c;这些问题往往让人头疼不已。然而&#xff0c;掌握一些基本的电脑故障检测方法&#xff0c;可以帮助我们快速定位问题所在&#xff0c;并采取相…

Jmeter-压测时接口如何按照顺序执行

Jmeter-压测时接口如何按照顺序执行-临界部分控制器 在进行压力测试时&#xff0c;需要按照顺序进行压测&#xff0c;比如按照接口1、接口2、接口3、接口4 进行执行 查询结果是很混乱的&#xff0c;如果请求次数少&#xff0c;可能会按照顺序执行&#xff0c;但是随着次数增加…

unity学习15:预制体prefab

目录 1 创建多个gameobject 2 创建prefab 2.1 创建prefab &#xff08;类&#xff09; 2.2 prefab 是一个文件 2.3 prefab可以导出 3 创建prefab variant &#xff08;子类&#xff09; 3.1 除了创建多个独立的prefab&#xff0c; 还可以创建 prefab variant 3.2 他…

(七)Linux库的串口开发

文章目录 基于官方提供的串口测试代码部分解析代码部分1. usage 函数2. opt_parsing_err_handle 函数3. sig_handle 函数4. init_serial 函数5. serial_write 函数6. serial_read 函数7. run_read_mode 函数8. run_write_mode 函数9. run_loopback_test 函数 进行测试第一步编译…

【Uniapp-Vue3】创建自定义页面模板

大多数情况下我们都使用的是默认模板&#xff0c;但是默认模板是Vue2格式的&#xff0c;如果我们想要定义一个Vue3模板的页面就需要自定义。 一、我们先复制下面的模板代码&#xff08;可根据自身需要进行修改&#xff09;&#xff1a; <template><view class"…

【机器视觉】OpenCV 图像轮廓(查找/绘制轮廓、轮廓面积/周长、多边形逼近与凸包、外接矩形)

文章目录 7. 图像轮廓7.1 什么是图像轮廓7.2 查找轮廓7.3 绘制轮廓7.4 轮廓的面积和周长7.5 多边形逼近与凸包7.6 外接矩形 OpenCV官网 7. 图像轮廓 7.1 什么是图像轮廓 图像轮廓是具有相同颜色或灰度的连续点的曲线. 轮廓在形状分析和物体的检测和识别中很有用。 轮廓的作用…

20250109使用M6000显卡在Ubuntu20.04.6下跑whisper来识别中英文字幕

20250109使用M6000显卡在Ubuntu20.04.6下跑whisper来识别中英文字幕 2025/1/9 20:57 https://blog.csdn.net/wb4916/article/details/144541848 20241217使用M6000显卡在WIN10下跑whisper来识别中英文字幕 步骤&#xff1a; 1、在NVIDIA的官网下载并安装M6000显卡在WIN10下的最…

EtherCAT转CANopen数字油田的高效解决方案

在数字化时代&#xff0c;油田的管理和运作正经历着深刻的变革。传统的油田监测系统受限于通信技术&#xff0c;往往难以满足实时、高效的数据传输需求。面对这一挑战&#xff0c;开疆智能EtherCAT转CANopen网关应运而生&#xff0c;它以其卓越的性能和高度的兼容性&#xff0c…

C语言——文件IO 【文件IO和标准IO区别,操作文件IO】open,write,read,dup2,access,stat

1.思维导图 2.练习 1&#xff1a;使用C语言编写一个简易的界面&#xff0c;界面如下 1&#xff1a;标准输出流 2&#xff1a;标准错误流 3&#xff1a;文件流 要求&#xff1a;按1的时候&#xff0c;通过printf输出数据&#xff0c;按2的时候&#xff0c;通过p…

Android - NDK :JNI实现异步回调

在android代码中&#xff0c;通过JNI调用c层子线程执行耗时任务&#xff0c;在c层子线程中把结果回调到android层&#xff0c; C语言小白&#xff0c;请批评指正&#xff01; android层代码&#xff1a; import androidx.appcompat.app.AppCompatActivity;import android.os.…

Java Web开发进阶——RESTful API设计与开发

随着分布式系统和微服务架构的流行&#xff0c;RESTful API已成为现代Web应用中后端与前端、第三方系统交互的重要方式。本节将深入探讨RESTful API的设计原则、实现方式以及如何使用Spring Boot开发高效、可靠的RESTful服务。 1. 理解RESTful API的设计原则 1.1 什么是RESTfu…

PWR-STM32电源控制

一、原理 睡眠模式不响应其他操作&#xff0c;比如烧写程序&#xff0c;烧写时按住复位键松手即可下载&#xff0c;在禁用JTAG也可如此烧写程序。 对于低功耗模式可以通过RTC唤醒、外部中断唤醒、中断唤醒。 1、电源框图&#xff1a; VDDA主要负责模拟部分的供电、Vref和Vref-…

深兰科技董事长陈海波应邀为华东师大心理学专业师生做AI专题讲座

12月28日&#xff0c;应上海华东师范大学的邀请&#xff0c;上海市科协常委、上海交通大学博士生导师、深兰科技创始人兼董事长陈海波专程到校&#xff0c;为该校心理学专业的全体师生做了一场关于人工智能推动个人数字化未来的专题讲座。 他在演讲中&#xff0c;首先详细讲述了…

ssh2-sftp-client uploadDir Upload error: getLocalStatus: Bad path: ./public

报错解释 这个错误表明在使用 ssh2-sftp-client 这个Node.js库进行目录上传时遇到了问题。具体来说&#xff0c;是指定的本地路径&#xff08;./public&#xff09;不正确或者不存在。 解决方法&#xff1a; 确认当前工作目录&#xff1a;确保你在执行上传操作时的当前工作目…

Vue指令的综合案例

Vue指令的综合案例 参考文献&#xff1a; Vue的快速上手 Vue指令上 Vue指令下 文章目录 Vue指令的综合案例记事本 列表渲染删除功能添加功能底部统计和清空总代码 结语 博客主页: He guolin-CSDN博客 关注我一起学习&#xff0c;一起进步&#xff0c;一起探索编程的无限可能…

2025新春烟花代码(二)HTML5实现孔明灯和烟花效果

效果展示 源代码 <!DOCTYPE html> <html lang"en"> <script>var _hmt _hmt || [];(function () {var hm document.createElement("script");hm.src "https://hm.baidu.com/hm.js?45f95f1bfde85c7777c3d1157e8c2d34";var …

# 网络编程 - 轻松入门不含糊

网络编程 - 轻松入门 介绍 网络编程指的是&#xff0c;在网络通信协议下。实现 不同计算机之间 的数据传输&#xff0c;例如 通信、聊天、视频通话 等。 1. 网络编程概述 学习网络编程过程 中我们可以将网络编程理解为 计算机之间的数据交互 但 前提是通…