跳到主要内容

Redis 的 TTL 到期后怎么回收的

Redis TTL 回收机制概述

Redis 的 TTL(Time To Live)机制允许为键设置过期时间,当键到期后需要被自动回收以释放内存。Redis 采用了两种互补的策略来处理过期键的回收:定期删除惰性删除

惰性删除(Lazy Expiration)

工作原理

惰性删除是指当客户端访问一个键时,Redis 会检查该键是否已过期,如果过期则立即删除该键并返回空值。

// 模拟 Redis 惰性删除的检查逻辑
type RedisKey struct {
Value interface{}
ExpireAt int64 // Unix 时间戳
}

func (r *RedisDB) Get(key string) (interface{}, bool) {
item, exists := r.data[key]
if !exists {
return nil, false
}

// 惰性删除检查
if item.ExpireAt > 0 && time.Now().Unix() > item.ExpireAt {
delete(r.data, key) // 立即删除过期键
r.updateMemoryStats(-r.getKeySize(key))
return nil, false
}

return item.Value, true
}

优势与局限

优势:

  • 精准性高:只在真正需要时才检查过期
  • CPU 开销小:不需要额外的后台扫描
  • 实时性强:访问时立即清理过期数据

局限性:

  • 依赖访问:如果过期键长期不被访问,会一直占用内存
  • 内存泄漏风险:在写多读少的场景下可能积累大量过期键

实际应用场景

// 缓存场景示例
func cacheExample() {
// 用户信息缓存,30分钟过期
redis.Set("user:1001", userInfo, 30*time.Minute)

// 访问时自动检查过期
if user, exists := redis.Get("user:1001"); exists {
fmt.Println("缓存命中:", user)
} else {
// 缓存过期或不存在,从数据库重新加载
user = loadUserFromDB(1001)
redis.Set("user:1001", user, 30*time.Minute)
}
}

定期删除(Periodic Expiration)

工作机制

Redis 通过后台定时任务主动扫描和删除过期键,这个过程独立于客户端访问,确保即使不被访问的键也能被及时清理。

扫描策略

// 模拟 Redis 定期删除的扫描逻辑
type ExpirationScanner struct {
db *RedisDB
scanFreq time.Duration // 扫描频率
maxScanTime time.Duration // 单次扫描最大时间
sampleSize int // 每次采样键数量
}

func (s *ExpirationScanner) StartPeriodicScan() {
ticker := time.NewTicker(s.scanFreq) // 默认100ms

go func() {
for range ticker.C {
s.scanExpiredKeys()
}
}()
}

func (s *ExpirationScanner) scanExpiredKeys() {
startTime := time.Now()

for {
// 随机采样键进行检查
keys := s.db.randomSampleKeys(s.sampleSize) // 默认20个
expiredCount := 0

for _, key := range keys {
if s.db.isExpired(key) {
s.db.deleteKey(key)
expiredCount++
}
}

// 如果过期键比例超过25%,继续扫描
expiredRatio := float64(expiredCount) / float64(len(keys))
if expiredRatio < 0.25 {
break
}

// 防止扫描时间过长影响性能
if time.Since(startTime) > s.maxScanTime {
break
}
}
}

自适应扫描频率

Redis 的定期删除采用自适应策略,根据过期键的密度动态调整扫描强度:

// 自适应扫描示例
func (s *ExpirationScanner) adaptiveScan() {
baseInterval := 100 * time.Millisecond

// 根据系统负载和过期键密度调整
expiredRatio := s.getRecentExpiredRatio()

switch {
case expiredRatio > 0.5: // 过期键很多
s.scanFreq = baseInterval / 2 // 加快扫描
case expiredRatio < 0.1: // 过期键很少
s.scanFreq = baseInterval * 2 // 减缓扫描
default:
s.scanFreq = baseInterval // 正常频率
}
}

内存回收的实际流程

键删除的完整过程

内存统计更新

// 内存回收统计示例
type MemoryStats struct {
TotalMemory int64 // 总内存使用
ExpiredKeys int64 // 已删除的过期键数量
LazyDeletes int64 // 惰性删除次数
PeriodicDeletes int64 // 定期删除次数
}

func (db *RedisDB) deleteExpiredKey(key string) {
item, exists := db.data[key]
if !exists {
return
}

// 计算释放的内存大小
freedMemory := db.calculateKeyMemory(key, item)

// 删除键值对
delete(db.data, key)
if item.ExpireAt > 0 {
delete(db.expires, key)
}

// 更新统计信息
db.stats.TotalMemory -= freedMemory
db.stats.ExpiredKeys++

// 发送过期事件通知
db.notifyExpiration(key)
}

实际业务场景中的应用

分布式锁超时释放

// 分布式锁场景
func acquireDistributedLock(lockKey string, timeout time.Duration) bool {
// 设置锁,30秒后自动过期
success := redis.SetNX(lockKey, "locked", timeout)
if success {
// 获取锁成功,TTL 确保即使程序异常也会自动释放
return true
}
return false
}

// Redis 的定期删除确保即使锁的持有者崩溃,锁也会在 TTL 到期后自动释放

限流窗口管理

// 限流场景
func rateLimitCheck(userID string) bool {
windowKey := fmt.Sprintf("rate_limit:%s", userID)

// 获取当前窗口的请求计数
current, _ := redis.Get(windowKey)
count := 0
if current != nil {
count = current.(int)
}

if count >= 100 { // 限制每分钟100次请求
return false
}

// 增加计数并设置1分钟过期
redis.Incr(windowKey)
redis.Expire(windowKey, time.Minute)

return true
}
// 定期删除确保过期的限流窗口被及时清理,避免内存堆积

会话管理

// 会话管理场景
func createUserSession(userID string, sessionData interface{}) string {
sessionID := generateSessionID()
sessionKey := fmt.Sprintf("session:%s", sessionID)

// 设置会话数据,2小时后过期
redis.Set(sessionKey, sessionData, 2*time.Hour)

return sessionID
}

// 惰性删除确保访问会话时立即检查过期状态
// 定期删除确保不活跃的会话也会被清理

性能优化与监控

TTL 回收性能监控

// TTL 回收监控指标
type TTLMetrics struct {
LazyDeleteRate float64 // 惰性删除速率 (keys/sec)
PeriodicDeleteRate float64 // 定期删除速率 (keys/sec)
MemoryFreedRate float64 // 内存释放速率 (bytes/sec)
AvgScanTime time.Duration // 平均扫描时间
}

func monitorTTLPerformance() {
ticker := time.NewTicker(time.Minute)

go func() {
for range ticker.C {
metrics := gatherTTLMetrics()

// 检查是否有性能问题
if metrics.AvgScanTime > 50*time.Millisecond {
log.Warn("TTL扫描时间过长,可能影响性能")
}

if metrics.MemoryFreedRate < expectedFreeRate {
log.Warn("内存释放速率低于预期,检查过期键积累")
}
}
}()
}

优化建议

  1. 合理设置 TTL:避免设置过短的 TTL 导致频繁扫描
  2. 监控过期键比例:定期检查过期键的积累情况
  3. 避免大量同时过期:分散过期时间避免瞬时压力
// 分散过期时间示例
func setWithJitteredTTL(key string, value interface{}, baseTTL time.Duration) {
// 添加随机偏移,避免大量键同时过期
jitter := time.Duration(rand.Intn(int(baseTTL.Seconds()/10))) * time.Second
actualTTL := baseTTL + jitter

redis.Set(key, value, actualTTL)
}

Redis 的 TTL 回收机制通过惰性删除和定期删除的结合,在保证内存及时释放的同时,最大化了性能效率。理解这些机制有助于在实际应用中合理使用 TTL 功能,避免内存泄漏和性能问题。