Java 8 多模块 Redis 监控与分析平台。在线监控 Redis 运行状态,离线分析 MONITOR 日志,并按 key 模式 给出内存增量归因报告。
MemoryIncrementAnalyzer 是一个独立的命令行工具,用于在一段时间内观测 Redis 写入流量,并按 key 模式 排名给出内存增量归因。支持两种数据源:
live模式:连接到 Redis,订阅MONITOR流,并发起MEMORY USAGE采样file模式:扫描指定目录下的.log文件(离线MONITOR日志),无需连接 Redis
# 1) 通过 fat-jar 运行
java -jar redis-monitor-analyzer/target/redis-monitor-analyzer-1.0.0-SNAPSHOT.jar [options]
# 2) 通过 Maven exec 插件运行
mvn -pl redis-monitor-analyzer exec:java -Dexec.args="--host=127.0.0.1 --port=6379 --duration=300"按 Ctrl-C 可提前结束并打印当前累计的报告(通过 JVM ShutdownHook 触发)。
| 参数 | 默认值 | 适用模式 | 说明 |
|---|---|---|---|
--source=<live|file> |
live |
共用 | 数据源:在线监控 / 离线日志解析 |
--host=<host> |
localhost |
live | Redis 主机 |
--port=<port> |
6379 |
live | Redis 端口 |
--password=<password> |
无 | live | Redis 密码(可选) |
--duration=<seconds> |
300 |
live | 监控时长(秒) |
--input-dir=<path> |
无 | file | .log 文件目录,source=file 时必填 |
--samples-per-pattern=<n> |
10 |
live | 每个模式的 MEMORY USAGE 采样上限 |
--ttl-samples-per-pattern=<n> |
5 |
共用 | 每个模式的 TTL 采样上限 |
--upgrade-threshold=<n> |
10 |
共用 | 段升级阈值:相同位置出现 N 个不同值后升级为通配符 * |
--top-n=<n> |
20 |
共用 | 报告输出 Top N |
--output=<console|json> |
console |
共用 | 输出格式 |
--print-interval=<seconds> |
30 |
共用 | 中间报告打印周期,0 关闭 |
参数格式必须为 --key=value,无值或非法值会立刻报错并打印 usage。
# 在线监控本地 Redis 5 分钟,输出 Top 30
java -jar redis-monitor-analyzer-*.jar \
--host=127.0.0.1 --port=6379 \
--duration=300 --top-n=30
# 在线监控带密码的 Redis,并以 JSON 输出,每分钟打一次中间报告
java -jar redis-monitor-analyzer-*.jar \
--host=10.0.0.10 --port=6380 --password=secret \
--duration=900 \
--output=json --print-interval=60
# 离线分析整批 MONITOR 日志
java -jar redis-monitor-analyzer-*.jar \
--source=file \
--input-dir=/data/redis_logs/batch_20260427_143001 \
--top-n=50控制台模式下会打印:
- Top N 模式表:每个模式的
Writes / Write/s / AvgTTL / AvgMem / Increment / Balanced *兜底桶的样例 key:用于排查未被聚类的异常 key- No-TTL 样例:识别到的"无过期、持续增长"的代表性 key(最多 5 条)
JSON 模式产出与上述等价的结构化结果,可直接喂给后续分析管道。
Rank Pattern Writes Write/s AvgTTL AvgMem Increment Balanced
------+--------------------------------+--------+----------+----------+------------+------------+------------
1 BATCH_RANDOM_TOP_ITEMS_* 3039 0.11 2.0h 22.95 KB 68.12 MB 17.36 MB
2 terminal_id_send_count_* 70204 2.44 720.0h 185 B 12.43 MB 1.09 GB
3 ITEMS_COLLECT_MEMBER_DATA_* 5735 0.20 1.0h 1.77 KB 9.92 MB 1.24 MB
4 CMS_RANDOM_ID_HISTORY 9093 0.32 240.0h 424 B 3.68 MB 110.36 MB
5 AUTH_LOGIN_TOKEN_* 124270 4.31 720.0h 30 B 3.61 MB 325.09 MB
6 SPREAD_ACTIVITY_FLAG_* 17898 0.62 168.0h 195 B 3.33 MB 69.88 MB
7 LATEST_BROWSE_KEY_* 17912 0.62 168.0h 192 B 3.29 MB 69.08 MB
8 REVIEW_NOTIFY_* 85893 2.98 24.0h 26 B 2.13 MB 6.38 MB
9 SELLER_STORE_DATA_*_* 7370 0.26 24.4h 155 B 1.09 MB 3.32 MB
10 portal:member:basket:total:meta:* 1110 0.04 1.0h 904 B 980.79 KB 122.51 KB
11 SPREAD_AB_EXPERIMENT_KEY_V2_* 3555 0.12 5m 249 B 865.49 KB 9.72 KB
12 *:* 863 0.03 57.7h 517 B 436.13 KB 3.07 MB
13 i18n_v2_*_en_* 2458 0.09 288.0h 140 B 337.73 KB 11.87 MB
14 NEW_HOME_CATALOG_* 192 0.01 4.0h 640 B 120.04 KB 60.23 KB
15 REDIS_GW_FETCH_HOME_CATALOG… 18 0.00 24.0h 5.83 KB 104.98 KB 314.71 KB
16 MODAL_V1_20260427_*_1 535 0.02 24.0h 196 B 102.40 KB 306.99 KB
17 member:labels:* 395 0.01 12.0h 237 B 91.61 KB 137.32 KB
18 member_signin_auto_get_voucher… 456 0.02 24.0h 186 B 82.83 KB 248.31 KB
19 SPREAD_WIDGET_LIST_V10… 13 0.00 1.0h 4.16 KB 54.04 KB 6.86 KB
20 SPREAD_WIDGET_LIST_V10… 9 0.00 1.0h 4.25 KB 38.21 KB 4.83 KB
------+--------------------------------+--------+----------+----------+------------+------------+------------
TOTAL 358671 110.75 MB 1.70 GB
incrementBytes 是首要排序指标,balanced ref 是参考值,二者解决的是不同问题。
每个模式独立维护以下统计:
writeCount // 监控窗口内的写入次数
writeRatePerSecond = writeCount / durationSec
avgMemoryBytes = mean(memorySamples) // 蓄水池采样
avgTtlSeconds = mean(ttlSamples) / 1000.0 // 蓄水池采样
incrementBytes = writeCount * avgMemoryBytes
balancedBytes = writeRatePerSecond * avgTtlSeconds * avgMemoryBytes
incrementBytes(主指标):监控期间该模式 新写入 的总字节量,用来回答"窗口内谁在涨内存"。balancedBytes(参考值):在写入与过期速率均衡的稳态下,该模式驻留的内存量;只在该模式存在 TTL 样本时计算,否则留空。可用来对比"短期增量"与"长期占用"。
假设:同一模式下所有 key 共享 TTL;过期速率 ≈ 写入速率。这两条假设决定了
balancedBytes是 估计,不是测量。
| 模式 | 来源 | 备注 |
|---|---|---|
live |
MEMORY USAGE key |
由独立后台线程异步采集,samplesPerPattern 控制配额,避免给 Redis 增加负担 |
file |
命令值参数的字符串长度 | 离线日志拿不到 RSS,使用值长度作为 相对 排序代理;不是真实字节,但能在不同模式之间保留排名一致性 |
- 直接抽取:
SETEX/PSETEX/SET ... EX|PX|EXAT|PXAT/EXPIRE系列等内联 TTL 命令直接读出 TTL,并把模式标记为hasTtlFromCommand。 - 延迟查询:
SET / HSET / SADD这类裸写命令,将 (key, pattern) 入队,约 1s 后再发TTL key。延迟的目的是给客户端"SET+EXPIREpipeline"留出落地时间,避免误把瞬时 -1 算成"无 TTL"。
延迟通道在以下情况主动丢弃任务:
- 该模式 TTL 配额已满
- 该模式已通过直接抽取拿到 TTL(避免 -1 污染已知 TTL)
如果一个模式的 TTL 平均值 ≤ 0,则视为"无过期"模式:不计算 balancedBytes,并把代表 key 写入 NoTtlKeyStore(环形 5 条),单独列出"持续增长"风险点。
TTL 与 Memory 都使用固定容量的蓄水池采样:
- 容量未满时直接追加
- 容量已满时以
capacity / totalSeen的概率随机替换 - 数学上保证窗口内任意一次写入被采到的概率是均等的
这样:
- 单模式查询 Redis 的次数有上限(
samplesPerPattern× 模式数) - 不需要存全量 key,内存占用恒定
- 长尾 key 也有公平的入选机会
目标:把 user:1001:profile、user:1002:profile、user:abc:profile 等成千上万的 key 自动归并为同一个 模式 user:*:profile,作为后续统计的基本单位。
实现采用 两层策略:
- 分隔符分段聚类(针对带
:或_的常规 key) - 前缀字典树(Prefix Trie)聚类(针对无分隔符的 key)
并通过一个 LRU keyToPattern 缓存(默认 10,000 条)短路重复 key 的聚类计算。
对 user:1001:profile:
-
自动检测分隔符:优先
:,其次_ -
按分隔符切段:
["user", "1001", "profile"] -
每个段被分类为:
类型 正则 示例 NUMBER^\d+$1001UUID^[0-9a-fA-F]{8}-?...{12}$e8a1...DATE^\d{4}[-/]?\d{2}[-/]?\d{2}$20260427HEX^[0-9a-fA-F]{16,}$deadbeef...STRING/FIXED兜底 profile检测顺序:UUID → DATE → NUMBER → HEX → FIXED。
DATE在NUMBER之前是为了让20260427被正确识别为日期而非纯数字。 -
已有模式中按 相同段数 + 相同分隔符 寻找候选:
- Exact match:每个
FIXED位等值,每个变量位匹配类型正则 → 计数 +1 - Near match:变量位全部匹配,只有 一个
FIXED位不等 → 在该位置记录新的 distinct 值 - 未命中:以全
FIXED段创建新模式
- Exact match:每个
-
升级(upgrade)规则:当某个
FIXED位收集到UPGRADE_THRESHOLD个不同值(默认 10),将其升级为变量段。升级后立即mergeDuplicatePatterns()合并因升级而重名的模式(例如user:1001:profile和user:1002:profile升级后都变成user:*:profile)。
这样处理可以做到:常见前缀(如
user)保持具体,业务变化部位(如 user id)自动收敛为*,避免 key 爆炸。
对 userprofile1001、userprofile1002... 这类不带分隔符的 key,用字符级 Prefix Trie 聚类。不用 LCS(最长公共子串),因为 LCS 可能匹配到任意中间子串,对 Redis key 没有业务语义;前缀树天然捕获"业务前缀"(如 userprofile_),更符合 Redis 命名习惯,且插入与抽取都是 O(key length)。
插入:将每个 key 按字符插入 Trie,路径上每个节点 count++。重复 key 只更新计数不重复建节点。
模式抽取(在打报告时执行):DFS 自根开始,对每个分支判断"切点":
- 分叉点:当前节点有 ≥ 2 个
count ≥ threshold的子节点 → 切,输出<前缀>* - 链尾:当前节点
count ≥ threshold,但所有子节点都 < threshold → 切 - 否则递归进入唯一仍超阈值的子节点
DFS 中那些无法被任何模式覆盖的 key(subtree count < threshold)统一并入兜底模式 *。
字典树最坏情况下会爆炸式生长,因此设置三道防护墙,触发任一即放弃 Trie 路径,把该 key 直接归入 *:
| 防护 | 阈值 | 防的是什么 |
|---|---|---|
| Key 长度 | 200 字符 | 形态异常(如序列化 JSON 当 key) |
| 唯一 key 数 | 100,000 | 全是 UUID 这种几乎不共享前缀的 key |
| Trie 节点数 | 500,000 | 对抗性 key 形状(共享前缀但分叉极多) |
整个 Trie 的内存上限约 110MB,对一个 CLI 工具完全可控。
以下场景的 key 会被放入 * 模式:
- key 反序列化全部失败(hex 兜底)
- 字典树被防护墙拒收
- 字典树中未达到阈值、无法独立成簇的散点
* 模式额外保留最多 10 条样例 key 与单独显示,便于排查"分布太散"的写入流量。
# 全量
mvn test
# 单模块
mvn test -pl redis-monitor-analyzer
# 单测试类
mvn test -pl redis-monitor-analyzer -Dtest=PatternClustererTestredis-monitor-analyzer 中有针对每个组件的单元测试,以及端到端集成测试 MemoryIncrementAnalyzerIntegrationTest(需要可用的 Redis 实例,主机/端口在测试源码顶部配置)。
- 包名:
com.yj.redis.monitor.{module-name} - 主入口:
redis-monitor-web/src/main/java/com/yj/redis/monitor/web/RedisMonitorApplication.java - 配置:
redis-monitor-web/src/main/resources/application.yml,前缀redis.monitor - 设计文档:
docs/superpowers/specs/、docs/superpowers/plans/