Redis 缓存雪崩

一、Bug 场景

在一个电商系统中,Redis 被广泛用于缓存商品信息、用户信息等各种数据。为了管理缓存,系统设置了不同数据的缓存过期时间。在某个特定时间点,大量缓存同时过期,导致大量请求直接涌向数据库,数据库不堪重负,最终可能导致整个系统崩溃,这就是典型的缓存雪崩场景。例如,在一次促销活动后,大量商品的缓存同时到期,大量用户在活动结束后查询商品信息,从而引发问题。

二、代码示例

商品服务(有缺陷)

java 复制代码
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

@Service
public class ProductService {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    // 假设这是数据库查询方法
    private Object queryProductFromDB(String productId) {
        // 模拟数据库查询逻辑,返回商品信息
        return "Product Data";
    }

    public Object getProduct(String productId) {
        Object product = redisTemplate.opsForValue().get(productId);
        if (product == null) {
            product = queryProductFromDB(productId);
            if (product != null) {
                // 设置缓存,假设过期时间为1小时,且大量商品都设置相同过期时间
                redisTemplate.opsForValue().set(productId, product, 1, TimeUnit.HOURS);
            }
        }
        return product;
    }
}

三、问题描述

  1. 预期行为:缓存过期时,请求能均匀地分散到数据库,数据库可以承受这些请求,系统稳定运行。
  2. 实际行为:大量缓存同时过期,大量请求瞬间穿透缓存到达数据库,数据库无法承受如此高的并发压力,可能出现响应缓慢、服务中断甚至崩溃的情况。这是因为在设计缓存过期时间时,没有充分考虑到大量缓存同时过期带来的风险,导致在同一时刻大量请求失去缓存的保护直接访问数据库。

四、解决方案

  1. 随机过期时间:为每个缓存设置一个随机的过期时间,避免大量缓存集中过期。
java 复制代码
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.ThreadLocalRandom;

@Service
public class ProductService {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    // 假设这是数据库查询方法
    private Object queryProductFromDB(String productId) {
        // 模拟数据库查询逻辑,返回商品信息
        return "Product Data";
    }

    public Object getProduct(String productId) {
        Object product = redisTemplate.opsForValue().get(productId);
        if (product == null) {
            product = queryProductFromDB(productId);
            if (product != null) {
                // 设置随机过期时间,范围在1小时到1.5小时之间
                int randomExpiration = ThreadLocalRandom.current().nextInt(3600, 5400);
                redisTemplate.opsForValue().set(productId, product, randomExpiration, TimeUnit.SECONDS);
            }
        }
        return product;
    }
}
  1. 加锁排队:当缓存失效时,通过加锁机制确保只有部分请求去查询数据库并更新缓存,其他请求等待,这样可以防止大量请求同时冲击数据库。
java 复制代码
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

@Service
public class ProductService {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    // 假设这是数据库查询方法
    private Object queryProductFromDB(String productId) {
        // 模拟数据库查询逻辑,返回商品信息
        return "Product Data";
    }

    public Object getProduct(String productId) {
        Object product = redisTemplate.opsForValue().get(productId);
        if (product == null) {
            String lockKey = "product:lock:" + productId;
            Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, "locked", 1, TimeUnit.MINUTES);
            if (locked) {
                try {
                    product = queryProductFromDB(productId);
                    if (product != null) {
                        redisTemplate.opsForValue().set(productId, product, 1, TimeUnit.HOURS);
                    }
                } finally {
                    redisTemplate.delete(lockKey);
                }
            } else {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
                return getProduct(productId);
            }
        }
        return product;
    }
}
  1. 二级缓存:采用二级缓存架构,一级缓存使用 Redis,二级缓存可以使用本地缓存(如 Caffeine)。当一级缓存失效时,先从二级缓存获取数据,如果二级缓存也没有,则查询数据库,并将数据同时放入一级和二级缓存。
java 复制代码
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

@Service
public class ProductService {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    private static final Cache<String, Object> localCache = Caffeine.newBuilder()
          .expireAfterWrite(10, TimeUnit.MINUTES)
          .build();

    // 假设这是数据库查询方法
    private Object queryProductFromDB(String productId) {
        // 模拟数据库查询逻辑,返回商品信息
        return "Product Data";
    }

    public Object getProduct(String productId) {
        Object product = localCache.getIfPresent(productId);
        if (product == null) {
            product = redisTemplate.opsForValue().get(productId);
            if (product == null) {
                product = queryProductFromDB(productId);
                if (product != null) {
                    redisTemplate.opsForValue().set(productId, product, 1, TimeUnit.HOURS);
                    localCache.put(productId, product);
                }
            } else {
                localCache.put(productId, product);
            }
        }
        return product;
    }
}
相关推荐
程序员清风14 小时前
程序员兼职必看:靠谱软件外包平台挑选指南与避坑清单!
java·后端·面试
皮皮林55115 小时前
利用闲置 Mac 从零部署 OpenClaw 教程 !
java
华仔啊20 小时前
挖到了 1 个 Java 小特性:var,用完就回不去了
java·后端
SimonKing20 小时前
SpringBoot整合秘笈:让Mybatis用上Calcite,实现统一SQL查询
java·后端·程序员
日月云棠2 天前
各版本JDK对比:JDK 25 特性详解
java
用户8307196840822 天前
Spring Boot 项目中日期处理的最佳实践
java·spring boot
JavaGuide2 天前
Claude Opus 4.6 真的用不起了!我换成了国产 M2.5,实测真香!!
java·spring·ai·claude code
IT探险家2 天前
Java 基本数据类型:8 种原始类型 + 数组 + 6 个新手必踩的坑
java
花花无缺2 天前
搞懂new 关键字(构造函数)和 .builder() 模式(建造者模式)创建对象
java
用户908324602732 天前
Spring Boot + MyBatis-Plus 多租户实战:从数据隔离到权限控制的完整方案
java·后端