缓存击穿
- 问题分析:
- 在高并发场景下,当一个设置了过期时间的key在过期的瞬间,有大量的请求同时访问该key对应的业务逻辑,这些请求发现缓存中没有数据,就会同时去访问数据库,可能导致数据库瞬间承受巨大压力,甚至崩溃。例如电商平台的某一热门商品缓存过期时,大量用户同时访问该商品详情页,可能出现缓存击穿情况。
- 调整策略:
- 互斥锁:
- 当缓存失效时,先获取一把互斥锁(如使用Redis的SETNX命令)。只有获取到锁的请求才能去查询数据库并更新缓存,其他请求等待。获取锁的请求完成后释放锁,其他等待的请求再次尝试获取锁并检查缓存,此时缓存已更新,直接从缓存获取数据。
- 示例代码(以Python为例,使用redis - py库):
import redis
r = redis.Redis(host='localhost', port = 6379, db = 0)
def get_data(key):
data = r.get(key)
if data is None:
lock_key = 'lock:' + key
lock_acquired = r.setnx(lock_key, 'locked')
if lock_acquired:
try:
# 从数据库查询数据
db_data = get_from_db(key)
r.set(key, db_data)
return db_data
finally:
r.delete(lock_key)
else:
# 等待一段时间后重试
import time
time.sleep(0.1)
return get_data(key)
else:
return data.decode('utf - 8')
def get_from_db(key):
# 模拟从数据库获取数据
return 'data from db for'+ key
- 永不过期:
- 对于一些重要且不经常变化的数据,可以设置为永不过期。但需要额外的机制去更新数据,比如在数据发生变化时主动去更新缓存,同时可以设置一个逻辑过期时间,在业务代码中判断数据是否逻辑过期,如果过期则异步更新缓存。
- 示例代码(以Java为例,使用Jedis库):
import redis.clients.jedis.Jedis;
public class RedisUtils {
private static final Jedis jedis = new Jedis("localhost", 6379);
public static String getData(String key) {
String data = jedis.get(key);
if (data == null) {
// 这里逻辑过期时间通过额外字段记录
String expireTimeStr = jedis.get(key + ":expire");
if (expireTimeStr!= null && Long.parseLong(expireTimeStr) < System.currentTimeMillis()) {
// 异步更新缓存
new Thread(() -> {
String newData = getFromDb(key);
jedis.set(key, newData);
jedis.set(key + ":expire", String.valueOf(System.currentTimeMillis() + 3600 * 1000));// 假设1小时逻辑过期
}).start();
}
}
return data;
}
private static String getFromDb(String key) {
// 模拟从数据库获取数据
return "data from db for " + key;
}
}
缓存雪崩
- 问题分析:
- 缓存雪崩是指在某一时间段内,大量的缓存key同时过期,导致大量请求直接访问数据库,使数据库压力骤增,甚至可能导致数据库服务崩溃。例如电商大促活动结束后,大量商品的缓存同时过期,大量用户访问商品相关接口,就可能引发缓存雪崩。
- 调整策略:
- 设置随机过期时间:
- 避免所有缓存设置相同的过期时间,而是在一个合理的时间范围内设置随机的过期时间。比如原本商品缓存设置过期时间为1小时,可以设置为50 - 70分钟之间的随机值。这样可以分散缓存过期的时间点,降低大量缓存同时过期的风险。
- 示例代码(以Node.js为例,使用ioredis库):
const Redis = require('ioredis');
const redis = new Redis();
async function setDataWithRandomExpiry(key, value) {
const minExpiry = 50 * 60; // 50分钟
const maxExpiry = 70 * 60; // 70分钟
const randomExpiry = Math.floor(Math.random() * (maxExpiry - minExpiry + 1)) + minExpiry;
await redis.setex(key, randomExpiry, value);
}
- 二级缓存:
- 可以设置两级缓存,一级缓存失效时,先从二级缓存获取数据。二级缓存可以设置较长的过期时间或者不设置过期时间。当一级缓存失效后,通过异步任务去更新一级缓存,而用户先从二级缓存获取数据,避免直接访问数据库。
- 示例代码(以Go语言为例,使用go - redis库):
package main
import (
"fmt"
"github.com/go - redis/redis/v8"
"time"
)
var rdb1 *redis.Client
var rdb2 *redis.Client
func init() {
rdb1 = redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "",
DB: 0,
})
rdb2 = redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "",
DB: 1,
})
}
func getData(key string) (string, error) {
data, err := rdb1.Get(rdb1.Context(), key).Result()
if err == redis.Nil {
data, err = rdb2.Get(rdb2.Context(), key).Result()
if err == nil {
// 异步更新一级缓存
go func() {
newData, err := getFromDb(key)
if err == nil {
rdb1.Set(rdb1.Context(), key, newData, time.Hour)
}
}()
}
}
return data, err
}
func getFromDb(key string) (string, error) {
// 模拟从数据库获取数据
return "data from db for " + key, nil
}
- 缓存预热:
- 在系统上线前或者高并发场景来临前,提前将部分数据加载到缓存中,并设置合理的过期时间。例如电商大促前,提前将热门商品数据缓存起来,避免在活动开始时大量缓存同时过期。可以通过定时任务或者手动调用的方式进行缓存预热。
- 示例代码(以Python为例,使用redis - py库):
import redis
r = redis.Redis(host='localhost', port = 6379, db = 0)
def warm_up_cache():
keys = ['product:1', 'product:2', 'product:3']
for key in keys:
data = get_from_db(key)
r.set(key, data)
def get_from_db(key):
# 模拟从数据库获取数据
return 'data from db for'+ key