🔗有一个人前来用 RabbitMQ,这是他AI接口发生的变化

5 阅读8分钟

大家好,我是EthanYuan,今天在优化昴云相册的功能时,突然想起来,我最应该优先优化的一个功能:AI 图片生成简介。为什么这个功能需要最先优化,接下来我就给大家讲讲我的思路。

AI 功能的迭代史

最初开发项目时,我发现每次上传图片都需要填简介,用户可能会因为自己暂时没想好怎么写而卡着,所以我就为上传图片的功能添加了一个辅助性的功能:AI 图片生成简介。

这个功能的作用是:用户从本地选取图片后,AI能根据图片进行理解,并返回生成好的简介

功能迭代了几个版本:

v0.0 - 获取图片 > 转换temp文件 > 上传COS、再下载回来 > 转base64 > 发给大模型 > 获取结果

这一版的调用时长经过计算,大概需要15s以上,远超用户体验可承受的范围,于是我对链路进行了优化:

v0.1 - 获取图片 > 本地转换temp文件 > 转base64 > 发给大模型 > 获取结果

这一次调用时长已经降低到8s ~ 15s 左右,平均10s,已经最大程度提高了性能,大模型调用与思考速度不可控,于是我就暂时放了放。当然,前两个版本均是同步调用,这是个大问题。

这将近10s一直在占用Tomcat线程,也就是主线程,一条线程仅仅是发了请求,然后一直阻塞等待结果,期间不能去干别的事情,利用率极低,毫无并发安全性。于是我简单地把同步请求改为了异步,在处理任务的方法打上@Async注解,释放主线程,那前端怎么知道AI啥时候生成完了?我想到了一个方法:前端轮询 + Redis记录任务状态


v0.2 - 前端发送taskId > Redis记录任务开始 > 获取图片 > 本地转换temp文件 > 转base64 > 发给大模型 > 获取结果 > 前端每秒轮询1次 > Redis更新任务状态,定时清掉 > 结果返回前端 > 轮询结束

本来以为这样就万无一失了,过了几天想了想,这样做在并发场景下如何,我总结了这一版功能的缺点:

  • 轮询会发送大量无效请求,占用线程资源
  • 高并发下成百上千个AI任务请求,轮询就会发上万个无效请求,服务器迟早要跨

此时我才想起来,如果Spring内置线程池是有限的这点没法改变,那就在外部进行限流,所以这时候我引入了本次优化方案:RabbitMQ - 用消息队列解决高并发下任务量与系统性能不对等的问题

RabbitMQ是什么?

RabbitMQ 是一款开源消息队列中间件,基于 AMQP 协议。主要作用是异步解耦、削峰限流、可靠消息投递。用来在系统之间收发消息,让服务不用同步等待,提升系统并发和稳定性。

引入RabbitMQ优化AI接口,有几个好处:

  • 请求的目的是发送taskId,发送完后立即释放,实测单次请求可在60ms上下
  • 立即建立WebSocket短时连接,仅用于任务结果的接收,接收后立即断开,无需轮询
  • 服务器性能不会影响前端的体验,就算任务失败了也可重试以及进入死信队列,消息可靠性有保障

可能会有人问我:为什么用RabbitMQ?其他消息队列中间件不是也能用吗,比如Kafka。

我就说一下我对这两个中间件的看法:

对比维度RabbitMQKafka
核心定位业务消息队列、任务调度高吞吐流式日志、大数据采集
适用场景接口异步解耦、订单、通知、延时任务日志收集、用户埋点、实时数据流
消息可靠性强,自带 ACK、持久化、死信队列,不易丢消息侧重高吞吐,业务级可靠需额外配置
消费模式点对点,一个任务只被一个消费者处理分区广播,易重复消费,不适合独占任务
延迟 / 死信原生插件支持,开箱即用原生不支持,实现复杂
部署复杂度轻量简单,有可视化管理后台依赖多、配置重、运维成本高
吞吐能力中等,满足业务任务足够极高,适合海量大数据流水

对于AI推理任务,本质上它是一种延时任务,需要消息可靠性,并且由于配置简单,是学习消息队列中间件的入门技术,所以我选择了RabbitMQ。

本项目的核心实现

1、安装RabbitMQ

我个人比较推荐用Docker安装,其内部已经内置好Erlang环境与对应版本的RabbitMQ,不会冲突,比手动安装要稳定。

线上环境优先使用腾讯云消息队列 RabbitMQ 版,保证服务不重启造成消息丢失,保障稳定性

本地先引入消息队列依赖

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

下面是我项目的具体实现:

1、生产者 / 消费者的配置

  • 生产者端采用条件化注入RabbitTemplate,配合配置开关控制MQ启用状态,若MQ不可用自动降级为@Async异步处理,保证服务可用性;
  • 发送消息时封装任务ID与重试次数,通过Jackson2Json消息转换器实现序列化,将消息投递至持久化的直连交换机;
  • 消费者端通过配置条件控制启用,采用手动ACK监听模式,设置单消费者、预取1条消息实现公平分发,避免消息积压,核心监听主队列执行AI任务处理逻辑。

2、重试机制的设计(失败重试次数、间隔)

  • 采用重试队列+TTL死信转发实现延迟重试,自定义重试队列设置10秒消息过期时间,过期后自动回流主队列重新消费;
  • 设定最大重试次数为3次,通过消息体中的attempt字段追踪重试次数,消费失败后递增次数并转发至重试队列,兼顾任务容错性与执行效率。

3、死信队列的作用与配置(处理重试失败的消息)

  • 死信队列用于兜底处理重试耗尽的失败任务,核心作用是保留失败消息用于问题排查、支持人工干预、避免消息丢失,同时可监控队列长度实现系统告警。
  • 配置上为主队列绑定死信交换机与路由键,当任务重试3次仍失败后,标记任务状态为失败,将消息转发至持久化死信队列,完成最终的异常消息兜底。

4、消息可靠性保障(生产者确认、消息持久化、消费者手动 ACK)
全链路实现消息高可靠:

  • 一是持久化保障,交换机、主队列、重试队列、死信队列均设置为持久化,消息默认持久化存储,服务重启不丢失数据;
  • 二是消费者手动ACK,全程采用手动确认模式,任务成功、重试、死信三种场景均执行正确ACK,杜绝消息丢失与重复消费;
  • 三是健壮性设计,生产者条件注入避免MQ不可用导致服务启动失败,搭配非MQ降级方案,最大化保证任务处理的稳定性。

开始测试异常情况

由于上传图片接口是需要登录校验的,我就没有先测并发任务,首先我在任务处理方法上加了一串代码:

if(true){throw new Exception}

然后用Mock对象注入,并将线程随机阻塞8~15s,用来模拟调用时长,这样做不消耗API免费额度。

这次测试的异常情况是:AI服务异常,任务失败

我在前端连续发了10次请求,经过测试,每个请求都正常进入重试队列,经过重试机制后仍然失败,最后10个任务全部进入死信队列,消息没有丢失,验证了消息的可靠性。

总结

这一次学习了RabbitMQ的作用,成功优化了AI接口调用这类延时任务的可靠性与稳定性,达到异步解耦、消息可靠的指标,下一步,我准备在另一个项目《AI情感大师》智能体,用RabbitMQ的三个特性优化普通的交互功能,大致的做法是:把同步调用换成异步解耦;建立WebSocket连接,用户只要进入聊天窗口,前端马上建立连接,超时15s无消息发送则自动断开;mock对象模拟对话,测试并发下各种场景的稳定性,并配置不同的异常率。