Spring Boot 集成免费的 EdgeTTS 实现文本转语音

新诶曰章第一阶段:认识消息队列

鱼皮:消息队列(俗称 MQ)就像一个快递驿站。快递员作为 生产者,把包裹(消息)放到驿站(消息队列),收件人作为 消费者,自己到驿站去取。

这样一来,快递员放下包裹就能走,不用费时间等你签收(异步)。

你也不用关心是谁送的、不用和快递员见面,有空去驿站取就行(解耦)。

就算双 11 包裹很多,驿站也能暂存起来等消费者慢慢取(削峰)。

这就是消息队列的三大作用:异步、解耦和削峰。

对于你的秒杀抢购系统,现在用户每次抢购都要同步执行各种操作(校验库存、扣减库存、创建订单),全部完成才能返回结果。

如果使用消息队列,用户点击抢购后,系统先做基本的校验并在缓存中预扣库存,确认可以抢购后,把抢购请求(消息)投入消息队列,就可以立刻返回结果了(页面)。

接下来由后台服务从队列中取出消息并处理那些耗时的操作,比如创建订单、在数据库中真正扣减库存。

这就是 异步:用户不用等所有后端操作完成,就能快速得到响应。

而且,抢购服务只管发消息,不用关心是谁来处理库存、谁来创建订单。假如以后要加短信通知、用户行为分析等功能,只需要让新服务也去监听消息队列就行,完全不用修改抢购服务的代码。

这就是 解耦:服务之间不直接依赖,让系统能灵活扩展。

如果同时抢购的用户过多,数据库处理不过来,也可以用消息队列做缓冲。所有的抢购请求先快速存入队列,后台服务可以根据自己的能力从队列中慢慢拉取消息并进行处理。

这就是 削峰:削平流量洪峰,保护后端系统不被压垮。

你恍然大悟:原来如此,消息队列也太强大了吧!俺要学,俺要学!

鱼皮:很有干劲嘛!主流的消息队列实现技术有:

RabbitMQ:简单易学、生态活跃,适合中小规模应用

Kafka:吞吐量高、延迟极低,适合大数据、日志收集场景

RocketMQ:阿里出品、支持事务消息,适合电商金融等对可靠性要求高的场景

你:阿巴阿巴,这么多都要学吗?

鱼皮:新手建议从 RabbitMQ 开始入门,学好一个再学其他的就很简单了。下面我就以 RabbitMQ 为例带你快速掌握消息队列必知必会的技术。

你:期待住了,这不得点赞狠狠支持?!

第二阶段:RabbitMQ 快速上手

鱼皮:首先来安装 RabbitMQ,笨办法是先安装 Erlang 语言环境,再去 官网下载 MQ 的安装包并执行。

但是更建议直接用 Docker 容器技术,快速安装和运行,不用担心版本不兼容。

安装完之后,访问 http://localhost:15672 就可以打开 RabbitMQ 内置的可视化管理界面(默认用户名和密码都是 guest),你可以在这里查看队列的运行情况、手动发送和接收消息。

另外,RabbitMQ 也提供了命令行工具 rabbitmqctl,主要用于运维管理,比如创建用户、查看状态等。

你:那我怎么用代码操作 RabbitMQ 呢?

鱼皮:RabbitMQ 提供了各种编程语言的 SDK 开发包,如果你是 Java 开发者,推荐使用 Spring AMQP。

AMQP 是高级消息队列协议(Advanced Message Queuing Protocol)的缩写,是一个开放标准,不绑定特定技术。RabbitMQ 就是基于 AMQP 实现的,所以客户端库叫 Spring AMQP。

只需几行代码,就能创建队列、让生产者发送消息、让消费者接收处理消息:

你:不是吧,这么简单?!

鱼皮:没错,但刚刚我们只是完成了最简单的 1 对 1 发送和接收消息。

实际上 RabbitMQ 有 6 种工作模式,适用于不同的业务场景,这也是消息队列的学习重点。

第三阶段:RabbitMQ 工作模式

简单模式

最简单的是 Simple 模式,一个生产者、一个队列、一个消费者。

就像老板派发任务给员工,队列(Queue) 是存储任务的容器,老板把任务放进去,员工从里面取出来完成。

工作队列模式

鱼皮:如果有很多任务要处理,一个员工忙不过来怎么办?

你:多找几个员工帮忙?

鱼皮:没错,这就是 Work Queue 工作队列模式,一个生产者、一个队列、多个消费者。

就像老板发布了一堆任务,RabbitMQ 会把任务依次分配给员工,但是一个任务只会被一个员工完成。

发布订阅模式

你:这样效率就高多了!

但如果老板要求所有员工都写工作总结,怎么把同样的任务发给多个员工呢?

鱼皮:好问题!之前工作队列是多个员工共享一个任务列表,而现在每个员工都要有自己的任务队列。

老板需要利用 交换机(Exchange) 来控制把任务分发给哪些和它绑定的队列。

比如想把任务发给所有员工,就要用到 广播交换机(Fanout Exchange),它会把任务发给所有已绑定的队列,然后每个员工分别从自己的队列取任务并完成。这就是 发布订阅模式(Publish/Subscribe)。

路由模式

你:那如果老板想把某些任务发给特定员工呢?比如鱼皮负责写代码和修 Bug,小阿巴负责写代码和摸鱼~

鱼皮:可以使用 路由模式(Routing),给每个员工的队列设置自己负责的路由键(Routing Key):

鱼皮的队列绑定 "写代码" 和 "修 Bug"

小阿巴的队列绑定 "写代码" 和 "摸鱼"

老板发布任务时会指定路由键,由 直接交换机(Direct Exchange) 根据路由键精确匹配,把任务发给对应的队列。

主题模式

你:那如果老板想把所有前端相关的任务都发给前端员工,后端相关的任务都发给后端员工呢?一个一个指定路由键是不是太麻烦了?

鱼皮:可以使用 主题模式(Topic),它使用 主题交换机(Topic Exchange),支持使用通配符模糊匹配。

可以给队列绑定通配符路由键,就能接收所有匹配的任务:

前端员工的队列绑定 frontend.*(匹配 1 个词),能匹配 frontend.Vue、frontend.React

后端员工的队列绑定 backend.#(匹配多个词),能匹配 backend、backend.Java、backend.Java.优化

你:哇,这样一来就灵活多了!

鱼皮:没错,这 5 种模式是企业中最常用的,掌握了它们就能应对大部分场景了。至于最后一种模式 ------ RPC(远程过程调用)模式,暂时不需要了解,因为企业开发中一般用专门的 RPC 框架,比如 gRPC、Dubbo。

你:好嘞,我先用这些工作模式来重构秒杀抢购功能,请好吧您嘞!

第四阶段:消息队列生产实践

一个月后,你意气风发地找到鱼皮:鱼皮 gie,我的抢购代码重构完了,快帮我看看能不能上线~

鱼皮看着你的代码,表情难受得像持矢一样:emmm,你这代码要是上线,公司就完蛋了!

你:阿巴阿巴,我本地测试过了,完全没问题啊!

鱼皮:消息队列在生产环境中的坑可多着呢!下面我来问你几个问题。

第一问:消息会丢吗?

鱼皮:如果重启 RabbitMQ 服务器,队列里的消息会丢吗?

你支支吾吾:会......会丢吧?

鱼皮:当然会!在抢购系统中,如果消息丢了,订单永远不会被创建,用户那边会一直显示 "抢购中"。

你着急了:那咋办啊,我会吃投诉的!

鱼皮:要保证消息不丢失,需要做好持久化 3 件套,把数据从内存保存到硬盘:

队列持久化,创建队列时设置 durable 为 true。

消息持久化,发送消息时设置消息为持久化模式(比如 deliveryMode = 2 或 persistent = true)

交换机持久化,创建交换机时设置 durable 为 true。

你:哈哈,这下消息就不会丢了吧!

鱼皮:不一定!光持久化还不够,还要有消息确认机制来保证消息的可靠性。

1)生产者确认(Publisher Confirm):生产者发送消息后,等待 RabbitMQ 的确认回复,确保消息真的被接收了。就像寄快递,你把包裹交给快递员,快递员要给你一个回执单,证明他收到了。

2)消费者确认(Consumer ACK):消费者处理消息成功后,要手动告诉 RabbitMQ "我处理完了,你可以删了"。

千万别用默认的自动确认,那样消息一收到就删除,万一处理失败消息就丢了。就像收快递要签收,确保包裹真的送到你手上了。

鱼皮:这样从生产到消费的整个链路都有保障,消息就不会丢失了。

第二问:消息会重复吗?

鱼皮:如果网络抖动,或者消费者重启,同一条消息可能被消费多次。比如抢购消息被重复消费,同一个用户的订单被创建了 2 次,库存被扣了 2 次怎么办?

你:那我得跑路了!

鱼皮:不至于不至于,我们要保证 消息幂等性,让重复消费等同于只消费一次。

常见的方案有 3 种:

1)给每条消息一个唯一 ID(比如 UUID 或雪花算法 ID),消费前先检查这个 ID 是否处理过。

2)利用数据库唯一索引,比如订单号设置为唯一索引,重复插入会失败。

3)使用 Redis 分布式锁,同一条消息同一时间只能有一个消费者处理。

你:明白了!抢购时为了防止重复下单,要检查用户是否已经下过单了。

鱼皮点头:没错,幂等性设计是分布式系统的基本功。

第三问:消息乱序怎么办?

鱼皮:比如用户先抢购下单、然后取消订单,分别向队列发了 "创建订单" 和 "取消订单" 2 条消息。如果处理顺序乱了,系统先处理取消、后处理创建,订单状态就错了。

你:队列不是先进先出的吗?RabbitMQ 应该天然有序吧?

鱼皮:单队列内确实有序,但如果你开了多个消费者并发处理,可能消息 1 还在处理,消息 2 已经处理完了,顺序就乱了。

你:那怎么保证顺序呢?

鱼皮:在 RabbitMQ 中,要严格保证顺序,建议用 单队列 + 单消费者。

你:那性能不就很低了吗?

鱼皮:没错,所以要根据业务需求权衡一致性和性能。

如果你全都要,可以考虑 Kafka 的分区机制,可以将同一用户的消息路由到同一个分区,同一个分区内的消息严格有序,不同用户的消息又可以并发处理。