审核结果持久化:MySQL 和 Elasticsearch 各存什么
一、审核结果的两类查询模式
审核结果的数据使用方至少有两个。运营平台需要按内容 ID、审核状态、审核时间做精确的条件查询和分页,这是典型的 OLTP 场景。安全分析团队需要按违规标签、置信度分布、审核延迟做聚合统计和多维分析,这是典型的 OLAP 场景。
把两种查询压在同一套存储上,要么 OLTP 被慢查询拖慢(Elasticsearch 的聚合查询占用 heap 内存导致写入抖动),要么 OLAP 在行存上硬跑会扫全表(MySQL 对大文本字段的 GROUP BY 性能惨烈)。
基础设施不需要漂亮话。真正的问题是两类查询对存储的索引结构、一致性要求和写入模式完全不同。分别对待是最务实的做法。
二、MySQL 存什么:事务性数据和主键精确查询
MySQL 存储审核结果的权威数据。每条审核记录对应一行,字段设计围绕单条记录的 CRUD 展开。
CREATE TABLE moderation_result ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, task_id VARCHAR(64) NOT NULL COMMENT '审核任务唯一 ID', biz_id VARCHAR(64) NOT NULL COMMENT '业务方内容 ID', biz_type VARCHAR(32) NOT NULL COMMENT '业务类型:article/comment/video', review_result TINYINT NOT NULL COMMENT '审核结果:0待审 1通过 2违规 3疑似', review_label VARCHAR(128) DEFAULT '' COMMENT '违规标签列表,逗号分隔', confidence DECIMAL(5,4) DEFAULT 0 COMMENT '模型置信度 0.0000-1.0000', review_source VARCHAR(32) DEFAULT '' COMMENT '审核来源:rule_engine/nlp_model/cv_model', review_detail JSON COMMENT '审核详情:命中的敏感词、模型原始输出等', reviewer VARCHAR(64) DEFAULT '' COMMENT '人工审核员(自动审核为空)', reviewed_at DATETIME(3) NOT NULL COMMENT '审核完成时间', created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), UNIQUE KEY uk_task_id (task_id), INDEX idx_biz_id (biz_id), INDEX idx_reviewed_at (reviewed_at), INDEX idx_biz_type_result (biz_type, review_result, reviewed_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;MySQL 在这里的定位是"精确、可靠、事务一致"。为什么审核结果需要事务一致性?因为审核通过后通常伴随一次状态更新——内容从"审核中"变为"已发布"。审核结果写入和内容状态更新必须在同一个事务里完成,否则就会出现"审核已通过但内容仍不可见"的数据不一致。
func (r *ModerationRepo) SaveAndPublish(ctx context.Context, result ReviewResult) error { return r.db.Transaction(func(tx *gorm.DB) error { // 写入审核结果 if err := tx.Create(&ModerationRecord{ TaskID: result.TaskID, BizID: result.BizID, ReviewResult: result.Level, ReviewDetail: result.Detail, ReviewedAt: time.Now(), }).Error; err != nil { return fmt.Errorf("insert moderation result: %w", err) } // 更新内容状态(同事务) if result.Level == LevelPass { if err := tx.Model(&Content{}). Where("biz_id = ?", result.BizID). Update("status", "published").Error; err != nil { return fmt.Errorf("update content status: %w", err) } } return nil }) }MySQL 不擅长的是:对review_detail这样的 JSON 大字段做全文检索、按日期范围做违规标签分布聚合、以及万级以上的分页深翻。这些是 Elasticsearch 的战场。
三、Elasticsearch 存什么:全文搜索和多维聚合
ES 存储的是 MySQL 审核数据的搜索副本。它的数据与 MySQL 同步(通过 Kafka 管道),但索引结构完全不同——按全文检索和聚合查询的需求建立倒排索引。
ES 的索引 Mapping 设计围绕两个核心操作:按违规标签和审核来源做 terms 聚合、按审核延迟做 histogram 聚合、以及在审核详情 JSON 中做全文匹配。
{ "mappings": { "properties": { "task_id": { "type": "keyword" }, "biz_id": { "type": "keyword" }, "biz_type": { "type": "keyword" }, "review_result": { "type": "byte" }, "review_label": { "type": "keyword" }, "confidence": { "type": "scaled_float", "scaling_factor": 10000 }, "review_source": { "type": "keyword" }, "review_detail": { "type": "text", "fields": { "keyword": { "type": "keyword", "ignore_above": 512 } } }, "reviewed_at": { "type": "date", "format": "yyyy-MM-dd HH:mm:ss.SSS" }, "review_latency_ms": { "type": "integer" }, "biz_create_date": { "type": "date", "format": "yyyy-MM-dd" } } } }ES 不建议做权威数据存储。原因有两个:一是 ES 的写入不是立即可见(refresh_interval 默认 1s),在"审核完成 → 内容发布"这个同步链路里引入 1s 延迟不划算;二是 ES 没有事务概念,如果 Kafka 管道出现消息乱序或重复,ES 的数据需要额外的去重逻辑。MySQL 的主键唯一约束天然防止了重复写入,而 ES 的_id去重依赖外部保证。
一个容易被忽略但实际影响很大的坑是 ES 的聚合精度。ES 的 terms 聚合默认有shard_size和size两个参数,如果size设为 10 但只取 top 10 标签,聚合结果在分片环境下存在误差。对于安全分析来说,低频但高危的违规标签被聚合误差吞掉是不可接受的。解决方案是对关键聚合设置"shard_size": 10000并关闭"show_term_doc_count_error"。
四、双写一致性与数据同步策略
MySQL 和 ES 之间的数据同步是双写架构的核心难题。方案有两种。
应用层双写:审核 Worker 在写 MySQL 事务提交后,同步或异步写入 ES。问题在于 MySQL 写入成功但 ES 写入失败时,两者的数据就不一致了。补救措施是 Worker 写入时增加一个同步状态标记,定期扫描 MySQL 中"未同步至 ES"的记录做补偿投递。
CDC 管道同步:使用 Canal 或 Debezium 监听 MySQL binlog,将审核表的变更事件投递到 Kafka,由独立消费者写入 ES。这个方案的解耦度更高,Worker 不需要感知 ES 的存在。代价是引入了一条新的数据管道,binlog 解析、Kafka 投递、ES 写入三层都得做监控和告警。
推荐走 CDC 管道。审核 Worker 的核心职责是生产审核结果,不应该为 ES 写入失败做额外的心智负担。管道的可靠性由基础设施团队统一保障。
五、总结
审核结果持久化的存储分工:
- MySQL 存权威数据:支持事务一致性(审核结果 + 内容状态同步更新),精确的主键和索引查询,满足运营平台的 CRUD 需求。
- Elasticsearch 存搜索副本:支持全文检索、多维聚合和时序分析,满足安全分析和监控大盘的查询需求。
- CDC 管道同步:通过 binlog → Kafka → ES 的方式做 MySQL 到 ES 的数据同步,Worker 不与 ES 直接耦合。
- 注意 ES 聚合精度:对安全分析类的 terms 聚合要显式设置
shard_size,避免低频高危标签被精度损失吞掉。
不要试图在 MySQL 上做聚合分析,也不要把 ES 当权威存储。各自做各自擅长的,通过管道保持最终一致,这是生产环境最务实的方案。