Redis 的过期策略以及内存淘汰机制详解
Redis 作为一个高性能的内存数据库,其内存管理机制至关重要。本文将深入探讨 Redis 的过期策略和内存淘汰机制,帮助您更好地理解和使用 Redis。
1. Redis 过期策略
Redis 提供了两种过期策略来处理过期键,这两种策略相互配合,共同确保过期键能够被及时清理。
1.1 惰性删除(Lazy Expiration)
介绍
惰性删除是一种"按需删除"的策略,它只在访问键时才检查键是否过期,只在真正需要时才执行删除操作,对 CPU 友好,不会占用额外的 CPU 时间,适合处理过期时间分布均匀的场景。如果过期键长期不被访问,导致大量过期键占用内存。
工作原理
1.2 定期删除(Periodic Expiration)
定期删除
定期删除是一种主动删除的策略,Redis 会定期检查并删除过期键,可以及时清理过期键,通过限制删除操作执行的时长和频率,减少对 CPU 的影响,如果过期键太多,可能会影响 Redis 的性能,也可能存在过期键未被及时删除的情况,需要合理配置检查频率和每次检查的键数量。
工作原理
1.3 策略配合使用
策略配合
Redis 的惰性删除和定期删除策略相互配合,共同确保过期键能够被及时清理。惰性删除保证访问时及时清理,定期删除则主动清理未被访问的过期键。
配置说明
# redis.conf
# 1. 基础配置
# 开启两种删除策略(默认开启)
lazyfree-lazy-eviction no
lazyfree-lazy-expire no
active-expire-cycle yes
# 2. 定期删除配置
# 设置执行频率(根据系统负载调整,默认10)
hz 10
# 设置每次检查的键数量(根据过期键数量调整,默认20)
active-expire-cycles-per-second 20
# 设置过期键比例阈值(默认25%)
active-expire-cycles-max-keys 25
# 设置时间限制(默认25ms)
active-expire-cycles-max-time 25
# 3. 键过期时间设置
# 设置键的过期时间
EXPIRE key seconds
EXPIREAT key timestamp
PEXPIRE key milliseconds
PEXPIREAT key milliseconds-timestamp
# 查看剩余生存时间
TTL key
PTTL key
# 移除过期时间
PERSIST key
最佳实践
合理设置过期时间
- 根据业务需求设置合适的过期时间
- 避免大量键同时过期
- 使用随机过期时间分散过期时间点
调整定期删除参数
- 根据系统负载调整
hz参数 - 根据过期键数量调整每次检查的键数量
- 监控过期键清理效果
- 根据系统负载调整
监控指标
- 监控过期键数量
- 监控内存使用情况
- 监控删除操作的执行时间
性能优化
- 避免存储过大的值
- 使用合适的数据结构
- 及时清理无用数据
实际应用示例
# 1. 设置不同过期时间的键
# 基础过期时间 + 随机值,避免同时过期
SET key1 "value1" EX $((3600 + RANDOM % 300)) # 1小时 + 随机0-5分钟
SET key2 "value2" EX $((7200 + RANDOM % 300)) # 2小时 + 随机0-5分钟
SET key3 "value3" EX $((10800 + RANDOM % 300)) # 3小时 + 随机0-5分钟
# 2. 监控过期键数量
INFO keyspace
# 3. 查看键的剩余生存时间
TTL key1
PTTL key2
# 4. 手动触发过期检查(不推荐,仅用于测试)
DEBUG OBJECT key1
2. 内存淘汰机制
当 Redis 内存使用达到上限时,会触发内存淘汰机制。Redis 提供了 8 种淘汰策略,可以根据实际需求选择合适的策略。
| 策略 | 说明 | 适用场景 |
|---|---|---|
| noeviction | 不淘汰,内存不足时报错 | 对数据一致性要求高的场景 |
| allkeys-lru | 所有键中最近最少使用的淘汰 | 热点数据访问场景 |
| volatile-lru | 设置了过期时间的键中最近最少使用的淘汰 | 需要过期时间的场景 |
| allkeys-random | 所有键中随机淘汰 | 数据访问均匀的场景 |
| volatile-random | 设置了过期时间的键中随机淘汰 | 需要过期时间的随机淘汰场景 |
| volatile-ttl | 设置了过期时间的键中即将过期的淘汰 | 优先淘汰即将过期的数据 |
实际应用示例
#redis.conf
# 配置 Redis 使用 volatile-lru 策略
CONFIG SET maxmemory-policy volatile-lru
# 设置最大内存为 1GB
CONFIG SET maxmemory 1gb
# 存储一些带过期时间的键
SET cache:user:1 "user_data" EX 3600
SET cache:product:1 "product_data" EX 7200
SET cache:order:1 "order_data" EX 1800
# 当内存不足时,Redis 会根据访问频率淘汰键
# 访问频率最低的键会被优先淘汰
3. 底层实现原理
3.1 过期键的存储结构
Redis 使用一个特殊的字典(expires)来存储设置了过期时间的键。这个字典的结构如下:
typedef struct redisDb {
dict *dict; // 数据字典
dict *expires; // 过期字典
// ... 其他字段
} redisDb;
过期字典的结构:
- 键:指向实际键的指针
- 值:过期时间戳(毫秒级)
3.2 惰性删除的实现原理
工作原理序列图
3.3 定期删除的实现原理
工作原理序列图
实现原理
Redis 使用一个定时任务来执行定期删除:
void activeExpireCycle(int type) {
// 每次处理的数据库数量
int dbs_per_call = CRON_DBS_PER_CALL;
// 每次处理的键数量
int keys_per_loop = 20;
// 计算需要处理的数据库数量
if (dbs_per_call > server.dbnum || type == ACTIVE_EXPIRE_CYCLE_FAST)
dbs_per_call = server.dbnum;
// 遍历数据库
for (int j = 0; j < dbs_per_call; j++) {
// 随机选择键
for (int i = 0; i < keys_per_loop; i++) {
// 随机选择一个键
key = dictGetRandomKey(db->expires);
// 检查是否过期
if (expireIfNeeded(db, key)) {
expired++;
}
}
}
}
3.4 LRU 算法的实现原理
Redis 使用近似 LRU 算法,通过记录键的最后访问时间来实现:
对象结构
typedef struct redisObject {
unsigned type:4;
unsigned encoding:4;
unsigned lru:LRU_BITS; // 记录最后访问时间
int refcount;
void *ptr;
} robj;
淘汰过程
int evictKey(redisDb *db, robj *key) {
// 获取对象的最后访问时间
unsigned long long idle = estimateObjectIdleTime(key);
// 选择最久未访问的键进行淘汰
if (idle > max_idle) {
max_idle = idle;
bestkey = key;
}
}
3.5 LFU 算法的实现原理
Redis 4.0 引入的 LFU 算法通过计数器记录访问频率:
计数器结构
typedef struct redisObject {
unsigned type:4;
unsigned encoding:4;
unsigned lru:LRU_BITS; // 用于 LFU 的计数器
int refcount;
void *ptr;
} robj;
计数器更新
void updateLFU(robj *val) {
// 获取当前计数器值
unsigned long counter = LFUDecrAndReturn(val);
// 增加计数器
counter = LFULogIncr(counter);
// 更新对象的计数器
val->lru = (LFUGetTimeInMinutes()<<8) | counter;
}
3.6 内存淘汰的触发机制
内存检查
int processCommand(client *c) {
// 检查内存使用情况
if (server.maxmemory) {
int retval = freeMemoryIfNeeded();
if (retval == C_ERR) {
addReply(c, shared.oomerr);
return C_ERR;
}
}
}
内存释放
int freeMemoryIfNeeded(void) {
// 计算需要释放的内存
size_t mem_tofree = mem_used - server.maxmemory;
// 根据策略选择要删除的键
while (mem_freed < mem_tofree) {
// 选择要删除的键
bestkey = selectKeyToEvict();
// 删除键
dbDelete(c->db, bestkey);
// 更新已释放内存
mem_freed += keySize + valSize;
}
}
4. 实际应用场景
4.1 百万级数据热点筛选案例
场景描述
假设有一个电商系统,需要从 100 万商品数据中筛选出热点商品进行缓存,同时需要处理商品数据的更新和过期。
1. 数据特点分析
高频访问商品(约 5 万)
- 热门商品、促销商品
- 访问频率:每分钟>100 次
- 数据特点:需要实时性,频繁更新
中频访问商品(约 15 万)
- 普通热销商品
- 访问频率:每小时>100 次
- 数据特点:需要一定实时性,定期更新
低频访问商品(约 80 万)
- 普通商品、冷门商品
- 访问频率:每天<100 次
- 数据特点:更新频率低,可以接受一定延迟
2. 缓存策略设计
# 1. 高频商品缓存配置
# 使用 volatile-lfu 策略,优先淘汰访问频率最低的键
CONFIG SET maxmemory-policy volatile-lfu
# 设置较短的过期时间(5分钟),确保数据实时性
SET product:hot:123 "商品数据" EX 300
# 2. 中频商品缓存配置
# 使用 volatile-lru 策略,优先淘汰最近最少使用的键
SET product:normal:456 "商品数据" EX 3600
# 3. 低频商品缓存配置
# 使用 volatile-ttl 策略,优先淘汰即将过期的键
SET product:cold:789 "商品数据" EX 86400
3. 实现方案
3.1 整体架构
3.2 热点数据识别流程
3.3 数据更新流程
3.4 过期处理流程
数据分层存储
@Service public class ProductCacheService { private final RedisTemplate<String, Object> redisTemplate; private static final int HOT_EXPIRE = 300; // 5分钟 private static final int NORMAL_EXPIRE = 3600; // 1小时 private static final int COLD_EXPIRE = 86400; // 24小时 @Autowired public ProductCacheService(RedisTemplate<String, Object> redisTemplate) { this.redisTemplate = redisTemplate; } public void cacheHotProduct(String productId, Product product) { String key = "product:hot:" + productId; redisTemplate.opsForValue().set(key, product, HOT_EXPIRE, TimeUnit.SECONDS); } public void cacheNormalProduct(String productId, Product product) { String key = "product:normal:" + productId; redisTemplate.opsForValue().set(key, product, NORMAL_EXPIRE, TimeUnit.SECONDS); } public void cacheColdProduct(String productId, Product product) { String key = "product:cold:" + productId; redisTemplate.opsForValue().set(key, product, COLD_EXPIRE, TimeUnit.SECONDS); } }热点数据识别
@Service public class HotProductDetector { private final RedisTemplate<String, Object> redisTemplate; private static final int HOT_THRESHOLD = 100; @Autowired public HotProductDetector(RedisTemplate<String, Object> redisTemplate) { this.redisTemplate = redisTemplate; } public void updateAccessCount(String productId) { String key = "product:access:" + productId; // 使用 HyperLogLog 统计访问次数 redisTemplate.opsForHyperLogLog().add(key, System.currentTimeMillis()); // 设置过期时间,避免占用过多内存 redisTemplate.expire(key, 1, TimeUnit.HOURS); } public boolean isHotProduct(String productId) { String key = "product:access:" + productId; Long count = redisTemplate.opsForHyperLogLog().size(key); return count != null && count > HOT_THRESHOLD; } public boolean isNormalProduct(String productId) { String key = "product:access:" + productId; Long count = redisTemplate.opsForHyperLogLog().size(key); return count != null && count > 10 && count <= HOT_THRESHOLD; } }数据更新机制
@Service public class ProductUpdateService { private final ProductCacheService cacheService; private final HotProductDetector hotDetector; private final ProductRepository productRepository; @Autowired public ProductUpdateService( ProductCacheService cacheService, HotProductDetector hotDetector, ProductRepository productRepository ) { this.cacheService = cacheService; this.hotDetector = hotDetector; this.productRepository = productRepository; } @Transactional public void updateProduct(String productId, Product product) { // 更新数据库 productRepository.save(product); // 更新缓存 if (hotDetector.isHotProduct(productId)) { cacheService.cacheHotProduct(productId, product); } else if (hotDetector.isNormalProduct(productId)) { cacheService.cacheNormalProduct(productId, product); } else { cacheService.cacheColdProduct(productId, product); } } @EventListener public void handleExpiredProduct(ExpiredProductEvent event) { String productId = event.getProductId(); // 从数据库重新加载数据 Product product = productRepository.findById(productId) .orElseThrow(() -> new ProductNotFoundException(productId)); // 根据访问频率决定缓存策略 if (hotDetector.isHotProduct(productId)) { cacheService.cacheHotProduct(productId, product); } else if (hotDetector.isNormalProduct(productId)) { cacheService.cacheNormalProduct(productId, product); } else { cacheService.cacheColdProduct(productId, product); } } }配置类
@Configuration public class RedisConfig { @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(factory); // 设置key的序列化方式 template.setKeySerializer(new StringRedisSerializer()); // 设置value的序列化方式 template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); // 设置hash key的序列化方式 template.setHashKeySerializer(new StringRedisSerializer()); // 设置hash value的序列化方式 template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer()); template.afterPropertiesSet(); return template; } @Bean public CacheManager cacheManager(RedisConnectionFactory factory) { RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofMinutes(5)) .serializeKeysWith(RedisSerializationContext.SerializationPair .fromSerializer(new StringRedisSerializer())) .serializeValuesWith(RedisSerializationContext.SerializationPair .fromSerializer(new GenericJackson2JsonRedisSerializer())) .disableCachingNullValues(); return RedisCacheManager.builder(factory) .cacheDefaults(config) .transactionAware() .build(); } }实体类
@Data @Entity public class Product { @Id private String id; private String name; private BigDecimal price; private String description; private LocalDateTime updateTime; // 其他字段... } @Data public class ExpiredProductEvent { private final String productId; private final LocalDateTime expiredTime; }
3.5 关键流程说明
热点数据识别流程
- 客户端请求进入系统
- 检查数据是否已缓存
- 更新访问计数
- 根据访问频率判断数据类别
- 按类别设置不同的缓存策略
数据更新流程
- 接收数据更新请求
- 进行数据验证
- 更新数据库
- 根据访问频率更新对应缓存
- 设置相应的过期时间
过期处理流程
- 监听缓存过期事件
- 重新加载数据
- 根据访问频率更新缓存
- 记录处理结果
监控指标
- 缓存命中率
- 内存使用情况
- 过期键数量
- 系统响应时间
4. 性能优化
内存使用优化
# 配置内存限制和淘汰策略 CONFIG SET maxmemory 4gb CONFIG SET maxmemory-policy volatile-lfu # 设置内存告警阈值 CONFIG SET maxmemory-samples 10过期时间优化
# 动态调整过期时间 def adjust_expire_time(self, product_id, access_count): base_time = 300 # 基础过期时间:5分钟 if access_count > 1000: # 高频访问:缩短过期时间 return base_time elif access_count > 100: # 中频访问:中等过期时间 return base_time * 12 else: # 低频访问:较长过期时间 return base_time * 288监控指标
# 监控关键指标 def monitor_metrics(self): # 1. 内存使用情况 memory_info = self.redis_client.info('memory') # 2. 过期键数量 expired_keys = self.redis_client.info('stats')['expired_keys'] # 3. 淘汰键数量 evicted_keys = self.redis_client.info('stats')['evicted_keys'] # 4. 热点商品命中率 hit_rate = self.calculate_hit_rate() return { 'memory_usage': memory_info['used_memory'], 'expired_keys': expired_keys, 'evicted_keys': evicted_keys, 'hit_rate': hit_rate }
5. 效果评估
性能指标
- 热点商品访问延迟:<10ms
- 缓存命中率:>95%
- 内存使用率:<80%
- 过期键清理效率:>99%
业务指标
- 商品数据实时性:<5 分钟
- 系统吞吐量:>10000 QPS
- 数据一致性:>99.9%
- 系统可用性:>99.99%
成本效益
- 内存使用优化:减少 50%
- 数据库压力:降低 80%
- 系统响应时间:提升 90%
- 运维成本:降低 60%
