配运基础数据缓存瘦身实践
字数 1630 2025-08-11 08:35:53
Redis缓存瘦身实践教学文档
一、背景与问题概述
在现代物流系统中,基础数据(如商家、车队、站点、分拣中心、客户等信息)支撑着整个业务流转。为提高数据读取性能,缓存技术被广泛应用。但长期运行后,缓存中会积累大量无过期时间的"僵尸key",导致:
- 商家基础资料Redis数据量达45G
- C后台Redis数据量达132G
这些无效key占用大量内存资源,增加硬件成本,急需优化。
二、解决方案与技术选型
2.1 问题根源分析
使用@Cache注解组件时存在两个问题:
- 早期版本没有为jimdb设置默认过期时间
- 使用注解时未显式声明过期时间
导致大量key成为永久key,形成"僵尸key"。
2.2 技术方案对比
方案1:keys命令
- 优点:简单直接
- 缺点:
- 阻塞式执行,时间复杂度O(n)
- 数据量大时(几十G)会长时间阻塞Redis
- 生产环境不可接受
方案2:scan命令(最终选择)
- 优点:
- 非阻塞式执行,分批次进行
- 可控制每次返回结果的最大条数(类似SQL的LIMIT)
- 缺点:
- 返回数据可能重复
- 执行期间新增/删除的数据可能不返回
三、SCAN命令详解
3.1 基本语法
SCAN cursor [MATCH pattern] [COUNT count]
参数说明:
cursor: 游标,初始为0MATCH pattern: 匹配的key模式(支持通配符)COUNT count: 每次返回的元素数量(默认10)
3.2 使用示例
# 第一次迭代
SCAN 0 MATCH user:* COUNT 100
# 返回结果示例
1) "17" # 下一次迭代的游标
2) 1) "user:123:profile"
2) "user:456:settings"
# 后续迭代使用返回的游标
SCAN 17 MATCH user:* COUNT 100
3.3 实现流程
- 初始化游标为0
- 循环调用SCAN命令,每次使用返回的新游标
- 使用
scanResult.isFinished()判断迭代是否完成 - 对匹配的key进行处理(如删除)
四、实践中的关键问题与解决方案
4.1 常见陷阱
- 问题:返回结果集为空但游标未结束
- 原因:SCAN是按字典槽遍历,再从结果中匹配条件,可能多次迭代无匹配数据
- 解决方案:必须使用
isFinished()判断而非依赖空结果
4.2 完整Java示例代码
public void cleanExpiredKeys(String pattern) {
long cursor = 0;
do {
ScanParams scanParams = new ScanParams().match(pattern).count(100);
ScanResult<String> scanResult = jedis.scan(cursor, scanParams);
List<String> keys = scanResult.getResult();
if (!keys.isEmpty()) {
jedis.del(keys.toArray(new String[0]));
}
cursor = Long.parseLong(scanResult.getStringCursor());
} while (cursor != 0);
}
五、SCAN命令底层原理
5.1 为什么数据可能重复?
Redis使用反向二进制迭代器算法解决字典表扩容/缩容时的遍历问题:
字典表的几种状态:
- 未扩容:顺序扫描无问题
- 已扩容:
- 表大小从8→16
- 已访问的3号桶数据rehash到8~11号桶
- 顺序扫描会导致4~15号桶数据重复
- 已缩容:
- 表大小从16→8
- 8~11号桶数据rehash到0号桶
- 顺序扫描会遗漏数据
- Rehashing中:同时存在扩容和缩容问题
5.2 反向二进制迭代器工作原理
- 将游标转换为二进制
- 按照字典的sizemask(掩码),在有效位上高位加1
- 这种遍历顺序避免了扩容时的重复扫描
示例流程:
- 初始表大小8,游标0开始
- 返回游标6后,表扩容到16并完成Rehash
- 客户端发送
SCAN 6- 扫描6号桶所有数据
- 二进制高位加1计算下次游标
- 避免扫描已迁移的数据(0,4,2→8,12,10)
缩容时的重复数据:
- 表大小从16→8
- 旧表6和14号桶→新表6号桶
- SCAN 14会重新扫描新表6号桶
- 导致已扫描数据再次出现
六、优化成果
通过该方案实现了显著的缓存瘦身:
- 商家基础资料Redis:45G → 8G (减少82%)
- C后台Redis:132G → 7G (减少95%)
七、最佳实践总结
- 缓存key必须设置过期时间:避免僵尸key积累
- 使用SCAN替代KEYS:生产环境必须使用非阻塞方式
- 正确处理游标:使用
isFinished()而非空结果判断 - 批量处理:适当设置COUNT参数提高效率
- 监控机制:定期执行瘦身操作,防止key再次积累
八、扩展思考
- 对于超大规模缓存,可考虑:
- 分布式扫描方案
- 按业务维度分批处理
- 在低峰期执行瘦身操作
- 结合监控系统,设置缓存增长告警阈值
- 开发自动化工具,定期清理无过期时间的key
通过本实践,不仅解决了实际问题,还深入理解了Redis底层设计,体现了"知其然更要知其所以然"的技术追求。