Redis缓存策略是高并发架构中提升响应速度的核心手段,但缓存引入后也带来了新的问题:缓存穿透、缓存击穿、缓存雪崩。这三种异常场景处理不当会导致大量请求直达数据库,引发性能雪崩。本文从问题诊断到解决方案,完整演示Redis缓存防护体系的搭建过程。
三种缓存异常场景区分
| 场景 | 触发条件 | 危害 |
|---|---|---|
| 缓存穿透 | 查询不存在的数据,缓存和DB都没有 | 每次请求都打到DB |
| 缓存击穿 | 热点Key过期瞬间,大量并发请求 | 瞬间DB压力激增 |
| 缓存雪崩 | 大量Key同时过期或Redis宕机 | DB被压垮,服务不可用 |
缓存穿透:布隆过滤器方案
缓存穿透的典型场景是恶意攻击——用大量不存在的ID发起查询,绕过缓存直接打数据库。MySQL性能调优无法解决这类问题,必须在缓存层拦截。
布隆过滤器是一种空间效率极高的概率型数据结构,能判断一个元素”一定不存在”或”可能存在”。误判率可控,不会漏判(不存在的不会误报存在)。
package cache
import (
"context"
"errors"
"time"
"github.com/redis/go-redis/v9"
"github.com/bits-and-blooms/bloom/v3"
)
// BloomFilter 基于Redis的分布式布隆过滤器
type BloomFilter struct {
rdb *redis.Client
key string
filter *bloom.BloomFilter // 本地缓存副本
}
// NewBloomFilter 创建布隆过滤器
// expectedItems: 预期元素数量
// falsePositiveRate: 期望误判率,如0.01表示1%
func NewBloomFilter(rdb *redis.Client, key string, expectedItems uint, falsePositiveRate float64) *BloomFilter {
filter := bloom.NewWithEstimates(expectedItems, falsePositiveRate)
return &BloomFilter{
rdb: rdb,
key: key,
filter: filter,
}
}
// Add 向布隆过滤器添加元素
func (bf *BloomFilter) Add(ctx context.Context, item string) error {
// 写入Redis(使用SETBIT操作)
// 同时写入本地缓存
bf.filter.Add([]byte(item))
// 实际项目中用Redis的SETBIT实现分布式布隆过滤器
// 这里简化演示
return nil
}
// Exists 判断元素是否存在
func (bf *BloomFilter) Exists(ctx context.Context, item string) (bool, error) {
// 本地判断
exists := bf.filter.Test([]byte(item))
return exists, nil
}
// CacheService 带防护的缓存服务
type CacheService struct {
rdb *redis.Client
bloom *BloomFilter
}
func NewCacheService(rdb *redis.Client) *CacheService {
bf := NewBloomFilter(rdb, "bloom:users", 1000000, 0.01)
return &CacheService{
rdb: rdb,
bloom: bf,
}
}
// GetUser 带穿透防护的查询
func (cs *CacheService) GetUser(ctx context.Context, userID string) (*User, error) {
// 第一层:布隆过滤器检查
exists, err := cs.bloom.Exists(ctx, userID)
if err != nil {
return nil, err
}
if !exists {
// 布隆过滤器说不存在,一定不存在
return nil, ErrUserNotFound
}
// 第二层:查Redis缓存
val, err := cs.rdb.Get(ctx, "user:"+userID).Result()
if err == nil {
// 缓存命中
return deserializeUser(val)
}
// 缓存未命中,查数据库
user, err := dbQueryUser(userID)
if err != nil {
if errors.Is(err, ErrUserNotFound) {
// 数据库也没有,缓存空值(短TTL)
cs.rdb.Set(ctx, "user:"+userID, "", 5*time.Minute)
return nil, ErrUserNotFound
}
return nil, err
}
// 回写缓存,加随机TTL防雪崩
ttl := 30*time.Minute + time.Duration(randInt(0, 300))*time.Second
cs.rdb.Set(ctx, "user:"+userID, serializeUser(user), ttl)
return user, nil
}
缓存击穿:分布式锁方案
热点Key过期瞬间,大量并发请求同时穿透到数据库。分布式锁方案让只有一个请求查DB并回填缓存,其他请求等待。
package cache
import (
"context"
"errors"
"time"
"github.com/redis/go-redis/v9"
"github.com/google/uuid"
)
type HotKeyCache struct {
rdb *redis.Client
}
// GetWithLock 使用分布式锁防止缓存击穿
func (hkc *HotKeyCache) GetWithLock(ctx context.Context, key string,
loader func() (string, error)) (string, error) {
// 先查缓存
val, err := hkc.rdb.Get(ctx, key).Result()
if err == nil && val != "" {
return val, nil
}
// 缓存未命中,尝试获取分布式锁
lockKey := "lock:" + key
lockValue := uuid.New().String()
// 尝试加锁,设置过期时间防止死锁
locked, err := hkc.rdb.SetNX(ctx, lockKey, lockValue, 10*time.Second).Result()
if err != nil {
return "", err
}
if locked {
// 获取锁成功,执行查询
defer hkc.releaseLock(ctx, lockKey, lockValue)
// double-check:防止前一个锁持有者已经写入缓存
val, err := hkc.rdb.Get(ctx, key).Result()
if err == nil && val != "" {
return val, nil
}
// 查数据库
result, err := loader()
if err != nil {
return "", err
}
// 回写缓存,加随机TTL
ttl := 30*time.Minute + time.Duration(randInt(0, 300))*time.Second
hkc.rdb.Set(ctx, key, result, ttl)
return result, nil
}
// 获取锁失败,短暂等待后重试读缓存
for i := 0; i < 3; i++ {
time.Sleep(50 * time.Millisecond)
val, err := hkc.rdb.Get(ctx, key).Result()
if err == nil && val != "" {
return val, nil
}
}
// 重试失败,降级返回默认值或报错
return "", ErrCacheMiss
}
// releaseLock 安全释放锁(Lua脚本保证原子性)
func (hkc *HotKeyCache) releaseLock(ctx context.Context, key, value string) {
script := `
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
`
hkc.rdb.Eval(ctx, script, []string{key}, value)
}
缓存雪崩:多级缓存与随机TTL
缓存雪崩的核心原因是大量Key同时过期。预防措施有两层:一是给TTL加随机抖动,避免同时失效;二是构建多级缓存,Redis挂掉时本地缓存兜底。
package cache
import (
"context"
"math/rand"
"time"
lru "github.com/hashicorp/golang-lru/v2"
)
// MultiLevelCache 多级缓存:本地缓存 → Redis → DB
type MultiLevelCache struct {
localCache *lru.Cache[string, string] // L1: 本地缓存
rdb *redis.Client // L2: Redis
localTTL time.Duration
redisTTL time.Duration
}
func NewMultiLevelCache(rdb *redis.Client, localSize int) *MultiLevelCache {
lc, _ := lru.New[string, string](localSize)
return &MultiLevelCache{
localCache: lc,
rdb: rdb,
localTTL: 5 * time.Minute,
redisTTL: 30 * time.Minute,
}
}
// Get 多级缓存查询
func (mlc *MultiLevelCache) Get(ctx context.Context, key string,
loader func() (string, error)) (string, error) {
// L1: 本地缓存
if val, ok := mlc.localCache.Get(key); ok {
return val, nil
}
// L2: Redis缓存
val, err := mlc.rdb.Get(ctx, key).Result()
if err == nil && val != "" {
// 回填L1
mlc.localCache.Add(key, val)
return val, nil
}
// L3: 数据库
result, err := loader()
if err != nil {
return "", err
}
// 回填L2和L1,TTL加随机抖动
redisTTL := mlc.redisTTL + time.Duration(rand.Intn(300))*time.Second
mlc.rdb.Set(ctx, key, result, redisTTL)
mlc.localCache.Add(key, result)
return result, nil
}
// randInt 辅助函数
func randInt(min, max int) int {
return min + rand.Intn(max-min)
}
Redis高可用架构配置
数据备份恢复方案中,Redis本身的可用性也需保障。Redis Cluster提供自动分片和故障转移,是生产环境的标准方案。
# redis.conf (哨兵模式配置示例)
port 6379
bind 0.0.0.0
protected-mode yes
# 开启AOF持久化
appendonly yes
appendfsync everysec
# 开启RDB快照
save 900 1
save 300 10
save 60 10000
# 内存淘汰策略
maxmemory 4gb
maxmemory-policy allkeys-lru
# 慢查询日志
slowlog-log-slower-than 10000
slowlog-max-len 128
缓存方案选择决策表
| 场景 | 推荐方案 | 注意事项 |
|---|---|---|
| 恶意查询不存在的数据 | 布隆过滤器 + 空值缓存 | 布隆过滤器需在数据写入时同步更新 |
| 热点Key过期 | 分布式锁 + 逻辑过期 | 锁超时时间要大于DB查询时间 |
| 大量Key同时过期 | 随机TTL + 多级缓存 | 本地缓存容量不宜过大,避免GC压力 |
| Redis整体宕机 | Redis Cluster + 降级策略 | 降级时返回默认值而非报错 |
| 缓存与DB不一致 | 延迟双删 + 消息队列 | 双删间隔需评估读耗时 |
NoSQL选型应用中,Redis因其纯内存操作的特性,QPS可达10万+。但缓存不是万能药——引入缓存的同时必须考虑一致性、可用性和防护策略。缓存防护体系的投入在流量高峰期会体现其价值,DBA和SRE需要定期演练缓存故障场景,确保降级链路可用。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/redis-huan-cun-chuan-tou-yu-xue-beng-fang-hu-bu-long-guo/