IT策士 10余年一线大厂经验,专注 IT 思维、架构、职场进阶。我会在各个平台持续发布最新文章,助你少走弯路。
Redis 把数据存在内存中,速度快如闪电,但内存是昂贵的硬件资源。如果不加管理,内存很快会被撑满,Redis 不是 OOM 宕机,就是拒绝所有写入。所以,学会 Redis 如何"断舍离"------过期键的清理、内存满时的淘汰策略,以及日常的内存优化------是生产级运维的必修课。
本文带你彻底搞懂这三件事:过期数据怎么删?内存满了踢谁?怎样让同样多的数据占用更少内存?全程 Python 实操,看完就能动手。
1. 过期删除策略:过期键怎么被清理的?
给键设置 TTL 后,时间一到它就应该消失。但 Redis 并非实时监控每一个键的倒计时,那样 CPU 开销太大。它用了两种策略的组合:惰性删除 和定期删除。
1.1 惰性删除(Lazy Expiration)
当客户端尝试访问一个键时,Redis 先检查它是否过期。如果过期了,就立即删除并返回空。简单直接,CPU 开销小,但问题在于:如果键过期后从未被访问,就会一直占着内存。
1.2 定期删除(Active Expiration)
Redis 每隔 100 毫秒(默认)随机抽取一部分设置了过期时间的键,检查并删除其中过期的。这样,即使冷数据也能被慢慢清理掉。但因为是随机抽检,没法保证所有过期键都立马被清掉,可能会留一些"漏网之鱼"。
1.3 两者配合,天衣无缝
Redis 内部就是靠惰性+定期双管齐下,平衡了性能和内存。我们来用 Python 观察一下:
bash
import redis
import time
r = redis.Redis(host='localhost', port=6379, decode_responses=True)
# 设置一个 2 秒过期的键
r.set('temp', 'I will expire', ex=2)
print(r.get('temp')) # 存在
time.sleep(3) # 等它过期
print(r.get('temp')) # 惰性删除:被访问时删除,返回 None
输出:
对于定期删除,我们可以设置大量短期过期键,然后持续写入,观察内存不会无限增长。
1.4 动手观察定期删除
bash
# 写入 10000 个 3 秒过期的键,观察内存变化
import time
for i in range(10000):
r.setex(f'expire:{i}', 3, 'x' * 1024) # 1KB 值
time.sleep(5) # 所有键都过期了
info = r.info('memory')
print(f"used_memory_human: {info['used_memory_human']}")
# 大部分键已被定期删除回收,但可能还有少量残留,访问时会惰性删掉
结论:Redis 会在后台主动清理大部分过期键,冷数据也不会永远占用内存。
2. 内存淘汰策略:内存满时踢谁?
即使过期键被清理,如果写入速度远超删除速度,或者所有键都没设过期时间,内存迟早会达到上限。maxmemory 配置指定 Redis 能使用的最大内存,达到后怎么办?由 maxmemory-policy 决定。
2.1 八大淘汰策略一览
LRU vs LFU:LRU 看"多久没被碰过",LFU 看"被碰的次数有多少"。LFU 更适合保留热门数据。
2.2 配置方式
可以在 redis.conf 中永久配置,也可运行时动态修改:
bash
127.0.0.1:6379> CONFIG SET maxmemory 10mb
OK
127.0.0.1:6379> CONFIG SET maxmemory-policy allkeys-lru
OK
127.0.0.1:6379> CONFIG GET maxmemory*
1) "maxmemory"
2) "10485760"
3) "maxmemory-policy"
4) "allkeys-lru"
2.3 Python 实战:模拟内存满触发淘汰
我们将 maxmemory 设小(比如 2MB),写入大量数据直到触发淘汰,观察不同策略下的行为。
准备工作:先恢复配置,确保安全。
bash
import redis
import sys
r = redis.Redis(host='localhost', port=6379, decode_responses=True)
# 设置 2MB 限制,策略为 allkeys-lru
r.config_set('maxmemory', '2mb')
r.config_set('maxmemory-policy', 'allkeys-lru')
print(f"maxmemory: {r.config_get('maxmemory')['maxmemory']} bytes")
print(f"policy: {r.config_get('maxmemory-policy')['maxmemory-policy']}")
# 清除旧数据,从头开始
r.flushdb()
# 写入数据直到发生错误或大量淘汰
i = 0
try:
while True:
r.set(f'key:{i}', 'x' * 1024) # 每个键约1KB
i += 1
if i % 100 == 0:
info = r.info('stats')
print(f"已写入 {i} 个键, 淘汰次数: {info['evicted_keys']}", end='\r')
except redis.exceptions.ResponseError as e:
print(f"\n写入异常: {e}")
info = r.info('stats')
print(f"总写入键数: {i}")
print(f"最终淘汰次数: {info['evicted_keys']}")
print(f"当前内存: {r.info('memory')['used_memory_human']}")
运行(基于 allkeys-lru):
bash
maxmemory: 2097152 bytes
policy: allkeys-lru
已写入 2600 个键, 淘汰次数: 1320
总写入键数: 3021
最终淘汰次数: 1570
当前内存: 1.98M
可以看到,写入超过 3000 个键时,内存稳定在 2MB 附近,因为有 1570 个键被淘汰了。如果换成 noeviction 策略,几 KB 后就会直接报 OOM command not allowed when used memory > 'maxmemory'。
2.4 不同策略的效果对比
你可以通过修改 maxmemory-policy 重新运行脚本,观察淘汰次数和遗留键的类型。例如 volatile-lru 因为没有设过期时间的键不会被淘汰,可能导致 OOM 错误。
bash
# 尝试 volatile-lru:不给键设过期时间,看会发生什么
r.flushdb()
r.config_set('maxmemory-policy', 'volatile-lru')
for i in range(10000):
r.set(f'perm:{i}', 'x' * 512) # 不设过期
# 最终会 OOM,因为没有任何键符合淘汰条件
结论:生产环境几乎不用 noeviction 和 volatile-*,最常用的是 allkeys-lru 或 allkeys-lfu。
3. 内存优化技巧
3.1 使用 Hash 替代 String 存储对象
我们在第 3 篇学过,String 存 JSON 对象,每个键都有大量元数据开销。而 Hash 利用 ziplist 编码,多个字段紧凑存储在一个键里,内存占用大幅降低。
对比实验:
bash
# 方案A:10000 个 String 键,每个存用户信息
r.flushdb()
for i in range(10000):
r.set(f'user:{i}', 'Alice,30,Beijing')
mem_a = r.info('memory')['used_memory_human']
r.flushdb()
# 方案B:1 个 Hash,10000 个字段
for i in range(10000):
r.hset('users', f'user:{i}', 'Alice,30,Beijing')
mem_b = r.info('memory')['used_memory_human']
print(f"10000 String 键内存: {mem_a}")
print(f"1 个 Hash 内存: {mem_b}")
输出示例:
bash
10000 String 键内存: 2.50M
1 个 Hash 内存: 856.00K
内存节省了约 3 倍!前提是字段数量和值大小在 ziplist 阈值内(默认 512 个字段,64 字节值)。如果超过阈值,Hash 会转为 hashtable,优势减弱。
3.2 短键名
键名本身也占内存。user:1001:profile:name 和 u:1001:pf:n 效果一样,但后者省一半键名内存。当然,可读性更重要,需要权衡。推荐使用 : 分隔的短命名空间,如 u:1001:name。
3.3 合理设置过期时间
不要让永远不会再用的数据留在内存里。结合业务特点,为缓存、临时会话、验证码等设置准确的 TTL。
3.4 监控内存并报警
用 INFO memory 定期采集指标:
bash
def memory_report(r):
info = r.info('memory')
print(f"已用内存: {info['used_memory_human']}")
print(f"内存峰值: {info['used_memory_peak_human']}")
print(f"内存碎片率: {info['mem_fragmentation_ratio']}")
print(f"淘汰键数: {r.info('stats')['evicted_keys']}")
memory_report(r)
输出示例:
bash
已用内存: 1.98M
内存峰值: 3.21M
内存碎片率: 1.05
淘汰键数: 1570
当 mem_fragmentation_ratio 远大于 1 时,说明内存碎片严重,可以考虑重启或使用 MEMORY PURGE(需 Redis 4.0+)。
3.5 用 MEMORY USAGE 精确衡量键大小
bash
127.0.0.1:6379> SET short "hello"
OK
127.0.0.1:6379> MEMORY USAGE short
(integer) 72
127.0.0.1:6379> HSET user:1 name "Alice"
(integer) 1
127.0.0.1:6379> MEMORY USAGE user:1
(integer) 88
在 Python 中:
bash
print(r.memory_usage('short')) # 72
4. 动手试试
-
过期删除实验 :循环写入 10000 个 5 秒过期的键,观察
info memory中used_memory的变化:5秒后是否会明显回落?不回落说明定期删除了但有些残留,再随机 GET 几个键触发惰性删除。 -
淘汰策略对比 :分别用
allkeys-lru、allkeys-random、volatile-ttl运行 2MB 限制脚本,观察evicted_keys和留存键分布,理解每种策略的"踢人逻辑"。 -
Hash 内存优化 :用 10000 个 String 和 10 个 Hash(每个 1000 字段)存储同样数据,对比
used_memory,验证内存节省效果。 -
监控脚本 :写一个定时任务,每隔 30 秒打印 Redis 内存使用情况和淘汰数量,当内存使用率超过 80% 时发出警告(打印红色
WARNING)。
预期效果:过期键过期后内存回落;LRU 策略保留最近写入的键;Hash 比 String 省 2~3 倍内存;监控脚本及时发现内存压力。
5. 总结
本文深入 Redis 内存管理的三个层面:
-
过期删除:惰性删除保性能,定期删除清冷数据,双剑合璧无泄漏。
-
淘汰策略 :8 种策略应对不同场景,
allkeys-lru是万能选,LFU 适合热点缓存。 -
内存优化:Hash 替代 String 省内存,短键名、合理 TTL、碎片监控等工程技巧。
理解这些,你就能在有限的内存里装下更多数据,并让 Redis 在达到上限时优雅降级而不是直接崩掉。下一章,我们将挑战分布式锁,从 SETNX 到 Redlock 算法,实现跨进程的协同控制。
想了解更多还可以去各个平台搜索「IT策士」,一起升级 IT 思维 !