审核结果持久化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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;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 当权威存储。各自做各自擅长的通过管道保持最终一致这是生产环境最务实的方案。