3. 缓存失效策略:TTL过期、主动失效、被动失效、基于事件的失效机制
在分布式缓存与数据库并存的架构中,缓存失效策略直接决定了数据一致性的时效窗口与系统负载。Kubernetes 环境下,Pod 生命周期短、配置变更频繁,失效策略的选择需要兼顾缓存命中率、数据库压力与一致性等级。本章从四种核心失效机制出发,分析其原理、适用场景及在 K8s 中的部署要点。
3.1 四种失效策略对比总览
| 策略 | 触发方式 | 一致性时效 | 对数据库压力 | K8s 部署适配性 |
|---|---|---|---|---|
| TTL 过期 | 时间驱动(被动) | 弱一致,窗口 = TTL | 低(批量过期) | 高,无外部依赖 |
| 主动失效 | 写操作后立即删除/更新缓存 | 强一致(写后即失效) | 中(单条失效) | 需保证写操作与失效操作的原子性 |
| 被动失效 | 读请求发现缓存缺失后回源 | 弱一致,依赖上游失效通知 | 高(缓存击穿风险) | 需配合限流与回源保护 |
| 基于事件的失效 | 数据变更事件驱动(CDC / 消息队列) | 最终一致,延迟可控 | 低(异步批量) | 高,与 K8s 事件驱动架构天然契合 |
3.2 TTL 过期:最基础的失效策略
TTL(Time-To-Live)是缓存系统内置的自动过期机制。每个缓存条目设置一个存活时间,到期后自动失效。在 Redis 中通过 EXPIRE 命令实现,在 Memcached 中通过 set 命令的过期时间参数实现。
关键指标: TTL 设置过短 → 缓存命中率下降,数据库压力上升;TTL 设置过长 → 数据不一致窗口增大。建议根据业务容忍度设置 TTL,通常为 30s ~ 5min。
# Redis TTL 设置示例
SET user:1001 '{"name":"Alice","role":"admin"}' EX 120 # 120秒后自动过期
K8s 部署注意事项:
- Redis / Memcached 作为 StatefulSet 部署时,需确保 TTL 机制不受 Pod 重启影响(RDB/AOF 持久化)。
- 避免所有缓存条目设置相同 TTL,防止缓存雪崩。建议在基础 TTL 上增加 ±10%~20% 的随机偏移。
- TTL 过期适合读多写少、对一致性要求不高的场景,如商品详情页、配置信息。
3.3 主动失效:写操作驱动的强一致性策略
主动失效是指在数据库写入操作完成后,立即对缓存中对应的条目执行删除或更新操作。这是实现缓存与数据库强一致性的最直接手段。
最佳实践: 在 K8s 微服务架构中,建议将缓存失效操作与数据库写操作放在同一个事务边界内(如使用本地事务 + 消息队列),避免写库成功但失效失败导致的数据不一致。
// 主动失效伪代码(Go 示例)
func UpdateUser(db *sql.DB, cache *redis.Client, userID string, newData User) error {
tx, _ := db.Begin()
// 1. 更新数据库
if err := tx.Exec("UPDATE users SET ... WHERE id=?", userID); err != nil {
tx.Rollback()
return err
}
// 2. 立即删除缓存(主动失效)
if err := cache.Del(ctx, "user:"+userID).Err(); err != nil {
tx.Rollback()
return err
}
return tx.Commit()
}
主动失效的三种实现模式:
- Cache-Aside 模式: 应用层在写库后主动删除缓存,下次读请求触发回源。
- Write-Through 模式: 写操作同时更新数据库和缓存,保证两者一致。
- Write-Behind 模式: 写操作先更新缓存,异步批量写入数据库,适用于写吞吐量高的场景。
3.4 被动失效:读请求驱动的回源策略
被动失效是指缓存条目在未被主动删除或 TTL 过期的情况下,由读请求发现缓存缺失后触发回源加载。这种策略通常与主动失效配合使用,作为兜底机制。
风险提示: 被动失效在高并发场景下容易引发缓存击穿(Cache Breakdown)。当某个热点 key 失效时,大量并发请求同时回源数据库,可能导致数据库过载。K8s 中建议配合限流(Rate Limiter)或分布式锁(如 Redis Redlock)保护数据库。
# 被动失效 + 分布式锁回源(Python 示例)
def get_user(user_id):
cache_key = f"user:{user_id}"
data = redis.get(cache_key)
if data:
return data
# 缓存缺失,尝试获取分布式锁
lock_key = f"lock:{cache_key}"
if redis.setnx(lock_key, "1", ex=5): # 5秒超时
data = db.query("SELECT * FROM users WHERE id=?", user_id)
redis.setex(cache_key, 120, data) # 回源后写入缓存
redis.delete(lock_key)
return data
else:
# 等待其他线程回源完成
time.sleep(0.1)
return get_user(user_id) # 递归重试
3.5 基于事件的失效:异步解耦的最终一致性方案
基于事件的失效机制通过捕获数据变更事件(如数据库 binlog、K8s ConfigMap 变更事件),异步通知缓存系统执行失效操作。这种策略在微服务架构中尤为流行,因为它实现了写操作与缓存失效的解耦。
K8s 中的事件源示例:
- 数据库 CDC 事件: 使用 Debezium 监听 MySQL binlog,将变更事件发送到 Kafka,缓存消费者订阅后执行失效。
- K8s ConfigMap / Secret 变更事件: 使用 Informer 监听资源变化,触发配置缓存的失效。
- 自定义业务事件: 微服务通过 CloudEvents 规范发布数据变更事件,缓存服务订阅后处理。
# K8s ConfigMap 变更事件监听(Go 示例)
import (
"k8s.io/client-go/informers"
"k8s.io/client-go/tools/cache"
)
func WatchConfigMapCache() {
factory := informers.NewSharedInformerFactory(clientset, 0)
informer := factory.Core().V1().ConfigMaps().Informer()
informer.AddEventHandler(cache.ResourceEventHandlerFuncs{
UpdateFunc: func(oldObj, newObj interface{}) {
cm := newObj.(*v1.ConfigMap)
// 失效对应配置缓存
redis.Del(ctx, "config:"+cm.Name)
},
})
stopCh := make(chan struct{})
informer.Run(stopCh)
}
3.6 策略选择矩阵
| 业务场景 | 推荐策略 | 理由 |
|---|---|---|
| 商品详情页(读多写少) | TTL 过期 + 主动失效 | TTL 兜底,主动失效保证价格/库存更新后立即生效 |
| 用户会话(Session) | TTL 过期 | 会话天然有时效性,无需主动失效 |
| 配置中心(ConfigMap) | 基于事件的失效 | K8s 原生事件驱动,配置变更后自动失效缓存 |
| 热点数据(秒杀/排行榜) | 主动失效 + 被动失效兜底 | 主动失效保证一致性,被动失效+分布式锁防止击穿 |
| 跨微服务数据共享 | 基于事件的失效 | 解耦服务间依赖,异步处理变更事件 |
K8s 部署建议: 在 Kubernetes 环境中,推荐采用混合策略:核心业务数据使用主动失效 + TTL 兜底,配置类数据使用基于事件的失效,热点数据增加被动失效的限流保护。所有失效策略都应通过 Prometheus 指标监控(如失效次数、回源延迟、缓存命中率),以便在 Dashboard 中实时调整策略参数。