应在日志写入前用 requestURI+user+timestamp 三元组内存去重,而非归档后处理;因80%重复发生在采集端,归档时多IO、易漏判;三元组需标准化字段避免序列化误差,且须在Collector层实现并统一语义定义。

requestURI + user + timestamp 三元组构造唯一键,而非全字段哈希。
为什么不能等归档时再统一去重
审计日志的重复往往集中在高频健康检查(如 /healthz)、批量操作(如同一用户连续更新 100 条配置)或代理转发日志中。这些重复项在写入阶段就已固化,归档时再处理要多走一遍磁盘 IO、解压、解析、哈希、合并,性能损耗大且容易漏掉时间窗口外的相似但不完全相同的记录(比如 timestamp 差毫秒但语义相同)。真实线上环境里,80% 的重复发生在采集端或写入缓冲区,不是存储层问题。
用三元组构造唯一键比全量序列化更稳
对审计日志这类结构化 JSON,常见错误是直接 serialize($log) 或 json_encode($log) 后哈希——字段顺序稍变、浮点数精度差异、null 与空字符串混用,都会导致哈希值不同,去重失效。正确做法是提取业务语义上真正决定“是否重复”的字段:
-
requestURI(请求路径,不含 query 参数或统一 decode 后标准化) -
user(认证主体,非 session_id 或 token) -
timestamp(截断到秒级,避免毫秒级抖动)
拼接成 $key = $log['requestURI'] . '|' . $log['user'] . '|' . floor($log['timestamp'] / 1),再存入 $seen 数组判断。这样既避开序列化陷阱,又保留审计可追溯性。
array_unique 对多维数组无效,别硬套
有人把整条日志当数组元素塞进 array_unique(),结果发现完全没去重——因为 PHP 默认按「值比较」,而数组/对象在 array_unique() 中会被转为 Array 字符串,所有日志都变成一样的字符串。即使加 SORT_REGULAR,也只比较浅层,嵌套字段一概忽略。实操中必须自己控制去重维度:
立即学习“PHP免费学习笔记(深入)”;
- 不要传整个
$logs给array_unique() - 先用
array_column($logs, 'requestURI')提取关键列,再配合array_keys()和array_unique()反查原始索引 - 或者更干脆:用
foreach遍历时,以三元组为键写入临时数组:$deduped[$key] = $log,天然覆盖重复
去重时机卡在 Collector 层最有效
PHP 审计日志通常由中间件或 SDK 采集,最佳去重点不是应用逻辑层,也不是日志轮转脚本,而是 Collector 模块(比如基于 Monolog 的自定义 Handler)。在这个环节拦截日志事件,做内存级去重,失败也不影响主流程:
- 启用
static $seen = [];缓存最近 5 分钟的三元组(注意内存泄漏,需 TTL 清理) - 对高并发场景,改用
apcu_add($key, true, 300)做分布式去重(APCu 5.1.2+ 支持 TTL) - 若日志已落盘(如
file_put_contents),则必须用流式读取 + 行缓冲,不能一次性file_get_contents加载全部再处理
真正难的是三元组设计边界:比如 user 是填用户名还是 UID?requestURI 是否包含 ?v=1.2.3 这类无意义参数?这些细节不统一会让去重率从 70% 掉到 20%,得跟安全团队对齐语义定义,而不是只调一个函数。



















