采用了 惰性删除 和 定期删除 两种策略处理过期键:
Redis 实现方式:每次访问前调用
expireIfNeeded()
判断是否过期,若已过期,Redis 4.0 起还支持lazyfree_lazy_expire
控制是否异步删除。
机制:Redis 每秒默认执行 10 次定期删除,每次从过期字典中随机抽取 20 个 key 检查。
流程:
优点:相较惰性删除,能主动清理部分过期 key,提升内存利用率;
缺点:受限于检查频率和时间,可能存在部分过期 key 无法及时清理的问题。
定时删除(即为每个 key 单独设置定时器,到期立即删除)并未被 Redis 实际采用。
Redis 的过期键清除策略采用了 惰性删除 + 定期删除的组合策略,在保证较低 CPU 开销的同时,尽可能释放内存空间。
- 惰性删除 是只在访问key的时候检查是否过期;
- 定期删除 定时进行部分key的过期检查;
- Redis 放弃了定时删除,是因为对每个key单独计时过期删除,会大大增加cpu负担
LRU,全称 Least Recently Used,即「最近最少使用」策略,用于在内存满时淘汰最久未被访问的键。
Redis 没有使用传统 LRU,而是实现了近似 LRU,采用“随机采样 + 时间戳”方式。
lru
字段,记录其最近访问时间(非绝对时间,是一个全局逻辑时钟)。maxmemory-samples
配置。LFU,全称 Least Frequently Used,即「最不常访问」策略。用于淘汰访问次数最少的键,更能避免短期热点带来的缓存污染。
特性 | Redis 中的 LRU | Redis 中的 LFU |
---|---|---|
全称 | Least Recently Used | Least Frequently Used |
淘汰依据 | 最近一次访问时间 | 被访问的频率 |
实现方式 | 每个 key 保存时间戳 + 随机采样 | 每个 key 保存访问次数 + 随机采样 |
淘汰方式 | 随机采样若干 key,淘汰其中最久未访问的 | 随机采样若干 key,淘汰其中访问最少的 |
优点 | 高效节省内存 | 能减少缓存污染 |
典型场景 | 访问时间分布均匀 | 存在热点和冷数据 |
编号 | 操作顺序 | 是否推荐 | 问题说明 |
---|---|---|---|
1 | 先更新数据库 → 后更新缓存 | 否 | 并发更新可能将旧值写入缓存,导致脏数据 |
2 | 先更新缓存 → 后更新数据库 | 否 | 缓存更新成功但数据库失败,导致数据不一致 |
3 | 先删除缓存 → 后更新数据库 | 是 | 可避免脏数据写入缓存,但可能存在缓存空窗期 |
4 | 先更新数据库 → 后删除缓存 | 是 | 若删除缓存失败,可能导致脏缓存,可使用“延迟双删”策略优化 |
时间线场景:
T1:请求A准备写操作,删除缓存
T2:请求B查询,发现缓存 miss(被 A 删除了)
T3:请求B 回源数据库,读取旧数据(因为 A 还没更新 DB)
T4:请求B 将旧数据写入缓存
T5:请求A 将新数据写入数据库
使用 github.com/bsm/redislock:
go get github.com/bsm/redislock
package main
import (
"context"
"fmt"
"log"
"time"
"github.com/bsm/redislock"
"github.com/redis/go-redis/v9"
)
var (
ctx = context.Background()
rdb = redis.NewClient(&redis.Options{Addr: "localhost:6379"})
locker = redislock.New(rdb)
)
type UserProfile struct {
ID int64
Name string
}
// 模拟数据库操作
func updateUserInDB(userID int64, newData *UserProfile) error {
fmt.Printf("DB Updated: userID=%d, data=%+v\n", userID, newData)
return nil
}
func UpdateUserWithLock(userID int64, newData *UserProfile) error {
lockKey := fmt.Sprintf("lock:user:%d", userID)
lock, err := locker.Obtain(ctx, lockKey, 5*time.Second, nil)
if err != nil {
return fmt.Errorf("failed to obtain lock: %w", err)
}
defer lock.Release(ctx)
// 删除缓存
cacheKey := fmt.Sprintf("cache:user:%d", userID)
if err := rdb.Del(ctx, cacheKey).Err(); err != nil {
log.Printf("failed to delete cache: %v", err)
}
// 更新数据库
if err := updateUserInDB(userID, newData); err != nil {
return fmt.Errorf("failed to update db: %w", err)
}
return nil
}
时间线场景:
T1:请求A准备更新,先写数据库
T2:请求B 查询缓存,发现存在旧值
T3:请求A 删除缓存(第一次)
T4:请求B 读取旧缓存数据并返回
T5:请求B 之后把旧数据重新写入缓存(或未写入)
T6:请求A 延迟100ms后再次删除缓存
package main
import (
"context"
"fmt"
"log"
"time"
"github.com/redis/go-redis/v9"
)
var (
ctx = context.Background()
rdb = redis.NewClient(&redis.Options{Addr: "localhost:6379"})
)
type UserProfile struct {
ID int64
Name string
}
// 模拟数据库操作
func updateUserInDB(userID int64, newData *UserProfile) error {
fmt.Printf("DB Updated: userID=%d, data=%+v\n", userID, newData)
return nil
}
func UpdateUserWithDelayDelete(userID int64, newData *UserProfile) error {
// 更新数据库
if err := updateUserInDB(userID, newData); err != nil {
return fmt.Errorf("failed to update db: %w", err)
}
// 删除缓存
cacheKey := fmt.Sprintf("cache:user:%d", userID)
if err := rdb.Del(ctx, cacheKey).Err(); err != nil {
log.Printf("delete cache failed: %v", err)
}
// 延迟双删(异步)
go func() {
time.Sleep(100 * time.Millisecond)
if err := rdb.Del(ctx, cacheKey).Err(); err != nil {
log.Printf("delayed delete failed: %v", err)
}
}()
return nil
}
场景 | 推荐方案 | 优点 | 需注意问题与优化点 |
---|---|---|---|
一般中小型项目 | 方案三 | 实现简单、延迟可控 | 可用 Redis 锁优化 |
高频写/对一致性敏感 | 方案四 | 数据库操作主导、双删更稳健 | 需延迟双删、监控缓存删除是否成功 |
高并发写入场景 | 方案三+锁 | 防止并发缓存回写 | Redlock 实现需健壮 |
https://github.com/0voice