Redis 作为纯内存数据库,内存是最宝贵的资源。当 Redis 内存用尽时,如何处理新数据?这就是内存逐出策略要解决的问题。
Redis 内存逐出策略
先了解 Redis 提供的 8 种逐出策略:
| 策略 | 逐出范围 | 逐出规则 | 适用场景 | Redis 版本 |
|---|---|---|---|---|
| noeviction | 不逐出 | 拒绝写入 | 数据完整性要求极高 | 所有版本 |
| allkeys-lru | 所有键 | 最近最少使用 | 一般缓存场景 | 所有版本 |
| volatile-lru | 有 TTL 的键 | 最近最少使用 | 希望结合过期机制的缓存 | 所有版本 |
| allkeys-random | 所有键 | 随机 | 键访问概率相近 | 所有版本 |
| volatile-random | 有 TTL 的键 | 随机 | 键访问概率相近且结合过期机制 | 所有版本 |
| volatile-ttl | 有 TTL 的键 | 即将过期 | 优先保留寿命长的数据 | 所有版本 |
| allkeys-lfu | 所有键 | 使用频率最少 | 数据访问频率差异大 | 4.0+ |
| volatile-lfu | 有 TTL 的键 | 使用频率最少 | 结合过期机制且频率差异大 | 4.0+ |
版本兼容性
| 功能 | 4.0 以下 | 4.0 | 5.0 | 6.0 |
|---|---|---|---|---|
| LFU 策略 | 不支持 | ✓ | ✓ | ✓ |
| MEMORY PURGE | 不支持 | ✓ | ✓ | ✓ |
| 集群逐出监控 | 部分支持 | ✓ | ✓ | ✓ |
策略性能对比
真实场景下不同策略的性能表现:
| 策略 | QPS(万/秒) | 内存碎片率 | 逐出延迟(ms) | 命中率 |
|---|---|---|---|---|
| LRU | 12.5 | 15% | <1 | 中等 |
| LFU | 11.2 | 8% | 1-3 | 高 |
| 随机 | 13.8 | 20% | <0.5 | 低 |
| TTL | 12.0 | 10% | 1-2 | 不确定 |
逐出策略选择流程图
以下流程图帮你选择合适的策略:
各策略详解与实战
1. noeviction(不逐出)
当内存不足时,新写入操作会报错。Redis 默认使用这种策略。
// 设置Redis不逐出策略
try (Jedis jedis = jedisPool.getResource()) {
jedis.configSet("maxmemory-policy", "noeviction");
} catch (JedisException e) {
logger.error("设置Redis逐出策略失败", e);
// 可降级为其他策略
}
适用场景:金融交易数据、支付记录等不能丢失的核心业务数据。
案例:支付平台中的交易记录缓存,每笔交易数据都必须保留。
// 交易数据缓存示例
public void cacheTransaction(String txId, String txData) {
try (Jedis jedis = jedisPool.getResource()) {
// 使用noeviction确保数据不被逐出
String result = jedis.set("tx:" + txId, txData);
if (!"OK".equals(result)) {
// 内存可能已满,需处理写入失败情况
logger.error("交易数据缓存失败,可能内存已满: {}", txId);
// 触发告警
alertService.sendAlert("Redis内存已满", "交易数据写入失败");
}
} catch (Exception e) {
logger.error("缓存交易数据异常", e);
}
}
2. allkeys-lru(最近最少使用)
Redis 的 LRU 不是完美实现,而是基于采样的近似 LRU 算法。默认从数据中随机选择 5 个键,逐出其中最久未使用的键。
// 设置LRU逐出策略
try (Jedis jedis = jedisPool.getResource()) {
jedis.configSet("maxmemory-policy", "allkeys-lru");
// 增大采样数量提高LRU精度
jedis.configSet("maxmemory-samples", "10"); // 默认为5,增大可提高精度但消耗更多CPU
} catch (JedisException e) {
logger.error("设置LRU策略失败", e);
}
适用场景:大多数 Web 应用缓存,如新闻列表、商品信息等。
案例:电商网站的商品详情缓存。
// 商品信息缓存
public String getProductInfo(String productId) {
String cacheKey = "product:" + productId;
try (Jedis jedis = jedisPool.getResource()) {
// 先查缓存
String productInfo = jedis.get(cacheKey);
if (productInfo != null) {
return productInfo;
}
// 缓存未命中,从数据库获取
productInfo = productDao.getProductById(productId);
if (productInfo != null) {
// 放入缓存,24小时过期
jedis.setex(cacheKey, 86400, productInfo); // O(1)复杂度
}
return productInfo;
} catch (Exception e) {
logger.error("获取商品信息失败", e);
// 降级直接查数据库
return productDao.getProductById(productId);
}
}
3. volatile-lru
只从设置了 TTL 的键中逐出最久未使用的键。
// 设置volatile-lru策略
try (Jedis jedis = jedisPool.getResource()) {
jedis.configSet("maxmemory-policy", "volatile-lru");
// 缓存用户信息并设置过期时间
jedis.setex("user:" + userId, 3600, userJson);
} catch (JedisException e) {
logger.error("Redis操作失败", e);
}
适用场景:希望结合 Redis 过期机制,又能在内存紧张时优先逐出不常用数据,如用户会话信息。
注意事项:使用此策略时,必须确保大部分键都设置了 TTL,否则当只有少量键有过期时间时,可能会导致逐出效率低下。
4. allkeys-random(随机逐出)
从所有键中随机选择并逐出数据。
try (Jedis jedis = jedisPool.getResource()) {
jedis.configSet("maxmemory-policy", "allkeys-random");
} catch (JedisException e) {
logger.error("设置Redis策略失败", e);
}
适用场景:所有键的访问概率相近,如随机 ID 生成器的记录。
性能优势:逐出延迟最低,CPU 消耗最小。
5. volatile-random
仅从设置了 TTL 的键中随机逐出。
try (Jedis jedis = jedisPool.getResource()) {
jedis.configSet("maxmemory-policy", "volatile-random");
} catch (JedisException e) {
logger.error("设置Redis策略失败", e);
}
适用场景:临时数据且访问概率相近的场景,如临时验证码存储。
6. volatile-ttl
从设置了 TTL 的键中,逐出剩余生存时间最短的键。
try (Jedis jedis = jedisPool.getResource()) {
jedis.configSet("maxmemory-policy", "volatile-ttl");
} catch (JedisException e) {
logger.error("设置Redis策略失败", e);
}
适用场景:优先保留寿命长的数据,如各种有时效性的活动信息。
案例:优惠券系统,保留有效期长的优惠券数据。
// 优惠券缓存示例
public void cacheCoupon(String couponId, String couponData, int expiryDays) {
try (Jedis jedis = jedisPool.getResource()) {
// TTL设置为优惠券实际有效期
int ttlSeconds = expiryDays * 86400;
jedis.setex("coupon:" + couponId, ttlSeconds, couponData);
} catch (Exception e) {
logger.error("缓存优惠券数据失败", e);
}
}
7. allkeys-lfu(最少使用频率)
Redis 4.0 引入的 LFU 算法逐出访问频率最低的键。LFU 使用 24 位记录上次访问时间,8 位记录访问频率。
// Redis 4.0+支持
try (Jedis jedis = jedisPool.getResource()) {
// 检查Redis版本
String version = jedis.info("server").get("redis_version");
if (version.compareTo("4.0") < 0) {
logger.warn("当前Redis版本不支持LFU,版本: {}", version);
return;
}
jedis.configSet("maxmemory-policy", "allkeys-lfu");
/**
* lfu-log-factor=10:
* 影响频率计数的增长速度,值越小计数增长越快。
* 例如:factor=10时,访问100次可能计数为~20
* lfu-decay-time=1:
* 每1分钟检查一次计数器,若未访问则按规则衰减
* 设置为0表示不自动衰减
*/
jedis.configSet("lfu-log-factor", "10"); // 计数器对数因子
jedis.configSet("lfu-decay-time", "1"); // 计数器衰减时间(分钟)
} catch (JedisException e) {
logger.error("设置LFU策略失败", e);
}
LFU 计数器原理:访问频率计数公式
c = log(freq+1) / log(lfu-log-factor+1)
其中freq为实际访问次数。
适用场景:访问频率差异明显的场景,如热门文章与冷门文章的缓存。
性能特点:命中率最高,但 CPU 消耗较高。
8. volatile-lfu
仅从设置了 TTL 的键中,逐出使用频率最少的键。
// Redis 4.0+支持
try (Jedis jedis = jedisPool.getResource()) {
jedis.configSet("maxmemory-policy", "volatile-lfu");
} catch (JedisException e) {
logger.error("设置Redis策略失败", e);
}
适用场景:结合过期时间和访问频率的场景,如用户活跃度数据缓存。
反模式与解决方案
实际项目中常见的几个问题及解决方法:
| 反模式 | 现象 | 解决方案 |
|---|---|---|
| 误用 volatile-lru 但未设置 TTL | 内存持续增长且无逐出 | 强制要求业务代码设置过期时间 |
| LRU 采样数过低 | 命中率突然下降 | 逐步增大 maxmemory-samples 至 10-20 |
| 集群中使用 allkeys-lru | 节点数据分布不均 | 结合数据分片策略或使用一致性哈希 |
生产环境配置示例
# redis.conf 完整配置示例
maxmemory 8gb
maxmemory-policy allkeys-lfu
maxmemory-samples 10
lfu-log-factor 10
lfu-decay-time 1
不同场景逐出策略实战
1. 社交媒体 Feed 流缓存
社交媒体的 Feed 流数据,新内容不断产生,旧内容关注度逐渐降低:
// Feed流缓存配置
public void configureFeedCache() {
try (Jedis jedis = jedisPool.getResource()) {
// 检查Redis版本
String version = jedis.info("server").get("redis_version");
boolean supportsLfu = version.compareTo("4.0") >= 0;
// 优先使用LFU,不支持则降级为LRU
String policy = supportsLfu ? "allkeys-lfu" : "allkeys-lru";
jedis.configSet("maxmemory-policy", policy);
logger.info("Feed流缓存使用策略: {}", policy);
}
}
// 缓存用户Feed数据
public void cacheFeedItem(String userId, String feedId, String content) {
try (Jedis jedis = jedisPool.getResource()) {
String key = "feed:" + userId + ":" + feedId;
// 新Feed数据7天过期
jedis.setex(key, 7 * 86400, content);
// 更新Feed索引
String indexKey = "feed:index:" + userId;
jedis.zadd(indexKey, System.currentTimeMillis(), feedId);
jedis.zremrangeByRank(indexKey, 0, -101); // O(log(N)+M)复杂度,只保留最新的100条
} catch (Exception e) {
logger.error("缓存Feed数据失败", e);
}
}
2. 日志系统缓存
日志系统按时间衰减重要性,适合使用volatile-ttl:
// 日志缓存系统
public void cacheLogEntry(String logId, String logData, LogLevel level) {
try (Jedis jedis = jedisPool.getResource()) {
// 根据日志级别设置不同的TTL
int ttl;
switch (level) {
case ERROR:
ttl = 7 * 86400; // 错误日志保留7天
break;
case WARN:
ttl = 3 * 86400; // 警告日志保留3天
break;
default:
ttl = 1 * 86400; // 其他日志保留1天
}
jedis.setex("log:" + logId, ttl, logData);
} catch (Exception e) {
logger.error("缓存日志数据失败", e);
}
}
3. 实时分析系统
访问频率差异明显的实时分析数据,适合使用allkeys-lfu:
// 实时分析指标缓存
public void cacheMetric(String metricName, double value) {
try (Jedis jedis = jedisPool.getResource()) {
String key = "metric:" + metricName;
// 保存最新值
jedis.set(key, String.valueOf(value));
// 同时更新时间序列
String tsKey = "ts:" + metricName;
jedis.zadd(tsKey, System.currentTimeMillis(), String.valueOf(value));
// 只保留最近100个采样点
jedis.zremrangeByRank(tsKey, 0, -101);
} catch (Exception e) {
logger.error("缓存指标数据失败", e);
}
}
如何选择并配置合适的逐出策略
选择逐出策略的核心考虑因素:
- 数据访问模式:有明显热点数据吗?
- 数据重要性:所有数据都同样重要吗?
- 过期需求:是否需要数据自动过期?
- 内存限制:实例内存有多大?
// 配置Redis逐出策略和内存限制
public void configureRedisEviction(String policy, long maxMemoryMB) {
// 验证策略合法性
Set<String> validPolicies = new HashSet<>(Arrays.asList(
"noeviction", "allkeys-lru", "volatile-lru",
"allkeys-random", "volatile-random", "volatile-ttl",
"allkeys-lfu", "volatile-lfu"
));
if (!validPolicies.contains(policy)) {
logger.error("无效的Redis逐出策略: {}", policy);
policy = "allkeys-lru"; // 降级为通用策略
}
// 创建Redis连接池
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxTotal(100); // 最大连接数
poolConfig.setMaxIdle(20); // 最大空闲连接
poolConfig.setMinIdle(5); // 最小空闲连接
poolConfig.setTestOnBorrow(true); // 借用连接时测试有效性
poolConfig.setTestWhileIdle(true); // 空闲时测试连接有效性
try (JedisPool jedisPool = new JedisPool(poolConfig, "localhost");
Jedis jedis = jedisPool.getResource()) {
// 设置最大内存限制
jedis.configSet("maxmemory", String.valueOf(maxMemoryMB * 1024 * 1024));
// 设置逐出策略
jedis.configSet("maxmemory-policy", policy);
// 如果使用LRU/LFU,调整采样参数提高精度
if (policy.contains("lru") || policy.contains("lfu")) {
jedis.configSet("maxmemory-samples", "10");
}
// 保存配置
jedis.configRewrite();
logger.info("Redis配置完成 - 策略: {}, 最大内存: {}MB", policy, maxMemoryMB);
} catch (Exception e) {
logger.error("配置Redis失败", e);
}
}
监控 Redis 内存和逐出情况
有效监控内存使用和逐出情况是保障 Redis 性能的关键:
// 记录上次监控的逐出数
private long lastEvictedKeys = 0;
// 全面监控Redis内存和逐出情况
public RedisHealthMetrics monitorRedisHealth() {
RedisHealthMetrics metrics = new RedisHealthMetrics();
try (Jedis jedis = jedisPool.getResource()) {
// 内存指标
Map<String, String> memoryInfo = jedis.info("memory");
long usedMemory = Long.parseLong(memoryInfo.getOrDefault("used_memory", "0"));
long maxMemory = Long.parseLong(memoryInfo.getOrDefault("maxmemory", "0"));
double fragmentationRatio = Double.parseDouble(
memoryInfo.getOrDefault("mem_fragmentation_ratio", "1.0"));
// 计算内存使用率
double memoryUsageRatio = (maxMemory > 0) ?
(double) usedMemory / maxMemory : 0.0;
// 逐出指标
Map<String, String> statsInfo = jedis.info("stats");
long evictedKeys = Long.parseLong(statsInfo.getOrDefault("evicted_keys", "0"));
long expiredKeys = Long.parseLong(statsInfo.getOrDefault("expired_keys", "0"));
// 计算逐出频率(每分钟)
long evictionRate = evictedKeys - lastEvictedKeys;
lastEvictedKeys = evictedKeys;
// 命中率计算
long keyspaceHits = Long.parseLong(statsInfo.getOrDefault("keyspace_hits", "0"));
long keyspaceMisses = Long.parseLong(statsInfo.getOrDefault("keyspace_misses", "0"));
double hitRatio = (keyspaceHits + keyspaceMisses > 0) ?
(double) keyspaceHits / (keyspaceHits + keyspaceMisses) : 1.0;
// 填充指标对象
metrics.setUsedMemory(usedMemory);
metrics.setMaxMemory(maxMemory);
metrics.setMemoryUsageRatio(memoryUsageRatio);
metrics.setFragmentationRatio(fragmentationRatio);
metrics.setEvictedKeys(evictedKeys);
metrics.setExpiredKeys(expiredKeys);
metrics.setHitRatio(hitRatio);
metrics.setEvictionRate(evictionRate);
// 内存碎片处理
if (fragmentationRatio > 1.5) {
logger.warn("内存碎片率过高: {:.2f},触发内存碎片整理...", fragmentationRatio);
try {
// Redis 4.0+支持的内存碎片整理命令
String version = jedis.info("server").get("redis_version");
if (version.compareTo("4.0") >= 0) {
jedis.executeCommand("MEMORY PURGE");
logger.info("内存碎片整理完成");
}
} catch (Exception e) {
logger.error("内存碎片整理失败", e);
}
}
// 告警判断
if (memoryUsageRatio > 0.85) {
logger.warn("Redis内存使用率超过85%: {:.2f}%", memoryUsageRatio * 100);
alertService.sendAlert("Redis内存告警",
String.format("内存使用率: %.2f%%", memoryUsageRatio * 100));
}
if (evictionRate > 100) { // 每分钟逐出超过100次
logger.warn("Redis逐出频率过高: {}次/分钟", evictionRate);
alertService.sendAlert("逐出频率过高",
String.format("当前逐出频率: %d次/分钟", evictionRate));
}
if (hitRatio < 0.8) {
logger.warn("Redis缓存命中率较低: {:.2f}%", hitRatio * 100);
}
} catch (Exception e) {
logger.error("监控Redis健康状态失败", e);
}
return metrics;
}
// Redis健康指标类
public class RedisHealthMetrics {
private long usedMemory;
private long maxMemory;
private double memoryUsageRatio;
private double fragmentationRatio;
private long evictedKeys;
private long expiredKeys;
private double hitRatio;
private long evictionRate; // 新增逐出频率指标
// getter和setter方法省略
}
分布式环境中的逐出策略
在 Redis Cluster 环境中使用逐出策略需要特别注意:
// 集群环境监控每个节点的逐出情况
public void monitorClusterEviction() {
try (JedisCluster cluster = new JedisCluster(/* 集群配置 */)) {
Map<String, JedisPool> nodes = cluster.getClusterNodes();
Map<String, RedisHealthMetrics> nodeMetrics = new HashMap<>();
// 收集每个节点的指标
for (Map.Entry<String, JedisPool> entry : nodes.entrySet()) {
String nodeId = entry.getKey();
try (Jedis node = entry.getValue().getResource()) {
// 检查节点角色
boolean isMaster = node.info("replication")
.getOrDefault("role", "").equals("master");
if (isMaster) {
// 只监控主节点的逐出情况
Map<String, String> stats = node.info("stats");
long evictedKeys = Long.parseLong(stats.getOrDefault("evicted_keys", "0"));
// 检查是否存在节点逐出不均衡
nodeMetrics.put(nodeId, collectNodeMetrics(node));
}
}
}
// 分析节点间逐出不均衡情况
analyzeClusterEvictionBalance(nodeMetrics);
} catch (Exception e) {
logger.error("监控集群逐出情况失败", e);
}
}
// 分析集群中逐出不均衡情况
private void analyzeClusterEvictionBalance(Map<String, RedisHealthMetrics> nodeMetrics) {
// 计算平均逐出率
double avgEvictionRate = nodeMetrics.values().stream()
.mapToDouble(m -> m.getEvictionRate())
.average()
.orElse(0);
// 检查是否有节点逐出率显著高于平均值
for (Map.Entry<String, RedisHealthMetrics> entry : nodeMetrics.entrySet()) {
double nodeEvictionRate = entry.getValue().getEvictionRate();
if (nodeEvictionRate > avgEvictionRate * 2) {
logger.warn("集群节点逐出不均衡: 节点{} 逐出率是平均值的{:.2f}倍",
entry.getKey(), nodeEvictionRate / avgEvictionRate);
// 获取热点key示例
String sampleKey = "hot:key:example";
logger.warn("建议操作:" +
"1. 使用CLUSTER KEYSLOT {} 检查键分布 " +
"2. 考虑调整片键策略(如按热度分片) " +
"3. 执行CLUSTER ADJUST-RANGE进行数据迁移",
sampleKey);
}
}
}
全链路监控建议
建议结合以下工具构建全链路监控:
1. Prometheus + Redis exporter 采集基础指标
2. Grafana 绘制内存使用率、逐出率、命中率趋势图
3. 业务埋点记录缓存命中率与业务响应时间的关联关系
总结
| 策略 | 逐出范围 | 逐出规则 | 适用场景 | 性能特点 | 版本要求 |
|---|---|---|---|---|---|
| noeviction | 不逐出 | 拒绝写入 | 数据完整性要求极高 | 写入可能阻塞 | 所有版本 |
| allkeys-lru | 所有键 | 最近最少使用 | 一般缓存场景 | 均衡的性能 | 所有版本 |
| volatile-lru | 有 TTL 的键 | 最近最少使用 | 希望结合过期机制的缓存 | 依赖过期键设置 | 所有版本 |
| allkeys-random | 所有键 | 随机 | 键访问概率相近 | CPU 消耗极低 | 所有版本 |
| volatile-random | 有 TTL 的键 | 随机 | 键访问概率相近且结合过期机制 | CPU 消耗低 | 所有版本 |
| volatile-ttl | 有 TTL 的键 | 即将过期 | 优先保留寿命长的数据 | 计算开销中等 | 所有版本 |
| allkeys-lfu | 所有键 | 使用频率最少 | 数据访问频率差异大 | 高命中率但 CPU 消耗较高 | 4.0+ |
| volatile-lfu | 有 TTL 的键 | 使用频率最少 | 结合过期机制且频率差异大 | 高命中率但依赖过期键 | 4.0+ |