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

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

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

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

1. 永不过期

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

2. 逻辑过期

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

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

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

二、实现步骤

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

三、Java 实现示例

以下是一个基于 Java 和 Redis 的完整实现示例。我们将使用 Redisson 作为 Redis 客户端,它支持分布式锁和异步操作,适合实现"永不过期" + "逻辑过期" 策略。

1. 引入依赖

首先,在项目的 pom.xml 中添加 Redisson 依赖:

xml 复制代码
<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. 定义缓存数据结构

我们需要一个数据结构来存储实际数据和逻辑过期时间。以下是一个示例类:

java 复制代码
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:

java 复制代码
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. 缓存管理器实现

实现缓存读取、逻辑过期检查和异步刷新:

java 复制代码
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 数据模型,并希望缓存用户信息:

java 复制代码
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
    // ...
}

模拟数据库查询方法:

java 复制代码
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);
    }
}

主程序示例:

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

相关推荐
月光水岸New2 小时前
Ubuntu 中建的mysql数据库使用Navicat for MySQL连接不上
数据库·mysql·ubuntu
狄加山6752 小时前
数据库基础1
数据库
我爱松子鱼2 小时前
mysql之规则优化器RBO
数据库·mysql
chengooooooo3 小时前
苍穹外卖day8 地址上传 用户下单 订单支付
java·服务器·数据库
Rverdoser4 小时前
【SQL】多表查询案例
数据库·sql
Galeoto4 小时前
how to export a table in sqlite, and import into another
数据库·sqlite
希忘auto4 小时前
详解Redis在Centos上的安装
redis·centos
人间打气筒(Ada)4 小时前
MySQL主从架构
服务器·数据库·mysql
leegong231114 小时前
学习PostgreSQL专家认证
数据库·学习·postgresql
喝醉酒的小白4 小时前
PostgreSQL:更新字段慢
数据库·postgresql