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;
    }
}
相关推荐
云烟成雨TD4 分钟前
Spring AI Alibaba 1.x 系列【1】阿里巴巴 AI 生态
java·人工智能·spring
诗人不写诗7 分钟前
spring是如何组织切面的
java·后端·spring
難釋懷9 分钟前
Redis分片集群手动故障转移
数据库·redis·缓存
大傻^1 小时前
Spring AI Alibaba Agent开发:基于ChatClient的智能体构建模式
java·数据库·人工智能·后端·spring·springaialibaba
li星野1 小时前
C++面试真题分享20260320
java·c++·面试
Irissgwe1 小时前
c++特殊类设计
java·开发语言·c++
大傻^1 小时前
Spring AI Alibaba ChatClient实战:流式输出与多轮对话管理
java·人工智能·后端·spring·springai·springaialibaba
小帅学编程1 小时前
英语学习笔记
java·笔记·学习
学编程就要猛1 小时前
JavaEE初阶:文件操作和IO
java·java-ee
ba_pi1 小时前
每天写点什么2026-03-19-Doris三种存储模型
java·数据库·mysql