2024最新版的SpringCloud黑马商城项目
笔记对应教学视频讲解序号,并附上每小节所在的视频分p位置
笔记包含了视频讲解的核心内容及实战功能实现的详细过程
课程地址: 2024最新SpringCloud微服务开发与实战,java黑马商城项目微服务实战开发(涵盖MybatisPlus、Docker、MQ、ES、Redis高级等)
项目代码:课程地址简介中领取
系列文章目录
本笔记包含Docker、微服务、RabbitMQ、Elasticsearch等(持续更新)
黑马商城项目—最新SpringCloud开发实战—功能实现详细学习笔记(Docker篇)
黑马商城项目—最新SpringCloud开发实战—功能实现详细学习笔记(微服务篇)
黑马商城项目—最新SpringCloud开发实战—功能实现详细学习笔记(RabbitMQ篇)
黑马商城项目—最新SpringCloud开发实战—功能实现详细学习笔记(Elasticsearch篇)
目录
- RabbitMQ
- p85 MQ入门-01 MQ课程介绍
- p86 MQ入门-02 初识MQ-同步调用优缺点
- p87 MQ入门-03 异步调用优缺点
- p88 MQ入门-04 初识MQ-技术选型
- p89 MQ入门-05 RabbitMQ-安装部署
- p90 MQ入门-06 RabbitMQ-快速入门
- p91 MQ入门-07 RabbitMQ-数据隔离
- p92 MQ入门-08 Java客户端-快速入门
- p93 MQ入门-09 Java客户端-WorkQueue
- p94 MQ入门-10 Java客户端-Fanout交换机
- p95 MQ入门-11 Java客户端-Direct交换机
- p96 MQ入门-12 Java客户端-Topic交换机
- p97 MQ入门-13 Java客户端-基于Bean声明队列和交换机
- p98 MQ入门-14 Java客户端-基于注解声明队列和交换机
- p99 MQ入门-15 Java客户端-消息转换器
- p100 MQ入门-16 业务改造
- p101 MQ高级-01 课程介绍
- p102 MQ高级-02 发送者可靠性-发送者重连
- p103 MQ高级-03 发送者可靠性-发送者确认机制
- p104 MQ高级-04 发送者可靠性-发送者确认的代码实现
- p105 MQ高级-05 MQ可靠性-数据持久化
- p106 MQ高级-06 MQ可靠性-LazyQueue
- p107 MQ高级-07 消费者可靠性-消费者确认机制
- p108 MQ高级-08 消费者可靠性-消费者失败重试机制
- p109 MQ高级-09 消费者可靠性-业务幂等处理
- p110 MQ高级-10 延迟消息-什么是延迟消息
- p111 MQ高级-11 延迟消息-死信交换机
- p112 MQ高级-12 延迟消息-延迟消息插件
- p113 MQ高级-13 延迟消息-取消超时订单
RabbitMQ
p85 MQ入门-01 MQ课程介绍
它是应用广泛的高性能异步通讯组件
使用MQ前,顺序执行操作
使用MQ后,异步执行操作,效率提高
基础篇和高级篇的学习内容
p86 MQ入门-02 初识MQ-同步调用优缺点
以余额支付为例,它首先扣减用户余额,然后更新支付状态,最后更新订单状态等,
其中扣减余额和更新支付状态为整个支付的核心业务,其它的是非核心业务,
这些非核心业务应该异步执行
同步调用的缺点:
1,拓展性差:如果后续有新的需求,那么调用会去调用很多服务,代码会变得很多,
2,性能下降:当依次调用的其中某个服务响应慢,会影响整个业务的响应时间,
3,级联失败:除了性能下降外,调用者会等待被调用者,会无效地占用调用者的资源,导致调用者雪崩,
如果调用的其中某个服务宕机了,还会导致数据不一致的情况
同步调用的优点是时效性强,等待结果后返回
p87 MQ入门-03 异步调用优缺点
支付服务发送消息通知后,服务会去监听消息代理的消息:
有新的需求增加时,支付服务无需增加代码,解除了服务之间的耦合,拓展性也提高了,
无需等待非核心业务执行后返回,性能提升了,
对于宕机的服务,当服务重新上线后,消息代理继续通知未完成的消息,实现了故障隔离,
消息代理可以缓存突然激增的消息之后再慢慢处理,流量削峰
p88 MQ入门-04 初识MQ-技术选型
MQ常见的技术
RabbitMQ采用Erlang语言开发(这是一种面向并发的语言)
RabbitMQ支持集群,因此可用性高
RabbitMQ的单机吞吐量每秒10万,而Kafka的吞吐量每秒100万
目前市面上很少(小于10%)有公司的并发超过10万,而大多数的并发在几百
RabbitMQ的底层是基于内存的处理,它的消息延迟很低
如果消息能满足至少被消费1次,那么它的可靠性就高,RabbitMQ的可靠性比Kafka好,Kafka容易消息丢失
比较突出的技术是RabbitMQ和RoketMQ因为可靠性好,而Kafka适用于大数据对吞吐量有要求的
p89 MQ入门-05 RabbitMQ-安装部署
rabbitmq的官方文档
教学视频以rabbitmq版本3.8为例
采用docker来安装rabbitmq
它会提供一个控制台,然后需要设置用户名密码,挂载数据卷,创建网络(加入黑马商城项目的网络)
控制台端口为15672,收发消息的端口为5672
下载或者从tar包加载好镜像
Docker run创建容器
Docker logs -f mq查看日志,两个端口都打开了
访问rabbitmq的网页控制台,输入设置的用户名和密码
可以在首页看到总览信息,它的版本,消息队列当前状态,节点等
可以在首页看到总览信息,它的版本,消息队列当前状态,节点等
connections可以看到连接的消息发送者和接收者
channels表示收发消息的通道,用通道进行消息传输
这是一些默认的交换机,
exchanges交换机和queues队列用于管理消息
在企业中,部署的一个rabbitmq可能会由多个项目使用,
为了防止不同的项目创建的exchange冲突,需要做隔离,用virtual-host虚拟主机来完成(一个项目一个虚拟主机)
在控制台右上角可以看到此时是使用默认的虚拟主机
p90 MQ入门-06 RabbitMQ-快速入门
在控制台中进行rabbitmq测试
添加一个队列
测试向交换机发消息看队列是否能收到
进入默认的fanout交换机,发送一条消息
点击发送消息后,提示not routed,然后队列里面也没有收到,
说明消息丢失了(交换机没有存储消息的能力,交换机只负责转发消息)
队列没有收到消息的原因是交换机和队列没有建立一种关系
进入到交换机里面,点击bindings绑定,设置与它绑定的队列
进入到队列里面也能看到与它绑定的交换机
再次发送一条消息,没有刚才那个提示了
去队列里面查看,这2个队列都收到了消息
查看队列里面的消息,内容就是刚刚发送的内容
p91 MQ入门-07 RabbitMQ-数据隔离
希望不同的项目是隔离开的,通常的做法是一个项目对应一个用户,再为这个用户创建一个虚拟主机
Users是用户管理,可以看到创建的用户
Virtual Hosts可以看到创建的虚拟主机,斜杠是目前默认的虚拟主机
创建一个用户,tags设为管理员
可以看到刚刚创建的用户没有可以访问的虚拟主机
因此,使用这个用户登录后无法查看其它虚拟主机里面消息队列的内容
用hmall的身份登录后创建一个名为/hmall的虚拟主机,
创建完成后它默认当前用户可以使用,在Users中也可以看到
在交换机里面也出现了一堆默认的交换机,名称与之前的一样,但是它们属于新的虚拟主机/hmall
p92 MQ入门-08 Java客户端-快速入门
AMQP是rabbitmq采用的协议,该协议与语言平台无关
这里java客户端的演示不用官方的AMQP客户端,而是Spring AMQP客户端(在Spring官网里面可以找到)
打开课程资料的项目工程
可以看到有消息的发送者和接收者
这里的演示省略发送消息发到交换机,再发到队列的过程
在RabbitMQ控制台中创建队列
父工程里面引入amqp依赖,包含了rabbitmq
配置rabbitmq服务端信息
alt加enter快捷键创建一个单元测试 (单元测试要在启动类的包下或子包下)
发送一条消息,可以看到连接消息队列成功
队列里面出现了一条消息
然后在控制台里面可以看到了发送的消息内容
在消费者主类下创建一个类用于监听队列中的消息,这里接收的参数类型是String因为发送的消息类型也是String
启动消费者的主类,控制台输出一条日志,监听到了队列的消息
p93 MQ入门-09 Java客户端-WorkQueue
WorkQueue是消息发送模型,它让多个消费者共同消费队列中的消息
在接收者中定义2个消息监听者
(在实际生产中一般不会写成这样的2个方法,因为这2个消费者运行时消耗的是相同的机器资源,这样做没有意义,
因此,通常是写一个消费者方法,然后再部署多个实例分别在不同的机器上,它们代码相同,这些实例就是多个消费者)
在发送者中定义一个测试方法,发送50条消息,每条消息带上一个序号便于之后观察
启动消息接收者主程序,启动消息发送者的测试类,
控制台的部分打印输出,发现消费者2接收了奇数序号的消息,消费者1接收了偶数序号的消息,消息被交替消费了
可以发现:
消息只会被1个消费者处理(被处理了就不能被其它消费者处理)
消息会被消费者均匀分配(轮询)
多个消费者去消费一个队列可以加快消息的处理速度,因此,消息堆积的问题可以添加更多的消费者来解决
由于消费者实例运行的机器性能可能存在差异,需要设置消费者的处理速度来适配对应的机器性能
比如,设置消费者的处理速度为以上速度
设置的方式是让这个线程休眠
再次发送消息进行测试,发现处理了5秒才完成,预计是1秒多(消费者1的40与消费者2的5,那么每秒就是45条),
这里消费者1处理速度很快,只处理自己的那一半消息,处理完后就没有消息了,
消费者2的处理速度很慢,但依然要处理自己的那一半消息,造成了时间浪费(应该消费者1要多处理一些才行)
造成的原因是,rabbitmq默认是将消息轮询投递给消费者,不考虑消费者的处理速度
解决方法是修改rabbitmq的配置,将prefetch设为1,表示每次只能获取1条消息,处理完成才会获取下一个消息
将消费者的配置prefetch设为1
再次发送消息进行测试,发现消费者1不仅处理奇数序号的消息还处理了偶数序号的消息(消费者1处理了比一半更多的消息),
整个消息也在大约1秒内处理完毕,与预期相符(能者多劳)
p94 MQ入门-10 Java客户端-Fanout交换机
消息队列的完整模型包括:发送者、交换机、队列、消费者
交换机的类型有:fanout(广播)、direct(定向)、topic(话题)
fanout交换机会复制消息给多个绑定的队列,那么一个消息会被多个消费者消费
(这种交换机就可以在需要发送消息给多个微服务的情况下使用)
声明一个fanout交换机
将交换机与队列绑定
这里依然用convertAndSend方法,但是填入3个参数,
两个参数的方法会将第1个参数当成队列名,三个参数的方法会将第1个参数当成交换机名,
这里选三个参数的方法,由于现在是发送者只发到交换机因此第二个参数不填,routingKey参数是其它交换机会用到的
从控制台可以看到2个消费者都收到了消息
p95 MQ入门-11 Java客户端-Direct交换机
希望消息被部分消费者收到,而不是所有的消费者
通过队列与交换机设置BingdingKey和发送者设置的RoutingKey,来决定哪些消费者能收到消息,
Direct交换机也能实现Fanout交换机的功能
(应用场景举例,比如用户取消了订单,只需要给交易服务发送消息,不用给用户发短信以及加积分了,即不用发送给所有的消费者)
设置交换机的绑定队列的RoutingKey
设置消费者监听消息
设置消息发送者的routingKey
两个消费者都监听到了消息
将routingKey设为blue
只有消费者1收到了消息
p96 MQ入门-12 Java客户端-Topic交换机
RoutingKey由多个单词组成,之间以点分割,允许使用通配符(井号和星号),
因此,具有共同前缀的routingKey可视为一个Topic,这种方式的拓展性更强
创建topic交换机
设置交换机绑定的队列并指定RoutingKey
2个消费者都监听到了
修改发送的RoutingKey,这次只有消费者1能收到
p97 MQ入门-13 Java客户端-基于Bean声明队列和交换机
基于Bean声明队列和交换机
这里声明交换机和队列有两种方式:new的方式、使用Buider的方式。这两种方式没有区别
其中,durable意味持久化
声明以及绑定的代码通常是在消费者里面写(而不是发送者),
这里声明1个交换机和2个队列,并将它们绑定,运行消费者主程序
进入RabbitMQ的控制台可以看到声明成功,并且完成了绑定
p98 MQ入门-14 Java客户端-基于注解声明队列和交换机
基于注解声明队列和交换机
之前的案例,这次在Java中声明(而不是控制台)
基于Bean的方式的缺点,如果要指定routingKey,但是一个Bean只能传一个参数,
有多个参数就要写多个Bean来实现绑定,这会造成Bean的数量很多,这个问题可以用注解解决
用一个注解完成队列声明、交换机声明、绑定关系
在监听者的注解中声明交换机和队列并且完成绑定
@RabbitListener注解的一个属性是bindings,而这个属性的值也是一个@QueueBinding注解
进入控制台,声明和绑定都正常配置了
p99 MQ入门-15 Java客户端-消息转换器
rabbitmq支持发送任意类型的对象,因此在发送消息时需要将java对象转换为字节,这个转换由消息转换器来完成
发送一条Map类型的消息,但不设置消费者,然后去队列里面查看这条消息
可以看到这条消息的内容是字符串
Spring rabbitmq的底层采用了jdk提供的序列化方式(ObjectOutputstream)将java对象转换成字节
jdk的序列化存在一些问题:反序列化时存在漏洞容易被注入代码、消息太大(比如刚刚很小的对象存储了170多个字节)、可读性差
建议采用JSON序列化
由于publisher和consumer都需要依赖,可以在父工程引入依赖
子工程引入了依赖
定义一个MessageConverter的Bean(在publisher和consumer都要写),可以使用快捷键ctrl + H快速找到所在的类
配置好Bean后再次运行单元测试发送一条消息
在mq控制台get两条消息,可以看到修改后的消息类型为json,占用的空间变小了,内容可读性好
在comsumer中定义一个监听器,参数类型与发送的消息类型保持一致
启动消费者程序,可以看到输出的消息,
投递消息时类型是Map然后mq会把消息转换为字节(json方式),取出消息时会把字节又转回Map类型
p100 MQ入门-16 业务改造
将黑马商城中的一个业务从同步调用改为异步调用
由于不是通知所有的服务,因此不能用fanout交换机,这里采用direct交换机
首先声明队列和交换机(在消费者里面去写),发送者是pay服务,消费者是trade交易服务
第一步引入依赖,第二步配置mq的地址,第三步配置消息转换器,第四步配置监听器指定队列交换机
在发送者和消费者中引入依赖
配置mq的地址,在发送者和消费者中都要配置,可以放到nacos的共享配置中
配置消息转换器,由于发送者和消费者中都要配置,这里就写在common中,但是现在这个包路径下无法被扫描到因此无法生效
因此在spring.factories文件中加上一句话,让它能被扫描到,让配置生效
注意这里由于在common中配置了mq消息转换器,因此所有的服务都需要配置amqp的依赖,那么可以在父工程中加入这个依赖,否则无法运行
在发送者中修改原有的业务逻辑,将使用同步调用的FeignClient方式改为使用MQ的方式,指定发送的交换机、routingkey、消息等,
为了防止队列的使用影响到原有的代码(比如队列出错导致这里发送者的代码报错),这里加上try-catch来捕获异常
在消费者(trade交易服务)中配置监听器,创建队列交换机配置绑定关系,然后在监听方法中写原有的controller调用的代码
运行发送者和消费者服务,在mq控制台中可以看到创建好的交换机并完成了队列绑定
订单创建成功,可以看到订单号
去数据库里面查这个订单,可以看到状态为1,未支付
通过用户余额的方式支付成功
用户的金额扣减成功
可以看到订单状态为2,已支付
p101 MQ高级-01 课程介绍
保证消息收发的高可靠性
消息可靠性对业务很重要,
比如支付成功后但是订单更新状态没有完成,就会造成严重问题
这可能由于一些问题导致的:
1,发送消息时的网络出现问题(发送者的可靠性)。2,消息代理出现问题。3,消费者服务出现问题。
而在做好前3种可靠性后,延迟消息作为一种兜底的方案来使用
p102 MQ高级-02 发送者可靠性-发送者重连
发送者重连机制默认是关闭的,需要通过配置打开
最大重试次数默认为3
通过配置打开重试机制,然后将mq服务关闭,然后启动发送者去往队列里面发消息,会出现连接失败,
可以看到它会尝试连接3次,间隔时间与预期设置的相符
这里的重试机制是阻塞式的,如果发送者的连接真的出现问题,它执行重试时会造成后续的代码被阻塞,影响性能
p103 MQ高级-03 发送者可靠性-发送者确认机制
通过发送者确认来确保发送消息到MQ的可靠性
未路由成功的消息也算成功,MQ返回ACK给发送者。因为这是发送者造成的错误,而不是MQ造成的,
临时消息指不需要持久化的消息(普通的消息会进行持久化)
持久消息必须要写入完成以下才算成功,返回ACK:1,到达交换机;2,进入队列;3,完成持久化
而临时消息由于不需要持久化,因此进入队列就算成功,返回ACK
在配置种开启2个确认机制
correlated机制与simple不同,它避免了同步等待,
这是publisher return(发送者返回)机制,发送失败时会记录日志
这是publisher confirm(发送者确认)机制,在发送消息的时候会多传一个参数cd(CorrelationData),
它首先用getFuture方法得到一个空的对象,而addCallback方法用于将来MQ执行完成后会把确认结果通过回调函数通知给cd的空对象
p104 MQ高级-04 发送者可靠性-发送者确认的代码实现
发送者确认的代码实现
在发送者的yaml中配置
Return callback定义在发送者的一个配置类中
Confirm callback定义在消息发送者中,测试消息正常发送情况下日志的输出结果
将消息的routingKey改为一个不存在的字符,会导致交换机路由失败,
在日志中可以看到return callback和confirm callback都收到了,并显示错误类型为路由失败
如果指定了一个并不存在的交换机名称,会返回nack消息发送失败
p105 MQ高级-05 MQ可靠性-数据持久化
MQ的数据持久化可以保证MQ的可靠性
解决方法是持久化和lazyMQ,这里先讲持久化
交换机、队列创建默认是持久化的(Durable),而Transient表示临时的不持久化
消息在发送时有投递模式,Non-persistent非持久化(默认)和Persistent持久化
往队列里面发送非持久化的消息和持久化的消息,然后将MQ重启,最后发现只有持久化的消息还在,其它消息不见了
现在,往队列里面发送一百万条消息进行测试
为了方便观察结果,关闭发送者确认(因为这会造成一定的性能代价)
由于消息默认是持久的,如果要发送非持久消息,要先定义一个消息体
非持久化的消息是基于纯内存模式,如果内存满了,它也会写入磁盘(因为内存装不下了只能先放在磁盘里),
而此时写入会造成阻塞(消息处理速度降至0),然后随着写入完成逐渐提升处理速度
如上图,通常情况是,内存满了之后写入磁盘引起性能下降(黄线下降),多出的消息写入完成后逐渐恢复性能(黄线上升)
接下来再看持久化的消息
将消息改为持久化模式,再进行一次测试
可以看到,消息进入内存和持久化的数量是一致的(而不是内存满了再进行持久化),它会同时进行这2个操作,
因此,这种持久化消息的优点不仅是保证消息丢失,而且不会造成阻塞
p106 MQ高级-06 MQ可靠性-LazyQueue
这节讲队列持久化,惰性队列,它直接将消息写入磁盘,
它对写磁盘做了优化,性能较好,推荐使用LazyQueue
为了减少从磁盘加载到内存所需时间的影响,它会监测消费者的处理速度,提前加载部分消息到内存
LazyQueue的优点:1,持久化消息(直接存入磁盘);2,不写入内存也就不会内存满了pageOut导致阻塞
在控制台中创建一个lazyQueue时需要指定参数
lazyQueue两种代码声明方式
同样地,也往lazyQueue里面发一百万的消息进行测试,可以看到一开始就出现在paged out中,而不是内存和持久化
p107 MQ高级-07 消费者可靠性-消费者确认机制
消费者返回给MQ的确认
有三种消息处理状态,ack、nack、reject
返回消息状态不是在消费者拿到消息后立即返回,而是处理消息后根据处理结果来返回
如果返回reject通常是消息的内容有问题,消息没有重试的必要(因此不能用nack)
接下来进行测试
将消费者的yaml配置文件的消费者确认模式设为none
然后用发送者Test测试类往队列里面发一条消息,进入控制台可以看到存在一条消息
然后在消费者打一个断点,debug启动
此时进入控制台查看,发现消息不见了,说明立即返回了一个ack,队列把消息删除了,
但此时消费者并没有执行完毕,说明是拿到消息后立即返回的
接下来将消费者确认模式改为auto来测试
同样地,进入上面的断点
进入控制台看到有一个未确认的消息
将这个断点放行,发现它又进入断点了(控制台输出里面再次监听到了消息),
说明消费者返回的是nack,那么队列会将消息再次投递给消费者(直至消费者宕机)
将消费者关闭后,进入控制台看到消息变回了ready
再测试一下抛出一个消息转换异常
同样地,进入到断点后,点击放行,发现它不会再次进入断点了,
说明队列没有再次重复发消息了,消息被拒绝了
p108 MQ高级-08 消费者可靠性-消费者失败重试机制
消费者失败重试机制
失败重试机制是消费者异常时本地重试,而不是重新入队再取出消息(消息在消费者和MQ之间踢皮球)
它默认是关闭的,需要在配置文件中打开
先测试一下现在的情况
在消费者中设定抛一个异常,然后往这个队列里面发一条消息,
启动消费者后它会不断地报错,说明这条消息一直在重复的发送(队列发给消费者,而不是消费者自己重试)
打开控制台可以看到每秒发了很多条消息(此时消费者和MQ都会有很大的性能消耗)
在消费者中打开失败重试,再用上面发消息的例子
失败重试3次后(默认3次),重试次数耗尽,消息就会被拒绝,此时消息既不在MQ也不在消费者中(可靠性降低了)
第一种方式是默认的,
第二种方式是重试耗尽后,重新入队
第三种方式是重试耗尽后,投入到指定交换机,Republish重发消息
接下来进行第三种方式的测试
在消费者里面创建一个配置类
重新用之前的例子进行测试,可以看到重试3次后,它会将消息发送到刚刚配置的交换机
进入控制台可以看到创建了error队列,并且里面有一条消息
查看这条消息,可以看到它带上了完整的异常栈信息(便于进行故障排查)
p109 MQ高级-09 消费者可靠性-业务幂等处理
以上的小节,保证了消息各方面的可靠性,但是可能出现消息被重复处理的可能性,
比如由于网络出现故障,网络恢复后再次投递一次消息,但是这条消息之前已经被处理过了,在某些业务中可能导致问题
解决消息重复消费问题就是要进行业务幂等处理,即业务执行一次或多次的业务状态影响是一样的
方案一
在发送者的消息转换器里面将消息id的设置打开,
然后往队列里面发送一条消息,之后观察结果
在控制台中可以看到这条消息的属性有一个id,那么消费者就可以将这个id存入数据库来判断消息是否被消费过,
接下来在消费者中查看这个id
将消费者的方法参数类型修改为Message(而不是之前的String),就可以获取到属性,从而拿到消息id
这种方案不推荐使用,业务里面会去判断与业务无关的消息这部分的逻辑,会造成业务侵入,而且也对业务性能有影响
第二种方案是在业务本身上增加健壮性,考虑到这种消息重发的场景,
上图的场景:
比如用户付款后通过MQ通知交易服务将订单标记为已支付,交易服务然后返回ack给MQ表示处理成功,
此时网络出现故障,MQ没有收到这个ack以为交易服务宕机了(接下来,消息会重新入队并发消息给交易服务)
此时,用户再点击申请退款(由交易服务完成),交易服务将订单改状态为退款中,
此时,MQ到交易服务的网络恢复,之前的MQ中的消息再次发送给交易服务,结果是造成订单状态从退款中被覆盖为已支付。
如果考虑到业务幂等就可以加上图红圈部分的逻辑,
在进行标记前先判断是否是未支付,可以得出是否是一条重复消息,如果为重复消息则不作标记直接结束
在交易服务的Listener中标记订单为已支付前加上这段逻辑,实现业务的幂等性
首先,支付服务和交易服务之间的订单状态一致性由MQ异步通知实现的,
然后,多种可靠性保证消息至少被消费一次,
最后,多次消费引起的幂等问题,是业务中的幂等判断避免的
服务消息处理失败的兜底方案见下节
p110 MQ高级-10 延迟消息-什么是延迟消息
延迟消息是在指定时间之后消费者才会收到的消息
比如说,用户下单后扣减了商品库存,
用户是否支付是由支付服务完成的,如果支付了支付服务需要通知交易服务修改订单状态为已支付,
但网络故障,支付服务给交易服务发送的消息没有收到,
这种情况下,交易服务就可以设置一个延迟任务,30分钟后去问一下支付服务是否用户支付了,
这样可以起到一个订单超时未支付自动取消的效果,
具体的做法是,在下单的时刻就往MQ里面发一个30分钟的延迟消息,
时间到了之后交易服务就会收到消息,从而去执行延迟任务
p111 MQ高级-11 延迟消息-死信交换机
使用死信交换机可以实现延迟消息
比如说,将normal.queue指定一个死信交换机dlx.direct,再往队列里面发送一条过期时间为30秒的消息,
由于这个队列是没有消费者监听的,那么30秒后它就会被发送到死信交换机,再投递到绑定的队列,
因此,消费者去监听这个队列就可以达到延迟消息的效果,
此外,这两个地方的交换机和队列的bindingKey需要设为一致(normal.direct到normal.queue、dlx.direct到dlx.queue)
在消费者中定义一个死信交换机并绑定队列
在消费者中定义一个normal交换机和队列并绑定它们,并指定队列的死信交换机,
这里创建交换机和队列用的是传统Bean的方式,而不是上面的@RabbitListener注解的方式,因为这个队列没有消费者
启动消费者后,进入MQ控制台可以看到交换机和队列都创建成功了,在队列中可以看到DLX标志,表示它指定了一个死信交换机
发送者发送一条消息,设置消息过期时间为10秒,
message是MessagePostProcessor是消息的后处理器,可以获取消息属性,来设置消息的过期时间
在消费者中可以看到10秒后收到了这条消息
p112 MQ高级-12 延迟消息-延迟消息插件
上一节的延迟消息是用死信交换机来实现的,这种方式配置比较复杂,可以用延迟消息插件来将交换机加上延迟功能
消息到达交换机后不会立刻进行路由,而是等待一段时间再路由到队列
插件可以在RabbitMQ社区下载也可以在GitHub下载,插件的版本要与MQ的版本一致
将插件复制到mq容器的/plugins目录下
然后先用命令docker exec -it mq进入容器,再用rabbitmq的插件命令启用这个插件,可以看到插件已启用
在代码中声明队列为延迟的队列的2种方式(注解方式和Bean方式),加上delayed表示这是一个延迟消息
在发送消息时需要设置消息的延迟时间,这里与上一节相同(都是通过MessagePostProcessor消息后处理器来完成),
不同的是,这里是setDelay(上一节是setExpiration)
在消费者中定义交换机和队列,并给交换机加上延迟属性
启动消费者,然后在控制台可以看到这个交换机的类型为延迟消息的交换机
启动发送者,发送一条延迟消息,时间设置为10秒
10秒后,消费者监听到了消息
这种延迟消息的实现是由cpu来完成计时的,是cpu密集型的任务,
如果有大量的消息需要计时,会给cpu带来很大的压力,
因此,尽量不要设置过长的延迟时间,否则会造成很多延迟消息需要计时,影响性能
p113 MQ高级-13 延迟消息-取消超时订单
在黑马商城中用延时消息解决用户未支付超时订单的问题
这里还是之前那个例子,假设支付服务向MQ发送消息的过程出现问题,支付服务和交易服务的一致性就会有问题,
那么无论支付服务是否能够成功通知交易服务,交易服务都应该有一个兜底方案(没有支付成功就取消订单恢复库存),
在下单逻辑结束前发送一个15分钟的延迟消息到队列,在15分钟后交易服务自己收到这条消息,然后去做后面的业务,
然后查询订单是否支付,如果已为支付就不作操作,
如果为未支付,有可能是支付服务的支付成功的消息还没有发到交易服务的情况,那么就去查支付服务的交易流水,
如果有支付的流水就改为已支付,没有就不作操作
由于交易服务要通过OpenFeign向支付服务发送请求,先定义好FeignClient需要的类
在订单创建业务的最后发送一条延迟消息到MQ,消息内容为订单id,延迟时间设为10秒
在交易服务的中定义一个消费者去监听这条延迟消息,收到消息后去做延迟的业务逻辑
接下来进行测试
首先将支付服务支付成功后发送MQ给交易服务的代码注释掉(用于通知交易服务修改订单为已支付),
为了模拟支付服务发送消息给MQ的故障情况
在商城里面下单并支付,显示支付成功
在交易服务的订单表里面可以看到这个订单的状态为2(已支付),支付时间是订单创建时间的10秒后,说明10秒后更新订单状态为已支付
在交易服务的日志中看到在10秒后,交易服务用PayClient向支付服务发了一个查询,发现订单已经支付了,就更新订单为已支付,
因此,实现了一种延迟查询的效果,作为交易服务订单业务的兜底方案