趣享社项目实战:同步 + 异步双模式内容审核与 RabbitMQ 可靠投递深度解析

对于 UGC 社区平台而言,内容安全是生死底线 。趣享社针对社区内容风控场景,落地了一套兼顾用户体验、系统性能、内容安全、运维可控 的双模式审核方案:同步阻塞审核满足信任用户 / 创作者 即时发布、卡点上线需求;异步 MQ 审核保障普通用户 发布无感知、无卡顿,同时审核期间笔记默认隐藏,从根源杜绝违规内容短暂曝光风险。同时围绕 RabbitMQ 实现完整的事务一致性、可靠投递、消费幂等、死信兜底、异常隔离机制,彻底解决内容审核场景下的消息丢失、重复消费、事务不一致、有毒消息阻塞等生产级问题。

本文将从架构设计、用户分层体系、业务流程、事务一致性、MQ 核心配置、消费幂等、死信队列、异常兜底、高并发优化、踩坑实录全维度拆解,完整还原生产级内容审核系统的落地细节。

一、项目核心概述

UGC 内容平台的内容审核,始终面临三大核心矛盾:

  1. 体验与安全的矛盾:实时审核阻塞发布接口,普通用户体验差;全异步审核会导致创作者卡点发布延迟、流失核心用户。
  2. 合规与曝光的矛盾:若发布即展示,违规内容会短暂外放,带来监管处罚风险;若全部延迟展示,优质创作者无法即时上线。
  3. 一致性与可靠性的矛盾:数据库事务与 MQ 消息如何保证一致?消息重复投递如何避免重复审核?异常消息如何不阻塞正常业务?

为此,趣享社设计 **「用户分层 + 同步 / 异步双模式 + 待审核默认隐藏」** 的闭环方案:

  • 信任用户 / 创作者:同步实时审核,发布即展示,满足卡点、活动即时发布需求;审核不通过直接拦截发布。
  • 普通用户 / 新用户 :异步 MQ 审核,发布秒响应,笔记状态标记为待审核,全程不对外展示,审核通过后才公开可见,从根源规避违规曝光风险。
  • 高危用户:强制异步 + 人工复核兜底,严格风控。
  • 三层审核链路:AC 自动机敏感词检测 → RAG 相似违规案例检索 → 大模型价值观判定。
  • 全链路可靠性保障:事务后投递 + 手动 ACK + 消费幂等 + 死信队列 + 异常兜底,杜绝内容漏审、消息丢失、重复审核。

整套方案完全适配中小型社区项目,零过度设计、运维成本低、可靠性拉满,同时可平滑扩展为大型人机协同审核平台。

二、整体架构梳理

2.1 核心架构分层

整个审核体系采用 **「用户分层、业务解耦、异步削峰、安全优先、异常兜底」** 的设计思路,严格分离发布流程与审核流程:

plaintext

复制代码
┌──────────────────────────────────────────────────────────────────────┐
│                    NoteServiceImpl.createNote()                       │
│  用户分层判断:基于trustScore信任分区分普通用户/信任用户/高危用户        │
│                                                                      │
│  ┌──────────────────────────────────────────────────────────┐       │
│  │ 信任用户 → asyncReviewEnabled=false → 同步审核             │       │
│  │          审核通过直接展示;违规直接拦截发布               │       │
│  │                                                          │       │
│  │ 普通/高危用户 → asyncReviewEnabled=true → 异步审核        │       │
│  │          status=0(待审核,前端不可见)                     │       │
│  │          afterCommit → 投递ReviewTaskMessage到MQ          │       │
│  └──────────────────────────────────────────────────────────┘       │
└──────────────────────────────────────────────────────────────────────┘
                    │                         │
                    │ 同步模式                │ 异步模式
                    ▼                         ▼
┌───────────────────────────┐   ┌──────────────────────────────────────┐
│ NoteReviewService         │   │      RabbitMQ REVIEW_QUEUE            │
│ ├── createReviewRecord()  │   │                                      │
│ ├── DoubaoLlmService      │   │  ┌─────────────────────────────┐     │
│ │   .review(title,content)│   │  │ ReviewTaskConsumer          │     │
│ ├── handleNormal()        │   │  │ ├── isAlreadyReviewed()     │     │
│ └── handleViolation()     │   │  │ │   幂等性保障               │     │
└───────────────────────────┘   │  │ ├── reviewAsyncTask        │     │
                                │  │ │   .asyncReview()         │     │
                                │  │ │   调用LLM审核服务         │     │
                                │  │ └── 异常→进入DLX死信队列   │     │
                                │  └─────────────────────────────┘     │
                                │                                      │
                                │  ┌─────────────────────────────┐     │
                                │  │ REVIEW_DLX_QUEUE (死信队列)  │     │
                                │  │ 多次重试失败→手动排查补偿    │     │
                                │  └─────────────────────────────┘     │
                                └──────────────────────────────────────┘

2.2 用户分层信任体系(核心设计)

系统通过信任分 trustScore自动区分三类用户,实现审核模式自动路由,分层规则可直接落地:

信任分加分项
  • 注册时长 ≥30 天 +10
  • 历史发布合规笔记 ≥5 条 +15
  • 无任何违规记录 +20
  • 粉丝数≥100 +25
  • 内容优质(高点赞、高互动)+30
  • 签约创作者、官方认证用户 +100
信任分减分项
  • 发布违规笔记 -50
  • 评论违规、恶意刷屏 -30
  • 频繁发布广告、低质内容 -40
分层审核路由规则
  1. 信任用户(trustScore ≥ 60):同步实时审核,发布即展示;审核失败直接抛出异常,无法发布。
  2. 普通用户(0<trustScore<60):异步 MQ 审核,发布秒响应,笔记默认隐藏,审核通过后对外展示。
  3. 高危用户(trustScore ≤0 / 新注册 7 天内):强制异步审核 + 人工复核兜底,严格风控。

关键安全设计:普通用户发布后笔记状态 = 0 待审核,全局不可见,只有审核通过才改为正常展示,从根源避免违规内容短暂曝光。

三、核心方案落地实现

3.1 事务一致性核心:TransactionSynchronization.afterCommit

这是异步审核最关键的设计,彻底解决 **「数据库事务回滚,但 MQ 消息已发出」** 的事务不一致问题。

3.1.1 问题场景

如果直接在noteMapper.insert之后投递 MQ:

  1. 笔记写入 DB,随后投递 MQ 成功
  2. 后续代码(如 ES 同步、缓存更新)抛出异常,触发事务回滚
  3. 数据库中笔记数据被回滚,不存在该笔记
  4. MQ 消费者已收到消息,执行审核时查询不到数据,持续报错→重试→进入死信,产生脏任务
3.1.2 解决方案:事务提交后再投递

只有当数据库事务真正成功提交,才会执行 MQ 消息投递,从根源保证数据一致:

java 复制代码
// NoteServiceImpl.createNote() 核心代码
// 用户分层,自动切换同步/异步模式
boolean isTrustUser = userTrustService.isTrustUser(userId);
if (!isTrustUser) {
    // 普通用户,异步审核
    note.setStatus(0); // 待审核,默认隐藏
    noteMapper.insert(note);

    final Long noteId = note.getId();
    final Long authorId = userId;
    TransactionSynchronizationManager.registerSynchronization(
        new TransactionSynchronization() {
            @Override
            public void afterCommit() {
                ReviewTaskMessage message = ReviewTaskMessage.builder()
                    .noteId(noteId)
                    .userId(authorId)
                    .title(title)
                    .content(content)
                    .imageUrls(images)
                    .submitTime(System.currentTimeMillis())
                    .build();
                rabbitTemplate.convertAndSend(
                    RabbitMQConfig.REVIEW_EXCHANGE,
                    RabbitMQConfig.REVIEW_ROUTING_KEY,
                    message
                );
            }
        });
} else {
    // 信任用户,同步审核
    boolean pass = noteReviewService.reviewNote(title, content, images);
    if (!pass) {
        throw new BusinessException("内容违规,发布失败");
    }
    note.setStatus(1);
    noteMapper.insert(note);
}
3.1.3 设计优势
  • Spring 内置能力,零额外依赖、零学习成本
  • 事务不提交,消息绝对不投递
  • 事务提交成功,微秒级触发消息投递
  • 极端进程崩溃场景,可通过定时任务扫描待审核笔记补偿

3.2 RabbitMQ 生产级配置:业务队列 + 死信队列完整实现

项目采用手动 ACK + 死信兜底 + 并发控制 + 消息持久化的生产级配置,完全规避消息丢失、无限重试、队列阻塞问题。

3.2.1 核心配置类
java 复制代码
/**
 * RabbitMQ配置:审核业务队列 + 死信队列
 * 开关控制:配置关闭时不加载,适配开发/测试环境
 */
@Configuration
@ConditionalOnProperty(name = "rabbitmq.enabled", havingValue = "true", matchIfMissing = true)
public class RabbitMQConfig {

    // ========== 审核队列常量 ==========
    public static final String REVIEW_EXCHANGE = "quxiangshe.review.exchange";
    public static final String REVIEW_QUEUE = "quxiangshe.review.queue";
    public static final String REVIEW_ROUTING_KEY = "review.task";
    public static final String REVIEW_DLX_EXCHANGE = "quxiangshe.review.exchange.dlx";
    public static final String REVIEW_DLX_QUEUE = "quxiangshe.review.queue.dlx";

    /**
     * 业务队列:绑定死信交换机
     * 消费失败/拒绝/过期,自动转入死信队列
     */
    @Bean
    public Queue reviewQueue() {
        return QueueBuilder.durable(REVIEW_QUEUE)
            // 绑定死信交换机
            .withArgument("x-dead-letter-exchange", REVIEW_DLX_EXCHANGE)
            // 死信路由键
            .withArgument("x-dead-letter-routing-key", "review.dlq")
            .build();
    }

    /**
     * 死信队列:存储异常/失败消息,人工排查补偿
     */
    @Bean
    public Queue reviewDlxQueue() {
        return QueueBuilder.durable(REVIEW_DLX_QUEUE).build();
    }

    /**
     * 消费者工厂:生产级并发、限流、ACK配置
     */
    @Bean
    public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(
            ConnectionFactory connectionFactory) {
        SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
        factory.setConnectionFactory(connectionFactory);
        factory.setMessageConverter(jsonMessageConverter());
        // 基础并发消费者
        factory.setConcurrentConsumers(10);
        // 最大并发消费者
        factory.setMaxConcurrentConsumers(20);
        // 单个消费者预取消息数,平衡吞吐量与负载均衡
        factory.setPrefetchCount(10);
        // 消费失败不重回队列,直接进入死信
        factory.setDefaultRequeueRejected(false);
        // 手动ACK,保证消息可靠消费
        factory.setAcknowledgeMode(AcknowledgeMode.MANUAL);
        return factory;
    }

    /**
     * JSON消息转换器,防止序列化异常
     */
    @Bean
    public MessageConverter jsonMessageConverter() {
        return new Jackson2JsonMessageConverter();
    }
}
3.2.2 死信队列触发条件

项目中死信队列主要处理三类异常消息:

  1. 消费者手动拒绝,且requeue=false
  2. 消息消费超时 / 异常抛出
  3. 队列达到最大长度

核心设计原则:绝不允许无限重试!有毒消息直接隔离到死信队列,不阻塞正常消息消费,运维人工排查后可手动重新投递。

3.3 消费幂等性保障:杜绝重复审核

MQ 网络波动、重试机制、重启恢复,都会导致消息重复投递 ,必须保证消费者幂等消费------ 同一笔记无论收到多少条消息,只执行一次审核。

3.3.1 幂等实现核心
java 复制代码
@Component
@RequiredArgsConstructor
public class ReviewTaskConsumer {

    private final NoteReviewMapper noteReviewMapper;
    private final ReviewAsyncTask reviewAsyncTask;

    /**
     * 审核队列消费者
     */
    @RabbitListener(queues = RabbitMQConfig.REVIEW_QUEUE)
    public void consumeReviewTask(ReviewTaskMessage message,
                                  @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) {
        log.info("收到审核任务: noteId={}, userId={}", message.getNoteId(), message.getUserId());

        try {
            // 核心:幂等校验,已审核直接跳过
            if (isAlreadyReviewed(message.getNoteId())) {
                log.info("笔记已完成审核,跳过重复消费: noteId={}", message.getNoteId());
                return;
            }

            // 执行异步审核逻辑
            reviewAsyncTask.asyncReview(
                message.getNoteId(), message.getUserId(),
                message.getTitle(), message.getContent(),
                message.getImageUrls()
            );

            log.info("审核任务执行完成: noteId={}", message.getNoteId());
        } catch (Exception e) {
            log.error("审核任务执行失败: noteId={}", message.getNoteId(), e);
            // 抛出异常,触发死信队列
            throw e;
        }
    }

    /**
     * 幂等判断:查询审核记录表,确保单笔记只审核一次
     * reviewStatus != 0:已完成审核(通过/拒绝)
     */
    private boolean isAlreadyReviewed(Long noteId) {
        if (noteId == null) {
            return false;
        }
        NoteReview reviewRecord = noteReviewMapper.selectByNoteId(noteId);
        // 存在审核记录且非待审核状态,代表已处理
        return reviewRecord != null && !NoteReviewStatus.PENDING.equals(reviewRecord.getReviewStatus());
    }
}
3.3.2 幂等底层保障

数据库note_review表针对note_id建立唯一索引,从存储层保证一条笔记只会存在一条审核记录,彻底杜绝重复审核。

3.4 异步审核执行体:@Async 线程池隔离

审核逻辑涉及大模型调用、敏感词匹配、RAG 检索,属于IO 密集型慢操作,必须独立线程池隔离,不影响主线程和其他业务线程。

java 复制代码
@Component
public class ReviewAsyncTask {

    /**
     * 异步审核核心:独立线程池执行,不阻塞消费线程
     */
    @Async("reviewExecutor")
    public CompletableFuture<ReviewResult> asyncReview(
            Long noteId, Long userId, String title, String content,
            List<String> imageUrls) {

        log.info("开始异步审核: noteId={}, 图片数={}", noteId, imageUrls != null ? imageUrls.size() : 0);
        long startTime = System.currentTimeMillis();

        try {
            // 1. 创建待审核记录
            NoteReview review = createReviewRecord(noteId, userId, title, content);

            // 2. 三层内容审核:敏感词+RAG+LLM
            ValueReviewService.ValueReviewResult valueResult =
                valueReviewService.review(title, content, null, imageUrls);

            // 3. 合并审核结果(安全优先策略)
            ReviewResult finalResult = combineResult(review, valueResult);

            // 4. 处理最终结果:上架/下架/通知/Feed推送
            handleReviewResult(noteId, finalResult);

            log.info("异步审核完成: noteId={}, 结果={}, 耗时={}ms",
                noteId, finalResult.getStatus(), System.currentTimeMillis() - startTime);

            return CompletableFuture.completedFuture(finalResult);
        } catch (Exception e) {
            // 异常兜底:系统异常默认按违规处理,防止内容挂起
            log.error("异步审核系统异常: noteId={}", noteId, e);
            return CompletableFuture.completedFuture(
                ReviewResult.violation("审核系统异常,请申诉复核", Collections.emptyList())
            );
        }
    }

    /**
     * 审核结果处理
     */
    private void handleReviewResult(Long noteId, ReviewResult result) {
        Note note = noteMapper.selectById(noteId);
        if (note == null) {
            log.error("审核笔记不存在: noteId={}", noteId);
            return;
        }

        // 违规:标记下架 + 发送系统通知
        if (ReviewResultStatus.VIOLATION.equals(result.getStatus())) {
            note.setStatus(2);
            noteMapper.updateById(note);
            // 发送审核拒绝通知
            sendReviewRejectNotify(note.getUserId(), noteId, result.getReason());
            return;
        }

        // 通过:正常上架 + 推送Feed流 + 视频转码
        note.setStatus(1);
        noteMapper.updateById(note);
        feedPusher.pushNoteToFans(noteId, note.getUserId());
        // 视频笔记异步转码
        if (StrUtil.isNotBlank(note.getVideo())) {
            sendVideoTranscodeTask(noteId, note.getVideo());
        }
    }
}

3.5 内容安全核心:保守式审核结果策略

UGC 平台审核安全优先于体验 ,采用宁可误杀、不可放过的保守判定策略:

java 复制代码
/**
 * 合并审核结果:疑似违规 == 违规
 */
private ReviewResult combineResult(NoteReview review,
        ValueReviewService.ValueReviewResult valueResult) {

    String llmStatus = valueResult.getStatus();

    // 违规、疑似违规,统一判定为违规下架
    if (StrUtil.isNotBlank(llmStatus) &&
        (ReviewResultStatus.VIOLATION.equals(llmStatus) 
            || ReviewResultStatus.SUSPICIOUS.equals(llmStatus))) {
        log.warn("内容违规/疑似违规,noteId={}, 原因={}", review.getNoteId(), valueResult.getReason());
        return ReviewResult.violation(valueResult.getReason(), valueResult.getTags());
    }

    // 审核通过
    return ReviewResult.pass();
}
  • 大模型返回VIOLATION(违规):直接下架
  • 大模型返回SUSPICIOUS(疑似违规):同等判定为违规,杜绝风险
  • 误判内容:提供用户申诉入口,人工复核恢复

四、为什么不全用异步?核心设计思考

很多人会提出疑问:**直接全部异步,发布后笔记隐藏,审核通过再展示,技术上完全可行,为什么还要做同步审核?**这里给出最真实的生产级思考,也是面试高频标准答案:

  1. 核心创作者需要实时发布,异步会直接流失用户 普通用户可以接受延迟展示,但签约作者、大 V、活动博主需要卡点发布、限时内容,异步延迟 3--10 分钟会直接错过流量窗口,导致创作者流失,而创作者是社区的核心流量来源。

  2. 普通用户异步 + 待审核隐藏,完全规避违规曝光风险 我们设计普通用户发布后笔记状态 = 待审核,全程不对外展示,审核通过后才公开,不存在违规内容短暂暴露问题,安全可控。

  3. 同步审核只针对信任用户,普通用户完全无卡顿同步 LLM 审核虽然耗时 2--5 秒,但仅针对高价值创作者,这类用户愿意为了实时发布接受短暂等待;普通用户永远走异步,发布秒响应,完全无感。

  4. 分层策略兼顾合规、体验、创作者留存 全异步技术可行,但业务上会流失核心创作者;全同步会导致普通用户卡顿、体验极差;用户分层 + 双模式是 UGC 社区最合理的折中方案。

五、多方案选型对比与决策依据

5.1 异步实现方案对比:MQ 架构 vs 纯 @Async 线程池 vs 定时轮询 DB

说明:本项目采用 RabbitMQ + 内层 @Async 线程池 组合架构。下表对比的是三种整体异步架构方案 :纯 MQ 架构、纯 @Async(无 MQ)、定时任务轮询。项目中并非用 @Async 替代 MQ,而是 MQ 负责消息可靠投递,@Async 负责隔离 LLM 慢调用任务。

对比维度 RabbitMQ 消息队列(本项目方案,内层可搭配 @Async) 纯 @Async 直接调用(无 MQ) 定时任务轮询 DB
可靠性 消息持久化 + 死信,重启不丢失 进程崩溃任务完全丢失 依赖轮询频率,存在延迟
削峰能力 强,队列天然缓冲高峰流量 弱,线程池满直接拒绝 无削峰能力
重试机制 原生支持,死信兜底 需手动编码实现 需手动实现
业务解耦 完全解耦,审核逻辑独立演进 耦合业务代码 高度耦合,难以扩展
可观测性 管控台直观查看积压、消费状态 无监控能力 无法感知任务堆积
运维成本 中等,需维护 MQ 实例 极低
本项目真实架构说明
  • 外层:RabbitMQ :负责消息持久化、可靠投递、流量削峰、死信兜底,保证审核任务不丢失、可重试、不阻塞主业务
  • 内层:@Async 独立线程池 :用于执行 LLM 大模型审核这类IO 密集型慢任务,避免慢调用阻塞 MQ 消费线程,实现线程池隔离。

5.2 事务一致性方案对比

对比维度 afterCommit 事务后投递 RocketMQ 事务消息 本地消息表 + 定时扫描
复杂度 极低,Spring 原生 较高,需接入 RocketMQ 中等,需建表 + 定时任务
中间件依赖 强依赖 RocketMQ 仅依赖数据库
性能开销 几乎为零 半消息 + 回查,开销较高 定时扫描有数据库压力
适用场景 中小型项目,低成本可靠 大型金融 / 电商核心链路 无 MQ 环境的兼容方案

5.3 项目选型核心原因

  1. 放弃 RocketMQ 事务消息:项目体量无需过度设计,仅为事务一致性引入新中间件,运维成本翻倍
  2. 选择 afterCommit+RabbitMQ:零额外依赖、实现极简、可靠性满足 99.9% 以上业务场景,极端情况定时任务补偿即可
  3. 放弃纯 @Async 异步:任务不持久、重启丢失、无法削峰,完全不适用于线上审核业务
  4. 选择死信队列:杜绝有毒消息阻塞消费链路,异常集中化运维,不影响正常业务

六、高并发与性能优化

  1. 消费者并发伸缩基础并发 10、最大 20,队列积压自动扩容,流量回落自动缩容,自适应高峰流量

  2. 手动 ACK 保障可靠业务逻辑完全执行成功后再 ACK,异常消息不确认,自动进入死信,杜绝消息丢失

  3. 预取数均衡设计 prefetchCount=10,平衡网络 IO 开销与消费者负载均衡,避免忙闲不均

  4. 线程池深度隔离 审核专用线程池reviewExecutor,核心 3、最大 8、队列 500,与 Feed、通知、转码线程池完全隔离,避免慢审核拖垮整个系统

  5. 异步化全链路审核、Feed 推送、视频转码、系统通知全部异步化,发布接口只做核心入库,RT 控制在 50ms 以内

七、总结与后续迭代方向

趣享社用户分层 + 双模式内容审核体系,构建了一套完整的 **「体验 - 安全 - 可靠 - 运维」** 闭环:

  1. 用户分层:信任用户同步实时发布,普通用户异步隐藏审核,兼顾创作者体验与平台合规。
  2. 事务后投递:彻底解决数据库与 MQ 消息一致性问题。
  3. 手动 ACK + 死信队列:实现全链路消息可靠,异常消息隔离不阻塞业务。
  4. 唯一索引 + 幂等检查:保证重复消息只处理一次。
  5. 保守审核策略:死守内容安全底线,线程池隔离 + 异步削峰保证高并发性能。

后续迭代方向

  1. 多模型投票审核:接入多个大模型投票决策,降低单模型误判率
  2. 审核分级机制:区分严重违规、轻微违规、建议优化,差异化处理
  3. 人机协同审核:LLM 判定疑似 / 违规内容,自动流入人工审核台
  4. 监控告警体系:接入 Prometheus 监控审核耗时、队列积压、违规率,配置 SLA 告警
  5. 案例库自迭代:已确认违规内容自动入库,持续优化 RAG 检索准确率
  6. 审核灰度放量:按用户等级、内容类型灰度开启异步审核,平滑升级

这套方案是中小型 UGC 平台内容审核的最优实践之一,既解决了真实生产问题,又避免了过度架构设计,非常适合直接复用到社交、社区、博客、短视频等内容类项目。

相关推荐
逆境不可逃20 小时前
Hello-Agents 第二部分-第六章:框架开发实践
java·人工智能·分布式·学习·架构·rabbitmq
徐子童1 天前
RabbitMQ---开篇
rabbitmq
小旭95271 天前
RabbitMQ 核心详解
分布式·rabbitmq
我只想困告2 天前
day01-RabbitMQ_2026-05-13
分布式·rabbitmq
cheems95272 天前
[RabbitMQ] RabbitMQ 工作流程全解析
分布式·rabbitmq
我只想困告2 天前
day02-RabbitMQ 2026-05-14
java·spring·rabbitmq
heimeiyingwang2 天前
【架构实战】RabbitMQ实战:企业级消息可靠传递
架构·rabbitmq·ruby
zkkkkkkkkkkkkk4 天前
python使用celery实现异步任务
redis·python·rabbitmq·rocketmq
小英雄大肚腩丶4 天前
RabbitMQ消息队列
java·数据结构·spring boot·分布式·rabbitmq·java-rabbitmq