目录
一、前言
本博客主要记录我完成黑马商城day08-Elasticsearch作业的过程,其中包括了我的一些理解、总结与想法,同时还有写作业过程中遇到的一些问题。本人水平有限,作业也是在借鉴很多前人之后完成的,整体来说给出答案的还是比较少,而且不同的答案反复切换来看确实是有点眼花缭乱,并且一部分答案我个人认为是有一定问题的。所以这篇博客只是单纯记录我自己的答案以及理解,答案并没有优化,仅仅是完成基础要求,如果有错误或者改进想法可以分享到评论区一起讨论
二、先理清整体逻辑
作业一共有三个要求,分别是:1、服务拆分 2、商品查询接口 3、数据同步
在拆分search-service之前我们点开item-service的所有代码大概看一下会发现:其实就是把SearchController拆出来。虽然看着是SearchController调用itemService接口中的lambdaQuery方法。但是实际你点开IItemService,ItemServiceImpl和ItemMapper可以看出里面编写的代码其实都与SearchController无关。因为lambdaQuery是MybatisPlus直接提供的(它来源是IItemService的父类IService),总结就是在search-service把空的ISearchService,serviceImpl,mapper按结构依次创建出来就好(继承实现对就行,把item-service的拿来改名字把里面代码去掉)
第二个作业很简单,item-service里面根据id查询商品的功能之前就导入过,所以只需要在hm-api中的FeignClient和fallback里加上部分代码即可
第三个作业是对商品服务实现增删改时做的,所以这部分就是在item-service里面编写listener,在飞书文档里面就有一个靠前的答案把listener写到search-service我个人觉得就是有问题的
三、服务拆分
创建search-service模块,模块整体的结构如图所示,先把图中的包一个一个创建好。这里domain一开始我把hm-api的ItemDTO和item-service的ItemDoc都复制过来了,后面发现ItemDTO用到了但是毕竟已经提取到hm-api了所以导api的包即可,然后ItemDoc还没用到暂时不复制过来(后面用到再说)。这里的话我个人认为Item和ItemQuery在item-service里面也用到了,可以像之前ItemDTO一样提到api里面去,但是毕竟当时我是先以简单实现完成作业为目的,多一事不如少一事就没动它了(现在我全部完成了回头写博客就觉得可以来提取一下)
包都创建好了就开始来复制各种文件了,先是把item-service的依赖复制pom文件里面,这一部分我也是看了好几个人的答案修修改改的,所以确实我也忘记了哪些是多补充进去的。印象中我是补了OK http、sentinel、amqp、es(OK http和sentinel那些是参考别人的,但是感觉也没用上,搞不太懂,反正依赖和配置文件这些宁多勿少吧)
<dependencies>
<!--common-->
<dependency>
<groupId>com.heima</groupId>
<artifactId>hm-common</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>com.heima</groupId>
<artifactId>hm-api</artifactId>
<version>1.0.0</version>
</dependency>
<!--web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--数据库-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--mybatis-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
<!--nacos 服务注册发现-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--OK http 的依赖 -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
</dependency>
<!--统一配置管理-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!--读取bootstrap文件-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
<!--seata-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
<!--sentinel-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!--amqp-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<!--es-->
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
</dependency>
</dependencies>
<build>
<finalName>${project.artifactId}</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
四个配置文件先从item-service直接复制过来,然后点开修改一下(一般就是改application.yaml和bootstrap)
我的application.yaml如下,仅供参考:
server:
port: 8087
feign:
okhttp:
enabled: true
sentinel:
enabled: true
hm:
db:
database: hm-item
swagger:
title: "黑马商城商品搜索接口文档"
package: com.hmall.search.controller
spring:
cloud:
sentinel:
transport:
dashboard: localhost:8090
http-method-specify: true #开启请求方式加前缀
然后bootstrap如果之前item-service也没加上mq配置的话就两个都加上
然后先把Item和ItemPageQuery从item-service复制过来,这两个没啥好说的,直接复制过来就行
然后就是把SearchController直接复制过来,IItemService复制过来把名字改成ISearchService,把ItemServiceImpl复制过来改成SearchServiceImpl,把ItemMapper复制过来改成SearchMapper,把ItemApplication复制过来改为SearchApplication。到此,文件复制基本就完成了,爆红别急,结合下面步骤慢慢来重新导包调整
在SearchController里面注意红框的部分,IItemService换成刚改名的ISearchService,ItemDTO导api的包,下面原来是itemService也改成searchService(我印象中是这些地方,还有其他地方爆红自己看看重新导包)
提一嘴,这里我就是完成最基本的拆分而已,你可能看到很多答案这里都开始用es,其实完全没必要这样急着做,es后面还有一节课没学完呢。并且你等下前端验证成功拆分,后续慢慢优化改进,突然开始跳步到时候找bug只会更难受
ISearchService由于是复制过来的,所以继承IService是已经有的,只要把里面内容删空如下图即可(Item导一下包就行)
同理,把复制过来的ItemMapper改了,Item导下包,内容删光就行
SearchMapper同理
SearchApplication把下面两处修改一下即可
此时服务拆分基本完成(还有爆红自己看一看吧,我实在记不清全部的问题了),然后就是开始测试是否成功拆分
Maven处重新加载所有Maven项目
在左下角点击服务图标,找到刷新出现的SearchApplication,右键点击编辑所选配置
这里加上local,然后上面的是添加的虚拟机选项(修改选项,点击Java下的添加虚拟机选项,复制下面给出的语句)。这里添加虚拟机选项是因为启动SearchApplication失败爆红,原因好像是JDK17兼容问题,反正大家如果没改JDK17先只加local启动试试看,不行就加个虚拟机选项再试试
--add-opens java.base/java.lang=ALL-UNNAMED
然后是修改动态路由,这一块是我当时偷懒把动态路由跳过了(跳步然后找bug找崩溃了,以后还是得一步步来,血泪教训)。这里就是点开hm-gateway的application.yaml把item路由那一块的search删掉,然后在下面单独加上search
如果是学了动态路由那一块的话,就是去nacos那里改,在路由表中添加search-service对应路由信息
{
"id": "search",
"predicates": [{
"name": "Path",
"args": {"_genkey_0":"/search/**"}
}],
"filters": [],
"uri": "lb://search-service"
}
四、测试服务拆分效果
此时我们是刚刚把search-service拆出来,也就是item-service里面还是有SearchController的。所以测试的时候启动SearchApplication,不启动ItemApplication然后打开前端搜索页面看看
搜索页面和之前显示一样(那个绿色箱子是我后面测试的,不知道怎么就跑前面来了),你们正常应该还是白鞋和牛仔裤开头。同时右上角会显示商品数量
再分页试试没问题就成功了,如果成功就直接回去把item-service里面的SearchController删掉就行
可能遇到的错误情况:如果你首页和之前的不太一样,然后点击分页发现效果也不对,你可以按F12打开浏览器的开发者工具,正常成功的情况状态码应该是200。如果你的状态码是503(好像是这个,我也不太记得了),就和我当时动态路由没修改的问题一样,建议倒回去看看动态路由那里配置是否正确
五、商品查询接口
在ItemController中已经存在根据id查询商品的方法了,按如下图选中蓝色部分复制
然后黏贴到hm-api的ItemClient中,黏贴之后一定要记得先把路径加上items,否则后面出问题找bug又是贼搞心态(本人亲身经历,如果你最后测试第三个作业监听消息发现控制台报错404可以看看是不是这里路径少了)。然后把public删掉,最后加个分号,注释自己想写就写
然后在fallback里面模仿其他三个补上就可以了
六、数据同步
先提前说明,这部分全程都只在item-service里面操作,这部分是争议最大的一部分。因为每个人代码风格不一样,同时关于数据库和es也有不是很能统一的点,本人水平有限,只能给出我自己想法实现的代码,仅供参考(我给的只是实现基础,有想法可以评论区交流)
首先是item-service的pom文件,我好像补了一部分依赖,但是说也说不清,我就放上我整个的依赖部分(还是宁多勿少)
<dependencies>
<!--common-->
<dependency>
<groupId>com.heima</groupId>
<artifactId>hm-common</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>com.heima</groupId>
<artifactId>hm-api</artifactId>
<version>1.0.0</version>
</dependency>
<!--web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--数据库-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--mybatis-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
<!--nacos 服务注册发现-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--统一配置管理-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!--读取bootstrap文件-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
<!--seata-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
<!--amqp-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<!--es-->
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
</dependency>
</dependencies>
先在ItemController里面注入RabbitTemplate
然后的话,数据同步这块主要是商品服务的增删改,我个人认为就是新增商品,更新商品状态(我后面感觉状态应该不需要,但是写都写了就不管了)、更新商品、删除商品这四个方法(有人说批量扣减库存和批量增加库存也需要,也有人说es里面不受status和库存影响,反正我个人认为这部分即使没有实现也问题不大,重在学习大概的流程,所以答案仅供参考)
四个方法都是要加上rabbitTemplate发送信息的代码,除了新增商品剩下三个都是加一行代码就行。因为新增商品如果不先获取到存入数据库的id的话,更新索引库就没id传入。然后交换机和routingKey的名字大家自行取
然后在item-service中新建一个listener包再创建ItemListener类,刚刚是发送信息,这个类里就是编写监听信息的代码了,具体的代码和相关注释我就直接给出,大家自行参考
public class ItemListener {
private final ItemClient itemClient;
private final RestHighLevelClient restHighLevelClient = new RestHighLevelClient(RestClient.builder(
HttpHost.create("http://192.168.100.128:9200"))
);
/**
* 监听新增商品 新增 index
*/
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "item.index.save.queue", durable = "true"),
exchange = @Exchange(name = "item.direct", type = ExchangeTypes.DIRECT),
key = "item.index.save"
))
public void listenSaveItem(Long id) throws IOException {
//读取数据库对象
ItemDTO itemDTO = itemClient.queryItemById(id);
if (itemDTO == null) {
return;
}
//转换
ItemDoc itemDoc = BeanUtil.copyProperties(itemDTO, ItemDoc.class);
//Json化
String jsonStr = JSONUtil.toJsonStr(itemDoc);
//准备request对象
IndexRequest request = new IndexRequest("items").id(itemDoc.getId());
//准备参数
request.source(jsonStr, XContentType.JSON);
//发送请求
restHighLevelClient.index(request, RequestOptions.DEFAULT);
log.info("监听到RabbitMQ发送的新增商品的通知信息,开始同步更新索引库的数据");
}
/**
* 监听更新商品状态 局部更新 update
*/
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "item.updateStatus.queue", durable = "true"),
exchange = @Exchange(name = "item.direct", type = ExchangeTypes.DIRECT),
key = "item.updateStatus"
))
public void listenUpdateItemStatus(Long id) throws IOException {
//读取数据库对象
ItemDTO itemDTO = itemClient.queryItemById(id);
if (itemDTO == null) {
return;
}
//转换
ItemDoc itemDoc = BeanUtil.copyProperties(itemDTO, ItemDoc.class);
//Json化
String jsonStr = JSONUtil.toJsonStr(itemDoc);
//准备request对象
UpdateRequest request = new UpdateRequest("items", itemDoc.getId());
//准备参数
request.doc(jsonStr, XContentType.JSON);
//发送请求
restHighLevelClient.update(request, RequestOptions.DEFAULT);
log.info("监听到RabbitMQ发送的局部更新商品状态的通知信息,开始同步更新索引库的数据");
}
/**
* 监听更新商品 全局更新 index
*/
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "item.index.update.queue", durable = "true"),
exchange = @Exchange(name = "item.direct", type = ExchangeTypes.DIRECT),
key = "item.index.update"
))
public void listenUpdateItem(Long id) throws IOException {
//读取数据库对象
ItemDTO itemDTO = itemClient.queryItemById(id);
if (itemDTO == null) {
return;
}
//转换
ItemDoc itemDoc = BeanUtil.copyProperties(itemDTO, ItemDoc.class);
//Json化
String jsonStr = JSONUtil.toJsonStr(itemDoc);
//准备request对象
IndexRequest request = new IndexRequest("items").id(itemDoc.getId());
//准备参数
request.source(jsonStr, XContentType.JSON);
//发送请求
restHighLevelClient.index(request, RequestOptions.DEFAULT);
log.info("监听到RabbitMQ发送的全局更新商品的通知信息,开始同步更新索引库的数据");
}
/**
* 监听删除商品 delete
*/
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "item.delete.queue", durable = "true"),
exchange = @Exchange(name = "item.direct", type = ExchangeTypes.DIRECT),
key = "item.delete"
))
public void listenDeleteItem(Long id) throws IOException {
//准备request对象
DeleteRequest request = new DeleteRequest("items", id.toString());
//发送请求
restHighLevelClient.delete(request, RequestOptions.DEFAULT);
log.info("监听到RabbitMQ发送的删除商品的通知信息,开始同步更新索引库的数据");
}
}
由于要读取数据库对象,所以要注入ItemClient,其实就是调用第二个作业的商品查询接口。然后发送信息需要注入RestHighLevelClient
一共四个监听方法我就拿一个举例了,注解上queue名字按自己想法取,交换机和key记得和前面controller的保持一致。从读取数据库对象到发送请求中间的这段代码有两个是index是重复的,一个是update,重复的可以抽取出一个方法变得更优雅。然后这些rabbitMQ的一系列名字也可以放到一个MQConstants常量类里面去,这些都是优化方案自己测试成功之后再来慢慢调整
最后在ItemApplication中加上一行注解,因为你远程调用了api的client,如果不加上这个注解启动服务的时候是会爆红的
@EnableFeignClients(basePackages = "com.hmall.api.client", defaultConfiguration = DefaultFeignConfig.class)
七、测试消息监听是否成功
正常启动所有该启动的服务,然后通过swagger调试(网址:http://localhost:8081/doc.html#),观察控制台是否有日志以及数据库变化
我这里拿更新商品状态测试,将商品状态由1改为2,然后再由2改为1
数据库我的datagrip查出来是没问题的,主要是看控制台的日志输出。显然成功了,当然这些都是很简单的实现,但是成功实现后续才能更好的优化,一开跳太多步后续找bug很难受