缓存击穿 · 缓存雪崩 · 缓存穿透 — 深入理解问题本质与解决方案
缓存击穿的关键词是"热点"和"过期"。与缓存穿透不同,击穿针对的是确实存在的数据,只是恰好在某一时刻缓存失效了。
热点数据 hot_key 存在于缓存中,所有请求直接命中缓存返回,数据库无压力。
hot_key 的 TTL 到期,缓存中该数据被删除。
同一时刻,10000+ 个请求到达,全部 cache miss,同时查询数据库。
数据库瞬间承受 10000+ 并发查询,连接池耗尽,响应时间从 5ms 飙升到 5000ms+。
第一个请求完成查询并回写缓存,后续请求重新命中缓存,系统逐渐恢复。
商品详情缓存过期瞬间,数万用户同时请求,全部穿透到数据库。
突发新闻页面缓存失效,大量用户刷新页面导致数据库压力暴增。
知名用户的主页数据缓存过期,粉丝频繁访问导致数据库过载。
针对缓存击穿问题,核心思路是避免大量请求同时穿透到数据库。以下是两种主流解决方案:
| 方案 | 核心思路 | 数据一致性 | 可用性 | 适用场景 |
|---|---|---|---|---|
| 🔒 逻辑过期(永不过期) | 缓存不设物理 TTL,在 value 中存储逻辑过期时间,过期后异步重建 | 弱(可能返回旧数据) | 高(不阻塞) | 对一致性要求不高的热点数据 |
| 🔐 互斥锁排队 | 缓存 miss 时用分布式锁控制,只允许一个线程查 DB 重建缓存 | 强(等待最新数据) | 中(可能等待超时) | 对一致性要求较高的场景 |
package cache
import (
"context"
"encoding/json"
"fmt"
"sync"
"time"
"github.com/redis/go-redis/v9"
)
// CacheData 带逻辑过期的缓存数据结构
type CacheData struct {
Data json.RawMessage `json:"data"` // 实际业务数据
ExpireTime int64 `json:"expire_time"` // 逻辑过期时间(Unix时间戳)
}
// LogicalExpireCache 逻辑过期缓存实现
type LogicalExpireCache struct {
rdb *redis.Client
localMutex sync.Map // 本地互斥锁,防止多个goroutine同时重建
}
func NewLogicalExpireCache(rdb *redis.Client) *LogicalExpireCache {
return &LogicalExpireCache{rdb: rdb}
}
// Get 获取缓存数据(核心逻辑)
func (c *LogicalExpireCache) Get(
ctx context.Context,
key string,
logicalTTL time.Duration,
loadFn func(ctx context.Context) (interface{}, error),
) (interface{}, error) {
// Step 1: 从缓存中获取数据
val, err := c.rdb.Get(ctx, key).Result()
if err == redis.Nil {
// 缓存不存在(首次),需要加载并设置
return c.loadAndSet(ctx, key, logicalTTL, loadFn)
}
if err != nil {
return nil, fmt.Errorf("redis get error: %w", err)
}
// Step 2: 反序列化
var cacheData CacheData
if err := json.Unmarshal([]byte(val), &cacheData); err != nil {
return nil, fmt.Errorf("unmarshal error: %w", err)
}
// Step 3: 检查是否逻辑过期
if time.Now().Unix() < cacheData.ExpireTime {
// 未过期,直接返回缓存数据
var result interface{}
json.Unmarshal(cacheData.Data, &result)
return result, nil
}
// Step 4: 已过期,尝试获取互斥锁进行异步重建
lockKey := "lock:" + key
locked := c.tryLock(ctx, lockKey, 5*time.Second)
if locked {
// 获取锁成功,开启goroutine异步重建缓存
go func() {
defer c.unlock(ctx, lockKey)
c.loadAndSet(ctx, key, logicalTTL, loadFn)
}()
}
// 无论是否获取到锁,都返回旧数据(保证可用性)
var result interface{}
json.Unmarshal(cacheData.Data, &result)
return result, nil
}
// loadAndSet 从数据库加载数据并写入缓存
func (c *LogicalExpireCache) loadAndSet(
ctx context.Context,
key string,
logicalTTL time.Duration,
loadFn func(ctx context.Context) (interface{}, error),
) (interface{}, error) {
// 双重检查:再次从缓存获取(可能其他goroutine已重建)
val, _ := c.rdb.Get(ctx, key).Result()
if val != "" {
var cd CacheData
json.Unmarshal([]byte(val), &cd)
if time.Now().Unix() < cd.ExpireTime {
var result interface{}
json.Unmarshal(cd.Data, &result)
return result, nil
}
}
// 从数据库加载最新数据
data, err := loadFn(ctx)
if err != nil {
return nil, err
}
// 构造带逻辑过期的缓存值
dataBytes, _ := json.Marshal(data)
cacheData := CacheData{
Data: dataBytes,
ExpireTime: time.Now().Add(logicalTTL).Unix(),
}
cacheBytes, _ := json.Marshal(cacheData)
// 写入Redis(不设TTL,永不过期)
c.rdb.Set(ctx, key, cacheBytes, 0) // TTL=0 表示永不过期
return data, nil
}
// tryLock 尝试获取分布式锁
func (c *LogicalExpireCache) tryLock(ctx context.Context, key string, ttl time.Duration) bool {
return c.rdb.SetNX(ctx, key, "1", ttl).Val()
}
// unlock 释放分布式锁
func (c *LogicalExpireCache) unlock(ctx context.Context, key string) {
c.rdb.Del(ctx, key)
}
package cache
import (
"context"
"errors"
"fmt"
"time"
"github.com/redis/go-redis/v9"
)
var (
ErrCacheMiss = errors.New("cache miss")
ErrDBError = errors.New("database error")
)
// MutexCache 互斥锁缓存实现
type MutexCache struct {
rdb *redis.Client
retryCount int // 重试次数
retryDelay time.Duration // 重试间隔
lockTTL time.Duration // 锁的过期时间
}
func NewMutexCache(rdb *redis.Client) *MutexCache {
return &MutexCache{
rdb: rdb,
retryCount: 10,
retryDelay: 50 * time.Millisecond,
lockTTL: 10 * time.Second,
}
}
// Get 带互斥锁的缓存查询
func (c *MutexCache) Get(
ctx context.Context,
key string,
ttl time.Duration,
loadFn func(ctx context.Context) (interface{}, error),
) (interface{}, error) {
// Step 1: 尝试从缓存获取
val, err := c.rdb.Get(ctx, key).Result()
if err == nil {
// 缓存命中,直接返回
return val, nil
}
if err != redis.Nil {
return nil, fmt.Errorf("redis error: %w", err)
}
// Step 2: 缓存未命中,尝试获取互斥锁
lockKey := "lock:" + key
return c.getWithLock(ctx, key, lockKey, ttl, loadFn)
}
// getWithLock 带锁的缓存重建逻辑
func (c *MutexCache) getWithLock(
ctx context.Context,
key, lockKey string,
ttl time.Duration,
loadFn func(ctx context.Context) (interface{}, error),
) (interface{}, error) {
for i := 0; i < c.retryCount; i++ {
// 尝试获取分布式锁
locked, err := c.rdb.SetNX(ctx, lockKey, "1", c.lockTTL).Result()
if err != nil {
return nil, fmt.Errorf("set lock error: %w", err)
}
if locked {
// 获取锁成功!执行数据库查询和缓存重建
defer c.rdb.Del(ctx, lockKey) // 确保释放锁
// 双重检查:获取锁后再次检查缓存
// (可能其他线程已在我们等锁期间重建了缓存)
val, err := c.rdb.Get(ctx, key).Result()
if err == nil {
return val, nil
}
// 从数据库加载数据
data, err := loadFn(ctx)
if err != nil {
return nil, fmt.Errorf("load from db error: %w", err)
}
// 写入缓存
c.rdb.Set(ctx, key, data, ttl)
return data, nil
}
// 获取锁失败,等待后重试
time.Sleep(c.retryDelay)
// 重试时先检查缓存是否已重建
val, err := c.rdb.Get(ctx, key).Result()
if err == nil {
return val, nil
}
}
return nil, errors.New("timeout: failed to acquire lock after retries")
}
| 对比维度 | 逻辑过期(永不过期) | 互斥锁排队 |
|---|---|---|
| 数据一致性 | 弱一致性(可能返回旧数据) | 强一致性(等待最新数据) |
| 可用性 | 高(不阻塞,始终返回数据) | 中(可能等待超时) |
| 实现复杂度 | 中等(需维护逻辑过期字段) | 中等(需处理锁竞争) |
| 资源消耗 | 额外内存(过期时间字段) | 线程等待(CPU/内存) |
| 适用场景 | 对一致性要求不高的热点数据 | 对一致性要求较高的场景 |
缓存数据在初始化时设置了相同的 TTL(如都是 30 分钟),30 分钟后所有缓存同时失效。
Redis 节点故障、网络分区、内存溢出等原因导致整个缓存服务不可用。
针对缓存雪崩问题,需要从预防过期集中、限流降级保护、高可用架构三个层面进行防护:
| 方案 | 核心思路 | 防护层面 | 实现复杂度 | 适用场景 |
|---|---|---|---|---|
| 🎲 随机失效时间 | 在基础 TTL 上加随机值,分散 Key 的过期时间 | 预防层 | 极低 | 所有场景(必做) |
| 🔐 加锁排队 + 限流降级 | 通过互斥锁 + 信号量 + 限流器控制并发,保护数据库 | 保护层 | 中等 | 高并发场景 |
| 🏗️ Redis 高可用架构 | 使用哨兵模式或 Cluster 集群,防止 Redis 整体宕机 | 架构层 | 中高 | 生产环境(推荐) |
公式:实际TTL = 基础TTL + random(0, 随机范围)
例如基础 TTL 为 30 分钟,随机范围为 1~5 分钟,则实际过期时间在 31~35 分钟之间随机分布。
package cache
import (
"context"
"math/rand"
"time"
"github.com/redis/go-redis/v9"
)
// RandomTTLCache 随机TTL缓存实现
type RandomTTLCache struct {
rdb *redis.Client
baseTTL time.Duration // 基础过期时间
jitterMax time.Duration // 随机抖动最大值
}
func NewRandomTTLCache(rdb *redis.Client, baseTTL, jitterMax time.Duration) *RandomTTLCache {
return &RandomTTLCache{
rdb: rdb,
baseTTL: baseTTL,
jitterMax: jitterMax,
}
}
// Set 设置缓存(带随机TTL)
func (c *RandomTTLCache) Set(ctx context.Context, key string, value interface{}) error {
// 计算实际TTL = 基础TTL + 随机抖动
actualTTL := c.baseTTL + time.Duration(rand.Int63n(int64(c.jitterMax)))
return c.rdb.Set(ctx, key, value, actualTTL).Err()
}
// SetBatch 批量设置缓存(每个Key独立随机TTL)
func (c *RandomTTLCache) SetBatch(ctx context.Context, items map[string]interface{}) error {
pipe := c.rdb.Pipeline()
for key, value := range items {
// 每个Key使用独立的随机TTL
actualTTL := c.baseTTL + time.Duration(rand.Int63n(int64(c.jitterMax)))
pipe.Set(ctx, key, value, actualTTL)
}
_, err := pipe.Exec(ctx)
return err
}
// 使用示例
func Example() {
rdb := redis.NewClient(&redis.Options{Addr: "localhost:6379"})
// 基础TTL 30分钟,随机抖动 0~5分钟
cache := NewRandomTTLCache(rdb, 30*time.Minute, 5*time.Minute)
// 设置商品缓存
ctx := context.Background()
cache.Set(ctx, "product:1001", productData1) // 可能 32分钟后过期
cache.Set(ctx, "product:1002", productData2) // 可能 34分钟后过期
cache.Set(ctx, "product:1003", productData3) // 可能 31分钟后过期
// ... 每个Key的过期时间都不同,避免雪崩
}
❌ 固定 TTL (全部 30 分钟):
✅ 随机 TTL (30~35 分钟):
每个时间点只有少量 Key 过期,数据库压力被平滑分散
package cache
import (
"context"
"errors"
"math/rand"
"sync"
"time"
"github.com/redis/go-redis/v9"
"golang.org/x/time/rate"
)
// AvalancheProtection 雪崩综合防护
type AvalancheProtection struct {
rdb *redis.Client
limiter *rate.Limiter // 限流器
semaphore chan struct{} // 信号量,控制并发查DB的数量
mu sync.Mutex
baseTTL time.Duration
jitterMax time.Duration
}
func NewAvalancheProtection(rdb *redis.Client, maxDBConcurrency int) *AvalancheProtection {
return &AvalancheProtection{
rdb: rdb,
limiter: rate.NewLimiter(1000, 2000), // 每秒1000个请求,桶容量2000
semaphore: make(chan struct{}, maxDBConcurrency), // 最多N个goroutine同时查DB
baseTTL: 30 * time.Minute,
jitterMax: 10 * time.Minute,
}
}
// Get 带多级保护的缓存查询
func (p *AvalancheProtection) Get(
ctx context.Context,
key string,
loadFn func(ctx context.Context) (interface{}, error),
) (interface{}, error) {
// 第一层保护:限流
if !p.limiter.Allow() {
return nil, errors.New("rate limit exceeded, please retry later")
}
// 尝试从缓存获取
val, err := p.rdb.Get(ctx, key).Result()
if err == nil {
return val, nil
}
// 第二层保护:信号量控制并发查DB数量
select {
case p.semaphore <- struct{}{}:
defer func() { <-p.semaphore }()
case <-time.After(3 * time.Second):
// 等待超时,返回降级数据
return p.getFallbackData(key)
case <-ctx.Done():
return nil, ctx.Err()
}
// 第三层保护:分布式锁,防止同一Key重复查DB
lockKey := "lock:" + key
locked := p.rdb.SetNX(ctx, lockKey, "1", 10*time.Second).Val()
if !locked {
// 其他goroutine正在重建该Key的缓存,等待
time.Sleep(100 * time.Millisecond)
// 重试从缓存获取
return p.rdb.Get(ctx, key).Result()
}
defer p.rdb.Del(ctx, lockKey)
// 查询数据库
data, err := loadFn(ctx)
if err != nil {
return p.getFallbackData(key) // 查询失败,返回降级数据
}
// 写入缓存(随机TTL)
actualTTL := p.baseTTL + time.Duration(rand.Int63n(int64(p.jitterMax)))
p.rdb.Set(ctx, key, data, actualTTL)
return data, nil
}
// getFallbackData 获取降级数据
func (p *AvalancheProtection) getFallbackData(key string) (interface{}, error) {
// 降级策略:返回默认值/上次缓存的快照/空结果
return map[string]interface{}{
"status": "degraded",
"message": "service temporarily unavailable",
}, nil
}
| 方案 | 架构 | 自动故障转移 | 数据分片 | 适用规模 |
|---|---|---|---|---|
| 主从复制 | 1主N从 | ❌ 需手动切换 | ❌ 全量复制 | 小型项目 |
| 哨兵模式 | 1主N从+哨兵 | ✅ 自动切换 | ❌ 全量复制 | 中型项目 |
| Cluster 集群 | 多主多从 | ✅ 自动切换 | ✅ 16384 哈希槽 | 大型项目 |
任意 Master 宕机,对应 Slave 自动提升为 Master,保证缓存服务持续可用
package cache
import (
"context"
"fmt"
"time"
"github.com/redis/go-redis/v9"
)
// NewRedisCluster 创建 Redis Cluster 客户端
func NewRedisCluster() *redis.ClusterClient {
return redis.NewClusterClient(&redis.ClusterOptions{
// 集群节点地址(至少配置一个,客户端会自动发现其他节点)
Addrs: []string{
"192.168.1.10:7000",
"192.168.1.10:7001",
"192.168.1.11:7000",
"192.168.1.11:7001",
"192.168.1.12:7000",
"192.168.1.12:7001",
},
// 密码认证
Password: "your_password",
// 最大重试次数(故障转移期间自动重试)
MaxRedirects: 3,
// 读取超时
ReadTimeout: 3 * time.Second,
// 写入超时
WriteTimeout: 3 * time.Second,
// 连接池配置
PoolSize: 100,
MinIdleConns: 20,
// 路由函数:读请求可以路由到从节点(读写分离)
RouteByLatency: true,
// 集群拓扑刷新间隔
ClusterSlotsRefreshInterval: 10 * time.Second,
})
}
// SentinelClient 哨兵模式客户端
func NewSentinelClient() *redis.Client {
return redis.NewFailoverClient(&redis.FailoverOptions{
// 哨兵节点地址
SentinelAddrs: []string{
"192.168.1.10:26379",
"192.168.1.11:26379",
"192.168.1.12:26379",
},
// 主节点名称(在哨兵配置中定义的)
MasterName: "mymaster",
// 数据库密码
Password: "your_password",
// 数据库编号
DB: 0,
// 只读节点(读写分离)
ReplicaOnly: false,
})
}
// HealthCheck 缓存健康检查
func HealthCheck(ctx context.Context, client *redis.ClusterClient) error {
if err := client.Ping(ctx).Err(); err != nil {
return fmt.Errorf("redis cluster health check failed: %w", err)
}
// 检查集群状态
clusterInfo, err := client.ClusterInfo(ctx).Result()
if err != nil {
return fmt.Errorf("failed to get cluster info: %w", err)
}
// 验证集群状态是否为ok
if !contains(clusterInfo, "cluster_state:ok") {
return fmt.Errorf("cluster state is not ok")
}
return nil
}
缓存穿透的关键词是"数据不存在"。攻击者利用不存在的数据 ID 发起大量请求,这些请求在缓存中永远 miss,每次都穿透到数据库。
针对缓存穿透问题,核心思路是拦截无效请求,防止其穿透到数据库。推荐三层防护组合使用:
| 方案 | 核心思路 | 防护能力 | 内存开销 | 适用场景 |
|---|---|---|---|---|
| ✅ 参数校验 | 在请求到达缓存前,拦截明显不合法的参数(如负数 ID) | 低(仅拦截格式错误) | 无 | 所有场景(必做) |
| 📦 缓存空对象 | 数据库查询为空时,缓存空值标记(短 TTL),防止重复穿透 | 中(防止重复查询) | 中(空值占内存) | 数据量不大,攻击 ID 有限 |
| 🌸 布隆过滤器 | 用位数组存储合法 ID,请求到达前先检查,不存在则直接拒绝 | 高(从根源拦截) | 低(位数组省内存) | 数据量大,防恶意攻击 |
package handler
import (
"errors"
"fmt"
"regexp"
"strconv"
)
var (
ErrInvalidID = errors.New("invalid parameter: id must be positive")
ErrInvalidFormat = errors.New("invalid parameter format")
ErrIDOutOfRange = errors.New("id out of valid range")
)
// ParamValidator 参数校验器
type ParamValidator struct {
maxUserID int64 // 最大合法用户ID(可根据业务设定)
}
func NewParamValidator(maxUserID int64) *ParamValidator {
return &ParamValidator{maxUserID: maxUserID}
}
// ValidateUserID 校验用户ID
func (v *ParamValidator) ValidateUserID(idStr string) (int64, error) {
// 1. 非空校验
if idStr == "" {
return 0, fmt.Errorf("%w: id is empty", ErrInvalidFormat)
}
// 2. 格式校验(纯数字)
matched, _ := regexp.MatchString(`^\d+$`, idStr)
if !matched {
return 0, fmt.Errorf("%w: id must be numeric", ErrInvalidFormat)
}
// 3. 数值转换与范围校验
id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil {
return 0, fmt.Errorf("%w: %v", ErrInvalidFormat, err)
}
// 4. 正数校验
if id <= 0 {
return 0, ErrInvalidID
}
// 5. 范围校验(防止超大ID)
if id > v.maxUserID {
return 0, fmt.Errorf("%w: max is %d", ErrIDOutOfRange, v.maxUserID)
}
return id, nil
}
// GetUserHandler 用户查询接口
func (h *Handler) GetUserHandler(idStr string) (interface{}, error) {
// 第一道防线:参数校验
userID, err := h.validator.ValidateUserID(idStr)
if err != nil {
// 参数不合法,直接拒绝,不查缓存也不查数据库
return nil, fmt.Errorf("bad request: %w", err)
}
// 参数合法,继续走缓存查询逻辑...
return h.cache.Get(ctx, fmt.Sprintf("user:%d", userID), ...)
}
package cache
import (
"context"
"encoding/json"
"errors"
"time"
"github.com/redis/go-redis/v9"
)
var (
ErrDataNotFound = errors.New("data not found")
)
// 空值标记常量
const (
NullValue = "@@NULL@@" // 空值标记字符串
NullTTL = 60 * time.Second // 空值的TTL(较短,防止长期占用内存)
NormalTTL = 30 * time.Minute // 正常数据的TTL
)
// NullCache 支持缓存空对象的缓存实现
type NullCache struct {
rdb *redis.Client
nullTTL time.Duration
normalTTL time.Duration
}
func NewNullCache(rdb *redis.Client) *NullCache {
return &NullCache{
rdb: rdb,
nullTTL: NullTTL,
normalTTL: NormalTTL,
}
}
// Get 获取数据(自动处理空值缓存)
func (c *NullCache) Get(
ctx context.Context,
key string,
loadFn func(ctx context.Context) (interface{}, error),
) (interface{}, error) {
// Step 1: 从缓存获取
val, err := c.rdb.Get(ctx, key).Result()
if err == nil {
// 缓存命中
// 检查是否是空值标记
if val == NullValue {
// 命中空值,直接返回"数据不存在",不查数据库
return nil, ErrDataNotFound
}
// 正常数据,反序列化返回
var result interface{}
json.Unmarshal([]byte(val), &result)
return result, nil
}
if err != redis.Nil {
return nil, err
}
// Step 2: 缓存未命中,查询数据库
data, err := loadFn(ctx)
if err != nil {
return nil, err
}
// Step 3: 判断数据库查询结果
if data == nil {
// 数据库也不存在 → 缓存空值
c.rdb.Set(ctx, key, NullValue, c.nullTTL)
return nil, ErrDataNotFound
}
// 数据库存在 → 缓存正常数据
dataBytes, _ := json.Marshal(data)
c.rdb.Set(ctx, key, string(dataBytes), c.normalTTL)
return data, nil
}
// 使用示例
func Example() {
cache := NewNullCache(rdb)
// 查询用户(用户不存在的情况)
data, err := cache.Get(ctx, "user:999999", func(ctx context.Context) (interface{}, error) {
return db.QueryUser(ctx, 999999) // 返回 nil, nil
})
if errors.Is(err, ErrDataNotFound) {
// 数据不存在(已被缓存为空值,后续请求不再查DB)
return "user not found"
}
}
布隆过滤器由一个很长的二进制位数组和多个哈希函数组成。
如果任一位为 0,则该元素一定不存在。这个判断 100% 准确。
如果所有位都为 1,该元素可能存在(有一定误判率/假阳性率)。
假设位数组长度 16,使用 3 个哈希函数 H1, H2, H3
初始状态(全 0):
插入 "user:1001" → H1=1, H2=5, H3=12:
插入 "user:1002" → H1=3, H2=5, H3=9:
查询 "user:9999" → H1=2, H2=7, H3=14:
→ 位2、位7、位14 有0 → 一定不存在!直接拒绝 ✅
package cache
import (
"context"
"hash/fnv"
"math"
"github.com/redis/go-redis/v9"
)
// BloomFilter 基于 Redis 的布隆过滤器实现
type BloomFilter struct {
rdb *redis.Client
key string // Redis 中存储位数组的 key
bitSize uint // 位数组大小
hashNum uint // 哈希函数个数
}
// NewBloomFilter 创建布隆过滤器
// expectedInsertions: 预计插入的元素数量
// falsePositiveRate: 期望的误判率(如 0.01 表示 1%)
func NewBloomFilter(
rdb *redis.Client,
key string,
expectedInsertions uint64,
falsePositiveRate float64,
) *BloomFilter {
// 计算最优位数组大小: m = -(n * ln(p)) / (ln(2))^2
bitSize := uint(math.Ceil(
-float64(expectedInsertions) * math.Log(falsePositiveRate) /
(math.Log(2) * math.Log(2)),
))
// 计算最优哈希函数数量: k = (m/n) * ln(2)
hashNum := uint(math.Ceil(
(float64(bitSize) / float64(expectedInsertions)) * math.Log(2),
))
return &BloomFilter{
rdb: rdb,
key: key,
bitSize: bitSize,
hashNum: hashNum,
}
}
// Add 添加元素到布隆过滤器
func (bf *BloomFilter) Add(ctx context.Context, value string) error {
pipe := bf.rdb.Pipeline()
for i := uint(0); i < bf.hashNum; i++ {
index := bf.hash(value, i)
pipe.SetBit(ctx, bf.key, int64(index), 1)
}
_, err := pipe.Exec(ctx)
return err
}
// AddBatch 批量添加元素
func (bf *BloomFilter) AddBatch(ctx context.Context, values []string) error {
pipe := bf.rdb.Pipeline()
for _, value := range values {
for i := uint(0); i < bf.hashNum; i++ {
index := bf.hash(value, i)
pipe.SetBit(ctx, bf.key, int64(index), 1)
}
}
_, err := pipe.Exec(ctx)
return err
}
// Exists 检查元素是否可能存在
// 返回 true: 元素可能存在(需要继续查缓存/DB)
// 返回 false: 元素一定不存在(直接拒绝)
func (bf *BloomFilter) Exists(ctx context.Context, value string) (bool, error) {
for i := uint(0); i < bf.hashNum; i++ {
index := bf.hash(value, i)
bit, err := bf.rdb.GetBit(ctx, bf.key, int64(index)).Result()
if err != nil {
return false, err
}
if bit == 0 {
// 任一位为0,元素一定不存在
return false, nil
}
}
// 所有位都为1,元素可能存在
return true, nil
}
// hash 使用 FNV 哈希 + 种子模拟多个哈希函数
func (bf *BloomFilter) hash(value string, seed uint) uint {
h := fnv.New64a()
h.Write([]byte(value))
// 加入种子产生不同的哈希值
h.Write([]byte{byte(seed)})
return uint(h.Sum64()) % bf.bitSize
}
// InitFromDB 从数据库加载所有合法ID到布隆过滤器
func (bf *BloomFilter) InitFromDB(ctx context.Context, loadAllIDs func() ([]string, error)) error {
ids, err := loadAllIDs()
if err != nil {
return err
}
// 批量添加到布隆过滤器
batchSize := 1000
for i := 0; i < len(ids); i += batchSize {
end := i + batchSize
if end > len(ids) {
end = len(ids)
}
if err := bf.AddBatch(ctx, ids[i:end]); err != nil {
return err
}
}
return nil
}
package cache
import (
"context"
"errors"
"fmt"
"time"
)
var (
ErrInvalidParam = errors.New("invalid parameter")
ErrDataNotExist = errors.New("data does not exist")
ErrServiceDegraded = errors.New("service degraded")
)
// PenetrationGuard 缓存穿透防护器(三层防护)
type PenetrationGuard struct {
bloomFilter *BloomFilter // 第三层:布隆过滤器
nullCache *NullCache // 第二层:空值缓存
}
// Get 带三层防护的缓存查询
func (g *PenetrationGuard) Get(
ctx context.Context,
key string,
loadFn func(ctx context.Context) (interface{}, error),
) (interface{}, error) {
// ========== 第一层:参数校验 ==========
// (在调用此方法之前已完成,此处省略)
// ========== 第二层:布隆过滤器检查 ==========
exists, err := g.bloomFilter.Exists(ctx, key)
if err != nil {
// 布隆过滤器查询失败,降级处理(放行,让后续层处理)
// 这里选择放行而不是拒绝,保证可用性
}
if !exists {
// 布隆过滤器判定不存在 → 一定不存在 → 直接拒绝
return nil, ErrDataNotExist
}
// ========== 第三层:空值缓存 ==========
// 布隆过滤器说可能存在,再查缓存(含空值缓存)
data, err := g.nullCache.Get(ctx, key, loadFn)
if err != nil {
if errors.Is(err, ErrDataNotFound) {
// 数据库也不存在
return nil, ErrDataNotExist
}
return nil, err
}
return data, nil
}
// ============ 完整使用示例 ============
func GetUserExample() {
// 初始化
guard := &PenetrationGuard{
bloomFilter: NewBloomFilter(rdb, "bloom:users", 1000000, 0.01),
nullCache: NewNullCache(rdb),
}
// 启动时从数据库加载所有合法用户ID到布隆过滤器
guard.bloomFilter.InitFromDB(ctx, func() ([]string, error) {
return db.GetAllUserIDs()
})
// 处理请求
userID := "-1" // 恶意请求
// 第一层:参数校验
if id, err := validateUserID(userID); err != nil {
return "参数不合法" // 直接拒绝,-1 不合法
}
// 第二层+第三层:布隆过滤器 + 空值缓存
data, err := guard.Get(ctx, "user:999999", func(ctx context.Context) (interface{}, error) {
return db.GetUser(ctx, 999999)
})
if errors.Is(err, ErrDataNotExist) {
return "用户不存在" // 布隆过滤器或空值缓存拦截
}
}
| 方案 | 防护能力 | 内存开销 | 实现复杂度 | 适用场景 |
|---|---|---|---|---|
| 参数校验 | 低(只能拦截格式错误) | 无 | 极低 | 所有场景(必做) |
| 缓存空对象 | 中(防止重复查询) | 中(空值占内存) | 低 | 数据量不大,攻击ID有限 |
| 布隆过滤器 | 高(从根源拦截) | 低(位数组很省内存) | 中高 | 数据量大,防恶意攻击 |
标准布隆过滤器不支持删除操作。如果需要删除,可使用计数布隆过滤器(Counting Bloom Filter),用计数器代替单个位。
当插入元素超过预期数量时,误判率会显著上升。需要定期重建或动态扩容。
每隔一定时间(如每天凌晨),从数据库重新加载所有合法 ID 到新的布隆过滤器,然后切换。
新增数据时同步添加到布隆过滤器。删除数据时,由于不能直接删除,可以标记为"逻辑删除",定期重建时过滤掉。
// RebuildBloomFilter 定期重建布隆过滤器
func RebuildBloomFilter(ctx context.Context, bf *BloomFilter) error {
// 使用新key创建新的布隆过滤器
newKey := bf.key + ":new"
newBF := NewBloomFilter(bf.rdb, newKey, bf.estimatedCapacity, bf.falsePositiveRate)
// 从数据库加载最新数据
err := newBF.InitFromDB(ctx, func() ([]string, error) {
return db.GetAllValidIDs(ctx) // 只加载有效数据
})
if err != nil {
return err
}
// 原子切换:rename 操作在 Redis 中是原子的
// 将旧key备份,新key替换为正式key
pipe := bf.rdb.Pipeline()
pipe.Rename(ctx, bf.key, bf.key+":old")
pipe.Rename(ctx, newKey, bf.key)
_, err = pipe.Exec(ctx)
if err != nil {
return err
}
// 延迟删除旧key
go func() {
time.Sleep(1 * time.Minute)
bf.rdb.Del(context.Background(), bf.key+":old")
}()
return nil
}
// StartPeriodicRebuild 启动定时重建任务
func StartPeriodicRebuild(ctx context.Context, bf *BloomFilter, interval time.Duration) {
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
if err := RebuildBloomFilter(ctx, bf); err != nil {
log.Printf("failed to rebuild bloom filter: %v", err)
} else {
log.Printf("bloom filter rebuilt successfully")
}
case <-ctx.Done():
return
}
}
}