第3章 审计数据模型设计:结构化与半结构化
说到审计数据模型,我先问一个问题:你手头的审计数据,是规整的表格,还是乱七八糟的日志?我干了这么多年审计接口,见过太多团队在这个问题上踩坑。
结构化数据的优势很明显——查得快、算得准。但审计场景下,会面临大量临时字段、异构系统的异常记录,纯结构化根本兜不住。那怎么办?我的答案是:两者都要,但要分清楚什么时候用谁。
3.1 结构化数据模型:审计系统的骨架
结构化的核心是“定死了”。每一列是什么类型,不能乱填。我最早设计审计表时,犯过一个错:把所有字段都设成 varchar(255)。后来被业务方追着骂,“你这时间字段怎么排序?”“金额字段怎么汇总?”——嗯,那时候我就明白了,类型约束不是摆设。
你想想看,审计数据模型里,结构化部分必须包含哪几样?我给你列个清单:
| 模型名称 | 核心字段 | 用途示例 |
|---|---|---|
| 审计事件模型 | event_id, event_type, user_id, action, resource, timestamp | 记录“谁在什么时间对什么资源做了什么” |
| 审计日志模型 | log_id, session_id, ip_address, user_agent, status, duration | 保存每一次请求的原始日志信息 |
| 审计结果模型 | result_id, rule_id, match_result, risk_level, alert_time | 存放审计规则匹配后的判定结果 |
我个人习惯是:只要字段在90%以上的记录里都会出现,就把它放进结构化表里。剩下的,交给半结构化处理。
举个具体例子。一个标准的审计事件表,DDL大概长这样:
CREATE TABLE audit_event (
event_id BIGINT PRIMARY KEY AUTO_INCREMENT,
event_type VARCHAR(64) NOT NULL COMMENT '事件类型:LOGIN/LOGOUT/DATA_ACCESS/...',
user_id VARCHAR(128) NOT NULL,
action VARCHAR(32) NOT NULL COMMENT 'CREATE/READ/UPDATE/DELETE',
resource VARCHAR(256) NOT NULL COMMENT '被操作的资源URI或ID',
timestamp DATETIME(6) NOT NULL,
source_ip VARCHAR(45) DEFAULT NULL COMMENT 'IPv4或IPv6',
status TINYINT DEFAULT 0 COMMENT '0-成功 1-失败 2-异常',
INDEX idx_type_time (event_type, timestamp)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
注意那个 DATETIME(6),为什么是6?审计记录往往在同一秒内并发产生,精确到微秒才能保证不乱序。我在项目中遇到过因为时间精度不够导致日志重排的破事,后来统一改成了微秒级。
3.2 半结构化数据模型:灵活兜底的好兄弟
讲完了骨架,来说血肉。半结构化数据,说白了就是那些“没法完全按表格填”的东西。比如:
- 用户自定义的审计扩展字段
- 不同系统传过来的参差不齐的请求头
- 审计上下文中的业务数据快照
- 第三方系统的原始报文
这些东西如果强行塞进结构化字段,要么是加一堆NULL列,要么是搞各种“扩展字段1、扩展字段2”这种弱智设计。我曾经接手过一个系统,扩展字段从1排到20,文档全丢,没人知道哪一列存什么——噩梦。
所以我的方案是:在结构化表上加一个 ext_info JSON 字段。像这样:
CREATE TABLE audit_event (
-- ...结构化字段同上...
ext_info JSON DEFAULT NULL COMMENT '半结构化扩展信息'
);
JSON 在 MySQL 5.7+ 里已经是原生类型了,不再是你想象的那个又慢又难查的文本。实际上,MySQL 8.0 的 JSON 索引已经能支持路径查询,性能完全够用。
我建议:把所有“可能变”的字段都放到 ext_info 里。比如 A 系统传了 user_agent,B 系统没传,没关系。JSON 天然支持稀疏字段,不会浪费空间。
举个例子,一条审计记录的 JSON 扩展信息可能是这样的:
{
"request_id": "req-20250321001",
"user_agent": "Mozilla/5.0 ...",
"device_fingerprint": "fp_a1b2c3",
"original_payload": {"action": "export", "count": 500},
"tags": ["high_risk", "bulk_export"]
}
你看,这里面有些字段是针对特定场景的。如果把这些都做成结构化列,明天的运维日志系统传个新字段,你又得改表结构。结构化稳,但不改;半结构化灵活,但不好查。两者互补才是正道。
3.3 字段规范:别让数据变成野马
模型确定了,字段怎么命名、怎么规定类型,这也是个大坑。我见过太多团队,自己写的审计表用 user_name,对方系统传过来的是 usrNm,代码里再转一次 userName——每次对接都能吵一架。
我的建议是定死三条规则:
- 全小写下划线命名。比如
user_ip而不是userIP或UserIp。为什么?因为不同语言的命名规范不一样,小写下划线是最大公约数。 - 字段类型明确且最小化。能用 INT 别用 BIGINT,能用 VARCHAR(64) 别上来就 255。我见过一张审计表,log_id 用 VARCHAR(500),后来查了才知道里面最多存 20 个字符。
- 统一时间精度。所有跟时间相关的字段,统一用
DATETIME(6)或TIMESTAMP(6),千万别混用。
注意了:不要用 TEXT 类型存储 JSON 字段。JSON 类型的校验、压缩、路径查询能力,TEXT 全都用不了。我就曾经因为偷懒用 TEXT 存 JSON,导致后来做审计查询时被性能问题折磨了一周。
对了,还有一个字段长度的坑。比如 source_ip,IPv4 最长 15 个字符,但 IPv6 是 45 个。我建议直接用 VARCHAR(45),省得到时候改表结构。
3.4 核心逻辑:结构化 + 半结构化双模设计
讲了这么多,核心思想其实就是一张图能说清楚的事。下面这张图展示了审计数据从产生到入库的完整链路,以及结构化与半结构化是如何分工协作的:
我习惯的做法是:先画出这张图,然后对着图一张一张表去落地。别上来就写 DDL,先用图把流程走通。结构化和半结构化的分界线,就是通过这张图来定的。
实际应用的时候,还有一个常被忽略的点:半结构化的字段一样要做校验。我在 ext_info 里存过用户设备指纹,后来发现有的系统传进来的是 null,有的传空字符串,有的传 "null" 字符串——查数据的时候想死的心都有。所以建议在应用层或者存储过程里,对 JSON 里的关键字段做规范性检查,哪怕是半结构化的,也不能放任自流。