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;
    }
}
相关推荐
李慕婉学姐4 小时前
【开题答辩过程】以《基于JAVA的校园即时配送系统的设计与实现》为例,不知道这个选题怎么做的,不知道这个选题怎么开题答辩的可以进来看看
java·开发语言·数据库
奋进的芋圆6 小时前
Java 延时任务实现方案详解(适用于 Spring Boot 3)
java·spring boot·redis·rabbitmq
sxlishaobin6 小时前
设计模式之桥接模式
java·设计模式·桥接模式
model20056 小时前
alibaba linux3 系统盘网站迁移数据盘
java·服务器·前端
荒诞硬汉7 小时前
JavaBean相关补充
java·开发语言
提笔忘字的帝国7 小时前
【教程】macOS 如何完全卸载 Java 开发环境
java·开发语言·macos
2501_941882487 小时前
从灰度发布到流量切分的互联网工程语法控制与多语言实现实践思路随笔分享
java·开发语言
華勳全栈7 小时前
两天开发完成智能体平台
java·spring·go
alonewolf_998 小时前
Spring MVC重点功能底层源码深度解析
java·spring·mvc
沛沛老爹8 小时前
Java泛型擦除:原理、实践与应对策略
java·开发语言·人工智能·企业开发·发展趋势·技术原理