大家好,我是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。
我就说一下我对这两个中间件的看法:
| 对比维度 | RabbitMQ | Kafka |
|---|---|---|
| 核心定位 | 业务消息队列、任务调度 | 高吞吐流式日志、大数据采集 |
| 适用场景 | 接口异步解耦、订单、通知、延时任务 | 日志收集、用户埋点、实时数据流 |
| 消息可靠性 | 强,自带 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对象模拟对话,测试并发下各种场景的稳定性,并配置不同的异常率。