配运基础数据缓存瘦身实践
字数 1630 2025-08-11 08:35:53

Redis缓存瘦身实践教学文档

一、背景与问题概述

在现代物流系统中,基础数据(如商家、车队、站点、分拣中心、客户等信息)支撑着整个业务流转。为提高数据读取性能,缓存技术被广泛应用。但长期运行后,缓存中会积累大量无过期时间的"僵尸key",导致:

  • 商家基础资料Redis数据量达45G
  • C后台Redis数据量达132G

这些无效key占用大量内存资源,增加硬件成本,急需优化。

二、解决方案与技术选型

2.1 问题根源分析

使用@Cache注解组件时存在两个问题:

  1. 早期版本没有为jimdb设置默认过期时间
  2. 使用注解时未显式声明过期时间

导致大量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: 游标,初始为0
  • MATCH 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 实现流程

  1. 初始化游标为0
  2. 循环调用SCAN命令,每次使用返回的新游标
  3. 使用scanResult.isFinished()判断迭代是否完成
  4. 对匹配的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使用反向二进制迭代器算法解决字典表扩容/缩容时的遍历问题:

字典表的几种状态:

  1. 未扩容:顺序扫描无问题
  2. 已扩容
    • 表大小从8→16
    • 已访问的3号桶数据rehash到8~11号桶
    • 顺序扫描会导致4~15号桶数据重复
  3. 已缩容
    • 表大小从16→8
    • 8~11号桶数据rehash到0号桶
    • 顺序扫描会遗漏数据
  4. Rehashing中:同时存在扩容和缩容问题

5.2 反向二进制迭代器工作原理

  1. 将游标转换为二进制
  2. 按照字典的sizemask(掩码),在有效位上高位加1
  3. 这种遍历顺序避免了扩容时的重复扫描

示例流程:

  1. 初始表大小8,游标0开始
  2. 返回游标6后,表扩容到16并完成Rehash
  3. 客户端发送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%)

七、最佳实践总结

  1. 缓存key必须设置过期时间:避免僵尸key积累
  2. 使用SCAN替代KEYS:生产环境必须使用非阻塞方式
  3. 正确处理游标:使用isFinished()而非空结果判断
  4. 批量处理:适当设置COUNT参数提高效率
  5. 监控机制:定期执行瘦身操作,防止key再次积累

八、扩展思考

  1. 对于超大规模缓存,可考虑:
    • 分布式扫描方案
    • 按业务维度分批处理
    • 在低峰期执行瘦身操作
  2. 结合监控系统,设置缓存增长告警阈值
  3. 开发自动化工具,定期清理无过期时间的key

通过本实践,不仅解决了实际问题,还深入理解了Redis底层设计,体现了"知其然更要知其所以然"的技术追求。

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 基本语法 参数说明: cursor : 游标,初始为0 MATCH pattern : 匹配的key模式(支持通配符) COUNT count : 每次返回的元素数量(默认10) 3.2 使用示例 3.3 实现流程 初始化游标为0 循环调用SCAN命令,每次使用返回的新游标 使用 scanResult.isFinished() 判断迭代是否完成 对匹配的key进行处理(如删除) 四、实践中的关键问题与解决方案 4.1 常见陷阱 问题 :返回结果集为空但游标未结束 原因 :SCAN是按字典槽遍历,再从结果中匹配条件,可能多次迭代无匹配数据 解决方案 :必须使用 isFinished() 判断而非依赖空结果 4.2 完整Java示例代码 五、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底层设计,体现了"知其然更要知其所以然"的技术追求。