Redis 雪崩是指大量缓存 key 在同一时间失效,导致所有请求直接打到数据库,造成数据库压力骤增甚至宕机的现象。
一、环境准备
1、依赖(Maven)
<dependencies>
<!-- Redis客户端 -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>4.4.3</version>
</dependency>
<!-- 数据库连接 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<!-- 线程池 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.12.0</version>
</dependency>
</dependencies>2、Redis/Jedis 配置
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
public class RedisConfig {
private static final JedisPool jedisPool;
static {
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(200);
config.setMaxIdle(50);
config.setMinIdle(10);
// 初始化连接池
jedisPool = new JedisPool(config, "localhost", 6379, 10000);
}
public static Jedis getJedis() {
return jedisPool.getResource();
}
public static void close(Jedis jedis) {
if (jedis != null) {
jedis.close();
}
}
}二、雪崩场景重现
1、核心逻辑(大量 key 同时失效)
模拟 1000 个商品缓存 key,全部设置 10 秒后过期,然后用多线程模拟高并发请求,观察数据库压力。
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class RedisAvalancheDemo {
// 数据库配置
private static final String DB_URL = "jdbc:mysql://localhost:3306/test?useSSL=false";
private static final String DB_USER = "root";
private static final String DB_PWD = "123456";
// 1. 初始化缓存:1000个key全部设置10秒过期(模拟雪崩前提)
public static void initCache() {
Jedis jedis = RedisConfig.getJedis();
try {
for (int i = 1; i <= 1000; i++) {
String key = "product:" + i;
// 所有key都设置10秒过期,模拟集中失效
jedis.setex(key, 10, getProductFromDB(i));
}
System.out.println("缓存初始化完成,1000个key将在10秒后同时失效");
} finally {
RedisConfig.close(jedis);
}
}
// 2. 模拟高并发请求
public static void simulateHighConcurrency() throws InterruptedException {
// 线程池:模拟100个并发请求
ExecutorService executor = Executors.newFixedThreadPool(100);
CountDownLatch latch = new CountDownLatch(10000); // 总请求数10000
// 延迟10秒(等缓存失效)后发起请求
Thread.sleep(10000);
System.out.println("缓存已失效,开始发起高并发请求...");
long start = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
int productId = (int) (Math.random() * 1000) + 1;
executor.submit(() -> {
try {
getProduct(productId); // 每次请求都尝试从缓存/数据库获取数据
} finally {
latch.countDown();
}
});
}
latch.await();
long end = System.currentTimeMillis();
System.out.println("所有请求完成,耗时:" + (end - start) + "ms");
executor.shutdown();
}
// 3. 核心业务方法:先查缓存,缓存失效查数据库
public static String getProduct(int productId) {
Jedis jedis = RedisConfig.getJedis();
String key = "product:" + i;
try {
// 1. 查缓存
String value = jedis.get(key);
if (value != null) {
return value;
}
// 2. 缓存失效,查数据库(雪崩时所有请求都会走到这一步)
String dbValue = getProductFromDB(productId);
// 3. 重新写入缓存(但此时并发请求会重复写入,加剧数据库压力)
jedis.setex(key, 10, dbValue);
return dbValue;
} finally {
RedisConfig.close(jedis);
}
}
// 模拟从数据库查询(模拟数据库耗时)
private static String getProductFromDB(int productId) {
try (Connection conn = DriverManager.getConnection(DB_URL, DB_USER, DB_PWD);
PreparedStatement ps = conn.prepareStatement("SELECT name FROM product WHERE id = ?")) {
// 模拟数据库查询耗时(50ms)
Thread.sleep(50);
ps.setInt(1, productId);
ResultSet rs = ps.executeQuery();
if (rs.next()) {
return rs.getString("name");
}
return "product_" + productId;
} catch (Exception e) {
throw new RuntimeException("数据库查询失败", e);
}
}
public static void main(String[] args) throws InterruptedException {
// 步骤1:初始化缓存(1000个key同时过期)
initCache();
// 步骤2:模拟高并发请求
simulateHighConcurrency();
}
}2、雪崩现象表现
缓存失效后,10000 个请求全部打到数据库;
每个数据库查询耗时 50ms,100 并发下数据库连接池会被打满;
最终总请求耗时会超过 50*10000/100 = 5000ms,且数据库 CPU/IO 飙升。
三、雪崩解决方案
针对雪崩的核心解决方案是避免大量 key 同时失效 + 保护数据库,下面是 4 种核心方案的实现:
方案 1:过期时间随机化(核心)
给每个 key 的过期时间增加随机偏移量,避免集中失效:
// 修改initCache和getProduct中的过期时间设置逻辑
public static void initCache() {
Jedis jedis = RedisConfig.getJedis();
try {
for (int i = 1; i <= 1000; i++) {
String key = "product:" + i;
// 基础过期时间10秒 + 随机0-5秒偏移
int expireTime = 10 + (int) (Math.random() * 5);
jedis.setex(key, expireTime, getProductFromDB(i));
}
} finally {
RedisConfig.close(jedis);
}
}方案 2:互斥锁(防止缓存击穿,间接缓解雪崩)
当缓存失效时,只有一个线程去查数据库,其他线程等待:
public static String getProductWithLock(int productId) {
Jedis jedis = RedisConfig.getJedis();
String key = "product:" + productId;
String lockKey = "lock:product:" + productId;
try {
// 1. 查缓存
String value = jedis.get(key);
if (value != null) {
return value;
}
// 2. 获取分布式锁(SET NX EX,过期时间3秒)
String lockResult = jedis.set(lockKey, "1", "NX", "EX", 3);
if ("OK".equals(lockResult)) {
// 3. 拿到锁,查数据库
String dbValue = getProductFromDB(productId);
// 4. 写入缓存(带随机过期时间)
int expireTime = 10 + (int) (Math.random() * 5);
jedis.setex(key, expireTime, dbValue);
// 释放锁
jedis.del(lockKey);
return dbValue;
} else {
// 5. 没拿到锁,休眠100ms后重试
Thread.sleep(100);
return getProductWithLock(productId); // 递归重试
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
RedisConfig.close(jedis);
}
}方案 3:缓存预热 + 永不过期 key
核心数据设置永不过期,通过后台线程定期更新缓存,避免失效:
// 1. 初始化永不过期缓存
public static void initPermanentCache() {
Jedis jedis = RedisConfig.getJedis();
try {
for (int i = 1; i <= 1000; i++) {
String key = "product:" + i;
jedis.set(key, getProductFromDB(i)); // 不设置过期时间
}
} finally {
RedisConfig.close(jedis);
}
}
// 2. 后台线程定期更新缓存(每9秒更新一次)
public static void startCacheRefreshTask() {
Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate(() -> {
Jedis jedis = RedisConfig.getJedis();
try {
for (int i = 1; i <= 1000; i++) {
String key = "product:" + i;
jedis.set(key, getProductFromDB(i)); // 覆盖更新
}
System.out.println("缓存更新完成");
} finally {
RedisConfig.close(jedis);
}
}, 0, 9, TimeUnit.SECONDS);
}方案 4:限流 + 降级(保护数据库)
通过信号量限制并发请求数,超出部分直接降级返回默认值:
import java.util.concurrent.Semaphore;
public class ProductService {
// 限制数据库并发数为20
private static final Semaphore DB_SEMAPHORE = new Semaphore(20);
public static String getProductWithLimit(int productId) {
Jedis jedis = RedisConfig.getJedis();
String key = "product:" + productId;
try {
// 1. 查缓存
String value = jedis.get(key);
if (value != null) {
return value;
}
// 2. 尝试获取信号量,失败则降级
if (!DB_SEMAPHORE.tryAcquire()) {
return "default_product"; // 降级返回默认值
}
try {
// 3. 拿到信号量,查数据库
String dbValue = getProductFromDB(productId);
jedis.setex(key, 10 + (int) (Math.random() * 5), dbValue);
return dbValue;
} finally {
DB_SEMAPHORE.release(); // 释放信号量
}
} finally {
RedisConfig.close(jedis);
}
}
}四、完整解决方案整合
public class RedisAvalancheSolution {
private static final JedisPool jedisPool;
private static final Semaphore DB_SEMAPHORE = new Semaphore(20);
private static final String DB_URL = "jdbc:mysql://localhost:3306/test?useSSL=false";
private static final String DB_USER = "root";
private static final String DB_PWD = "123456";
static {
// 初始化Redis连接池
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(200);
config.setMaxIdle(50);
config.setMinIdle(10);
jedisPool = new JedisPool(config, "localhost", 6379, 10000);
// 启动缓存预热和定期更新任务
initPermanentCache();
startCacheRefreshTask();
}
// 缓存预热
private static void initPermanentCache() {
try (Jedis jedis = jedisPool.getResource()) {
for (int i = 1; i <= 1000; i++) {
String key = "product:" + i;
jedis.set(key, getProductFromDB(i));
}
}
}
// 后台更新缓存
private static void startCacheRefreshTask() {
Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate(() -> {
try (Jedis jedis = jedisPool.getResource()) {
for (int i = 1; i <= 1000; i++) {
String key = "product:" + i;
jedis.set(key, getProductFromDB(i));
}
}
}, 0, 9, TimeUnit.SECONDS);
}
// 核心方法:整合锁 + 限流 + 随机过期
public static String getProduct(int productId) {
String key = "product:" + productId;
String lockKey = "lock:product:" + productId;
try (Jedis jedis = jedisPool.getResource()) {
// 1. 查缓存
String value = jedis.get(key);
if (value != null) {
return value;
}
// 2. 尝试获取分布式锁
String lockResult = jedis.set(lockKey, "1", "NX", "EX", 3);
if ("OK".equals(lockResult)) {
try {
// 3. 限流:获取数据库访问许可
if (!DB_SEMAPHORE.tryAcquire()) {
return "default_product";
}
// 4. 查数据库
String dbValue = getProductFromDB(productId);
// 5. 写入缓存(随机过期时间)
int expireTime = 10 + (int) (Math.random() * 5);
jedis.setex(key, expireTime, dbValue);
return dbValue;
} finally {
DB_SEMAPHORE.release();
jedis.del(lockKey); // 释放锁
}
} else {
// 6. 没拿到锁,重试
Thread.sleep(100);
return getProduct(productId);
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
// 数据库查询
private static String getProductFromDB(int productId) {
try (Connection conn = DriverManager.getConnection(DB_URL, DB_USER, DB_PWD);
PreparedStatement ps = conn.prepareStatement("SELECT name FROM product WHERE id = ?")) {
Thread.sleep(50); // 模拟耗时
ps.setInt(1, productId);
ResultSet rs = ps.executeQuery();
return rs.next() ? rs.getString("name") : "product_" + productId;
} catch (Exception e) {
throw new RuntimeException("数据库查询失败", e);
}
}
// 测试
public static void main(String[] args) throws InterruptedException {
ExecutorService executor = Executors.newFixedThreadPool(100);
CountDownLatch latch = new CountDownLatch(10000);
long start = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
int productId = (int) (Math.random() * 1000) + 1;
executor.submit(() -> {
try {
getProduct(productId);
} finally {
latch.countDown();
}
});
}
latch.await();
long end = System.currentTimeMillis();
System.out.println("优化后总耗时:" + (end - start) + "ms");
executor.shutdown();
}
}总结
雪崩核心原因:大量缓存 key 集中失效 + 高并发请求直接穿透到数据库;
核心解决方案:
过期时间随机化:避免 key 集中失效(最基础有效);
分布式锁:防止缓存击穿,减少数据库重复查询;
限流降级:保护数据库,避免被打垮;
缓存预热 + 后台更新:核心数据永不过期,定期主动更新。
效果:优化后请求耗时会从秒级降至百毫秒级,数据库压力大幅降低。