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;
    }
}
相关推荐
YOU OU22 分钟前
Redis基础常识与命令
数据库·redis·缓存
腻害兔44 分钟前
【若依项目-产品经理视角】深度拆解 RuoYi-Vue-Pro 商城模块:从商品管理到交易引擎,50 张表撑起一整套电商系统
java·大数据·vue.js·产品经理·ai编程
码智社2 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
萧瑟余晖3 小时前
JDK 26 新特性详解
java·开发语言
马优晨3 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
吳所畏惧5 小时前
宝塔面板Redis密码修改指南:SSH命令修改 vs 面板UI界面修改,哪个更靠谱?
运维·服务器·数据库·redis·缓存·ssh
赵丙双5 小时前
Redis 概率相关的数据类型
redis
维天说6 小时前
CLI-Switch 2026年3月版历史设计:Hook、TTY 隔离与 JSON 状态
java·服务器·json
Zane19946 小时前
并发 vs 并行:别再傻傻分不清了,一文讲透 Java 并发编程的第一课
java·后端
噢,我明白了7 小时前
java中Excel的导入和导出(EasyExcel)
java·开发语言·excel