0.学习目标
- 了解常见的MQ产品
- 了解RabbitMQ的5种消息模型
- 利用MQ实现搜索和静态页的数据同步
源码及笔记:
链接:https://pan.baidu.com/s/1REHmVFclFI9_pu1ESB_sZw
提取码:voyc
1.RabbitMQ
1.1.搜索与商品服务的问题
目前我们已经完成了商品详情和搜索系统的开发。我们思考一下,是否存在问题?
- 商品的原始数据保存在数据库中,增删改查都在数据库中完成。
- 搜索服务数据来源是索引库,如果数据库商品发生变化,索引库数据不能及时更新。
- 商品详情做了页面静态化,静态页面数据也不会随着数据库商品发生变化。
如果我们在后台修改了商品的价格,搜索页面和商品详情页显示的依然是旧的价格,这样显然不对。该如何解决?
这里有两种解决方案:
- 方案1:每当后台对商品做增删改操作,同时要修改索引库数据及静态页面
- 方案2:搜索服务和商品页面服务对外提供操作接口,后台在商品增删改后,调用接口
以上两种方式都有同一个严重问题:就是代码耦合,后台服务中需要嵌入搜索和商品页面服务,违背了微服务的独立
原则。
所以,我们会通过另外一种方式来解决这个问题:消息队列
1.2.消息队列(MQ)
1.2.1.什么是消息队列
消息队列,即MQ,Message Queue。
消息队列是典型的:生产者、消费者模型。生产者不断向消息队列中生产消息,消费者不断的从队列中获取消息。因为消息的生产和消费都是异步的,而且只关心消息的发送和接收,没有业务逻辑的侵入,这样就实现了生产者和消费者的解耦。
结合前面所说的问题:
- 商品服务对商品增删改以后,无需去操作索引库或静态页面,只是发送一条消息,也不关心消息被谁接收。
- 搜索服务和静态页面服务接收消息,分别去处理索引库和静态页面。
如果以后有其它系统也依赖商品服务的数据,同样监听消息即可,商品服务无需任何代码修改。
1.2.2.AMQP和JMS
MQ是消息通信的模型,并不是具体实现。现在实现MQ的有两种主流方式:AMQP、JMS。
两者间的区别和联系:
- JMS是定义了统一的接口,来对消息操作进行统一;AMQP是通过规定协议来统一数据交互的格式
- JMS限定了必须使用Java语言;AMQP只是协议,不规定实现方式,因此是跨语言的。
- JMS规定了两种消息模型;而AMQP的消息模型更加丰富
1.2.3.常见MQ产品
- ActiveMQ:基于JMS
- RabbitMQ:基于AMQP协议,erlang语言开发,稳定性好
- RocketMQ:基于JMS,阿里巴巴产品,目前交由Apache基金会
- Kafka:分布式消息系统,高吞吐量
1.2.4.RabbitMQ
RabbitMQ是基于AMQP的一款消息管理系统
官网: http://www.rabbitmq.com/
官方教程:http://www.rabbitmq.com/getstarted.html
1.3.下载和安装
参考另一篇博客:rabbitMQ安装
https://blog.csdn.net/qq_38454176/article/details/105338529
2.五种消息模型
RabbitMQ提供了6种消息模型,但是第6种其实是RPC,并不是MQ,因此不予学习。那么也就剩下5种。
RabbitMQ的具体学习和使用,请看另一篇博客:
day72 JavaWeb框架阶段——RabbitMQ消息队列
https://blog.csdn.net/qq_38454176/article/details/105339764
3.项目改造
接下来,我们就改造项目,实现搜索服务、商品静态页的数据同步。
3.1.思路分析
发送方:商品微服务
-
什么时候发?
当商品服务对商品进行写操作:增、删、改的时候,需要发送一条消息,通知其它服务。
-
发送什么内容?
对商品的增删改时其它服务可能需要新的商品数据,但是如果消息内容中包含全部商品信息,数据量太大,而且并不是每个服务都需要全部的信息。因此我们只发送商品id,其它服务可以根据id查询自己需要的信息。
接收方:搜索微服务、静态页微服务
接收消息后如何处理?
- 搜索微服务:
- 增/改:添加新的数据到索引库
- 删:删除索引库数据
- 静态页微服务:
- 增/改:创建新的静态页
- 删:删除原来的静态页
3.2.商品服务发送消息
我们先在商品微服务leyou-item-service
中实现发送消息。
3.2.1.引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
3.2.2.配置文件
我们在application.yml中添加一些有关RabbitMQ的配置:
spring:
rabbitmq:
host: 192.168.56.101
username: leyou
password: leyou
virtual-host: /leyou
template:
exchange: leyou.item.exchange
publisher-confirms: true
- template:有关
AmqpTemplate
的配置- exchange:缺省的交换机名称,此处配置后,发送消息如果不指定交换机就会使用这个
- publisher-confirms:生产者确认机制,确保消息会正确发送,如果发送失败会有错误回执,从而触发重试
3.2.3.改造GoodsService
在GoodsService中封装一个发送消息到mq的方法:(需要注入AmqpTemplate模板)
private void sendMessage(Long id, String type){
// 发送消息
try {
this.amqpTemplate.convertAndSend("item." + type, id);
} catch (Exception e) {
logger.error("{}商品消息发送异常,商品id:{}", type, id, e);
}
}
这里没有指定交换机,因此默认发送到了配置中的:leyou.item.exchange
注意:这里要把所有异常都try起来,不能让消息的发送影响到正常的业务逻辑
然后在新增的时候调用:
修改的时候调用:
3.3.搜索服务接收消息
搜索服务接收到消息后要做的事情:
- 增:添加新的数据到索引库
- 删:删除索引库数据
- 改:修改索引库数据
因为索引库的新增和修改方法是合二为一的,因此我们可以将这两类消息一同处理,删除另外处理。
3.3.1.引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
3.3.2.添加配置
spring:
rabbitmq:
host: 192.168.56.101
username: leyou
password: leyou
virtual-host: /leyou
这里只是接收消息而不发送,所以不用配置template相关内容。
3.3.3.编写监听器
代码:
@Component
public class GoodsListener {
@Autowired
private SearchService searchService;
/**
* 处理insert和update的消息
*
* @param id
* @throws Exception
*/
@RabbitListener(bindings = @QueueBinding(
value = @Queue(value = "leyou.create.index.queue", durable = "true"),
exchange = @Exchange(
value = "leyou.item.exchange",
ignoreDeclarationExceptions = "true",
type = ExchangeTypes.TOPIC),
key = {"item.insert", "item.update"}))
public void listenCreate(Long id) throws Exception {
if (id == null) {
return;
}
// 创建或更新索引
this.searchService.createIndex(id);
}
/**
* 处理delete的消息
*
* @param id
*/
@RabbitListener(bindings = @QueueBinding(
value = @Queue(value = "leyou.delete.index.queue", durable = "true"),
exchange = @Exchange(
value = "leyou.item.exchange",
ignoreDeclarationExceptions = "true",
type = ExchangeTypes.TOPIC),
key = "item.delete"))
public void listenDelete(Long id) {
if (id == null) {
return;
}
// 删除索引
this.searchService.deleteIndex(id);
}
}
3.3.4.编写创建和删除索引方法
这里因为要创建和删除索引,我们需要在SearchService中拓展两个方法,创建和删除索引:
public void createIndex(Long id) throws IOException {
Spu spu = this.goodsClient.querySpuById(id);
// 构建商品
Goods goods = this.buildGoods(spu);
// 保存数据到索引库
this.goodsRepository.save(goods);
}
public void deleteIndex(Long id) {
this.goodsRepository.deleteById(id);
}
创建索引的方法可以从之前导入数据的测试类中拷贝和改造。
3.4.静态页服务接收消息
商品静态页服务接收到消息后的处理:
- 增:创建新的静态页
- 删:删除原来的静态页
- 改:创建新的静态页并覆盖原来的
不过,我们编写的创建静态页的方法也具备覆盖以前页面的功能,因此:增和改的消息可以放在一个方法中处理,删除消息放在另一个方法处理。
3.4.1.引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
3.4.2.添加配置
spring:
rabbitmq:
host: 192.168.56.101
username: leyou
password: leyou
virtual-host: /leyou
这里只是接收消息而不发送,所以不用配置template相关内容。
3.4.3.编写监听器
代码:
@Component
public class GoodsListener {
@Autowired
private GoodsHtmlService goodsHtmlService;
@RabbitListener(bindings = @QueueBinding(
value = @Queue(value = "leyou.create.web.queue", durable = "true"),
exchange = @Exchange(
value = "leyou.item.exchange",
ignoreDeclarationExceptions = "true",
type = ExchangeTypes.TOPIC),
key = {"item.insert", "item.update"}))
public void listenCreate(Long id) throws Exception {
if (id == null) {
return;
}
// 创建页面
goodsHtmlService.createHtml(id);
}
@RabbitListener(bindings = @QueueBinding(
value = @Queue(value = "leyou.delete.web.queue", durable = "true"),
exchange = @Exchange(
value = "leyou.item.exchange",
ignoreDeclarationExceptions = "true",
type = ExchangeTypes.TOPIC),
key = "item.delete"))
public void listenDelete(Long id) {
if (id == null) {
return;
}
// 删除页面
goodsHtmlService.deleteHtml(id);
}
}
3.4.4.添加删除页面方法
public void deleteHtml(Long id) {
File file = new File("C:\\project\\nginx-1.14.0\\html\\item\\", id + ".html");
file.deleteOnExit();
}
3.5.测试
3.5.1.查看RabbitMQ控制台
重新启动项目,并且登录RabbitMQ管理界面:http://192.168.56.101:15672
可以看到,交换机已经创建出来了:
队列也已经创建完毕:
并且队列都已经绑定到交换机:
3.5.2.修改数据试一试
在后台修改商品数据的价格,分别在搜索及商品详情页查看是否统一。