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;
    }
}
相关推荐
市场部需要一个软件开发岗位2 小时前
JAVA开发常见安全问题:纵向越权
java·数据库·安全
历程里程碑2 小时前
普通数组----合并区间
java·数据结构·python·算法·leetcode·职场和发展·tornado
程序员泠零澪回家种桔子3 小时前
Spring AI框架全方位详解
java·人工智能·后端·spring·ai·架构
CodeCaptain3 小时前
nacos-2.3.2-OEM与nacos3.1.x的差异分析
java·经验分享·nacos·springcloud
Anastasiozzzz4 小时前
Java Lambda 揭秘:从匿名内部类到底层原理的深度解析
java·开发语言
骇客野人4 小时前
通过脚本推送Docker镜像
java·docker·容器
铁蛋AI编程实战4 小时前
通义千问 3.5 Turbo GGUF 量化版本地部署教程:4G 显存即可运行,数据永不泄露
java·人工智能·python
晚霞的不甘4 小时前
CANN 编译器深度解析:UB、L1 与 Global Memory 的协同调度机制
java·后端·spring·架构·音视频
SunnyDays10114 小时前
使用 Java 冻结 Excel 行和列:完整指南
java·冻结excel行和列
摇滚侠4 小时前
在 SpringBoot 项目中,开发工具使用 IDEA,.idea 目录下的文件需要提交吗
java·spring boot·intellij-idea