Redis利用逻辑过期解决缓存击穿问题

 更新时间:2026年06月07日 09:08:38   作者:ShaneD771  
本文主要介绍了Redis利用逻辑过期解决缓存击穿问题,介绍了两种解决方案,互斥锁与逻辑过期,介绍了两种解决方案:互斥锁与逻辑过期,并重点阐述了逻辑过期的实现原理与代码示例,强调了在高并发场景下选择逻辑过期的重要性及二次检查的必要性

一、 什么是缓存击穿?

缓存击穿(Cache Breakdown) 是指一个热点 Key(比如某次秒杀活动的商品详情),在某个时间点过期了。恰好在这个时间点,有大量的并发请求访问这个 Key。这些请求发现缓存过期,瞬间全部打到数据库上,就像在防线上凿穿了一个洞,导致数据库压力激增甚至宕机。

核心特征:

  1. 高并发:访问量巨大。
  2. 热点 Key:大家都在查同一个数据。
  3. 瞬间失效:缓存 TTL 到期,数据物理消失。

二、 互斥锁&逻辑过期

面对缓存击穿,通常有两种解法:

1. 互斥锁(Mutex Lock)

  • 思路:谁发现缓存过期了,谁就去抢一把锁。抢到锁的人去查数据库写缓存,其他人排队等待。
  • 优点:数据强一致性(查到的绝对是新的)。
  • 缺点性能较差。所有人都得等那一个线程干完活,如果不巧那个线程挂了或慢了,后面就是灾难性的阻塞。

2. 逻辑过期(Logical Expiration)

  • 思路“永不过期”。不在 Redis 层面设置 TTL,而是把过期时间写在 Value 里面。发现“逻辑”过期后,先返回旧数据,然后异步开个线程去后台更新。
  • 优点高可用,性能极佳。用户永远不需要等待,拿了数据就走。
  • 缺点:数据存在短暂的不一致(在重建完成前,用户看到的是旧数据)。

三、 逻辑过期的实现原理

我们不使用 Redis 的 setex 来控制生死,而是引入一个包装类 RedisData,人为地记录一个 expireTime

1. 数据结构设计

我们需要一个容器来封装真实的业务数据和逻辑过期时间:

@Data
public class RedisData {
    private LocalDateTime expireTime; // 逻辑过期时间
    private Object data;              // 真实的业务数据(如 Shop 对象)
}

2. 执行流程图解

  1. 查询缓存:从 Redis 取出数据(逻辑过期前提是数据必须预热,如果 Redis 没数据,直接返回空或降级)。
  2. 判断逻辑时间
    • 如果 expireTime > now():数据新鲜,直接返回。
    • 如果 expireTime <= now()逻辑已过期
  3. 重建缓存
    • 抢锁:尝试获取互斥锁。
    • 抢锁失败:说明有人在更新了,不要等,直接返回旧数据。
    • 抢锁成功:再次检查缓存是否已更新(Double Check)。如果没更新,则开启独立线程查库写缓存;如果已更新,直接释放锁并返回新数据。
  4. 返回数据:无论是否抢到锁,主线程都直接返回数据(旧的或新的)。

四、 代码实现 (Java)

以下是基于 SpringBoot + StringRedisTemplate 的完整实现,包含了二次检查(Double Check)逻辑

1. 缓存预热

因为 Redis 里没有 TTL,数据不会自己消失。我们需要在活动开始前把数据“预热”进去。

/**
 * 预热数据到 Redis
 * @param id 商品ID
 * @param expireSeconds 逻辑过期时间(秒)
 */
public void saveShop2redis(Long id, Long expireSeconds) {
    // 1. 查询数据库
    Shop shop = getById(id);
    
    // 2. 封装成 RedisData
    RedisData redisData = new RedisData();
    redisData.setData(shop);
    // 重点:设置逻辑过期时间 = 当前时间 + 指定秒数 (注意单位是 PlusSeconds)
    redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
    
    // 3. 写入 Redis (不设置 TTL)
    stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
}

2. 业务逻辑 (queryWithLogicalExpire)

// 线程池:用于异步重建缓存
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

public Shop queryWithLogicalExpire(Long id) {
    String key = RedisConstants.CACHE_SHOP_KEY + id;
    
    // 1. 从 Redis 查询
    String shopJson = stringRedisTemplate.opsForValue().get(key);
    
    // 2. 如果未命中(未预热),直接返回 null
    if (StrUtil.isBlank(shopJson)) {
        return null;
    }

    // 3. 反序列化
    RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
    JSONObject data = (JSONObject) redisData.getData();
    Shop shop = JSONUtil.toBean(data, Shop.class);
    LocalDateTime expireTime = redisData.getExpireTime();

    // 4. 判断是否过期
    if (expireTime.isAfter(LocalDateTime.now())) {
        // 未过期,直接返回
        return shop;
    }

    // ==========================================================
    // 5. 已过期,需要缓存重建
    // ==========================================================
    
    String lockKey = RedisConstants.LOCK_SHOP_KEY + id;
    // 6. 尝试获取互斥锁
    boolean isLock = tryLock(lockKey);
    
    if (isLock) {
        // 6.1 获取锁成功
        
        // 【二次检查 (Double Check)】
        // 再次查询 Redis,防止在上一个线程释放锁的瞬间,缓存已经被更新了
        shopJson = stringRedisTemplate.opsForValue().get(key);
        if (StrUtil.isNotBlank(shopJson)) {
             RedisData newRedisData = JSONUtil.toBean(shopJson, RedisData.class);
             LocalDateTime newExpireTime = newRedisData.getExpireTime();
             // 如果发现已经被更新(不过期了)
             if (newExpireTime.isAfter(LocalDateTime.now())) {
                 // 释放锁,直接返回新数据,不再开启线程重建
                 unlock(lockKey);
                 return JSONUtil.toBean((JSONObject) newRedisData.getData(), Shop.class);
             }
        }
        
        // 6.2 确认依然过期,开启独立线程重建
        CACHE_REBUILD_EXECUTOR.submit(() -> {
            try {
                // 重建缓存(假设逻辑过期时间 20秒)
                this.saveShop2redis(id, 20L);
            } catch (Exception e) {
                e.printStackTrace(); // 建议使用 log.error
            } finally {
                // 释放锁
                unlock(lockKey);
            }
        });
    }

    // 7. 【核心】无论是否抢到锁,都直接返回旧数据,绝不等待!
    return shop;
}

// 辅助方法:获取锁
private boolean tryLock(String key) {
    Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
    return BooleanUtil.isTrue(flag);
}

// 辅助方法:释放锁
private void unlock(String key) {
    stringRedisTemplate.delete(key);
}

五、 总结

1. 为什么选择逻辑过期?

逻辑过期本质上是一种**“妥协的艺术”**。它牺牲了短暂的数据一致性(用户可能在几百毫秒内看到旧数据),换取了系统在极高并发下的稳定性(Redis 永不阻塞,数据库压力极小)。

2. 为什么要做二次检查 (Double Check)?

如果不加二次检查,在高并发下,线程 B 可能会在线程 A 重建完刚刚释放锁的时候抢到锁。此时线程 B 以为数据还过期,会再次开启线程去查库。虽然不影响正确性,但浪费了性能。加上二次检查可以避免不必要的重建。

到此这篇关于Redis利用逻辑过期解决缓存击穿问题的文章就介绍到这了,更多相关Redis 缓存击穿内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Redis中管道操作pipeline的实现

    Redis中管道操作pipeline的实现

    RedisPipeline是一种优化客户端与服务器通信的技术,通过批量发送和接收命令减少网络往返次数,提高命令执行效率,本文就来介绍一下Redis中管道操作pipeline的实现,具有一定的参考价值,感兴趣的可以了解一下
    2025-03-03
  • Quarkus集成redis操作Redisson实现数据互通

    Quarkus集成redis操作Redisson实现数据互通

    这篇文章主要为大家介绍了Quarkus集成redis操作Redisson实现数据互通的示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步
    2022-02-02
  • Redis Cluster部署实践

    Redis Cluster部署实践

    本文详细介绍了在一台机器上上上搭建一个3主3从的Redis集群的步骤,包括准备阶段、启动实例、组成集群、故障恢复等内容;并提供了一些常用命令和注意事项,帮助;以帮助读者成功搭建Redis集群
    2026-04-04
  • Redis实现锁续期的项目实践

    Redis实现锁续期的项目实践

    本文介绍了使用Redis实现分布式锁的续期,包括使用Lua脚本、Redlock算法和Redisson客户端等方法,具有一定的参考价值,感兴趣的可以了解一下
    2024-12-12
  • springmvc集成使用redis过程

    springmvc集成使用redis过程

    这篇文章主要介绍了springmvc集成使用redis过程,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-11-11
  • Windows环境下查看、添加、修改redis数据库的密码两种方式

    Windows环境下查看、添加、修改redis数据库的密码两种方式

    在Windows系统上设置Redis密码的过程与Linux系统类似,但需注意几个关键步骤以确保正确配置,这篇文章主要给大家介绍了关于Windows环境下查看、添加、修改redis数据库的密码两种方式,需要的朋友可以参考下
    2024-07-07
  • redis分片集群的部署和使用教程

    redis分片集群的部署和使用教程

    这篇文章介绍了如何使用Docker和脚本在虚拟机上部署Redis集群,并详细讲解了Spring Boot整合Redis集群的方法,包括配置、读写分离以及一些须知,感兴趣的朋友跟随小编一起看看吧
    2025-12-12
  • redis8.0新特性之布谷鸟过滤器(Cuckoo Filter)的使用

    redis8.0新特性之布谷鸟过滤器(Cuckoo Filter)的使用

    布谷鸟过滤器是一种概率数据结构,就像布隆过滤器一样,可以以非常快速且节省空间的方式检查元素是否存在于集合中,同时还支持删除操作,并在某些场景下表现优于布隆过滤器,感兴趣的可以了解一下
    2025-08-08
  • Redis数据持久化方式技术解析

    Redis数据持久化方式技术解析

    Redis(Remote Dictionary Server ),即远程字典服务,是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API
    2021-09-09
  • 查看Redis内存信息的命令

    查看Redis内存信息的命令

    Redis 是一个开源、高性能的Key-Value数据库,被广泛应用在服务器各种场景中。本文介绍几个查看Redis内存信息的命令,包括常用的info memory、info keyspace、bigkeys等。
    2020-09-09

最新评论