学习笔记:SpringCloud 微服务技术栈_实用篇②_黑马旅游案例
- 若文章内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系博主删除。
前言
- 学习视频链接
- SpringCloud + RabbitMQ + Docker + Redis + 搜索 + 分布式,史上最全面的 SpringCloud 微服务技术栈课程 | 黑马程序员 Java 微服务
- 学习资料链接
- https://pan.baidu.com/s/169SFtYEvel44hRJhmFTRTQ(
提取码:1234
)
- 写这篇博客旨在制作笔记,巩固知识。同时方便个人在线阅览,回顾知识。
- 博客的内容主要来自视频内容和资料中提供的学习笔记。
- 强调:本博客主要是对 【SpringCloud 微服务技术栈 | 实用篇① | 基础知识】 内容的补充
系列目录
SpringCloud 微服务技术栈_实用篇①_基础知识
SpringCloud 微服务技术栈_实用篇②_黑马旅游案例
SpringCloud 微服务技术栈_高级篇①_微服务保护
SpringCloud 微服务技术栈_高级篇②_分布式事务
SpringCloud 微服务技术栈_高级篇③_分布式缓存
SpringCloud 微服务技术栈_高级篇④_多级缓存
SpringCloud 微服务技术栈_高级篇⑤_可靠消息服务
0.微服务技术栈导学
- 以下为视频中的截图
# SpringCloudDay06
1.项目简述
通过该案例来实战演练下之前所学知识。
实现四部分功能:
- 酒店搜索和分页
- 酒店结果过滤
- 周边的酒店
- 酒店竞价排名
启动资料中提供的 hotel-demo
项目,其默认端口是 8089,访问 http://localhost:8090
,就能看到项目页面了。
- 课前资料链接:https://pan.baidu.com/s/169SFtYEvel44hRJhmFTRTQ(
提取码:1234
)1.微服务开发框架 SpringCloud + RabbitMQ + Docker + Redis + 搜索 + 分布式史上最全面的微服务全技术栈课程>
实用篇>学习资料>day06-Elasticsearch02>代码
2.酒店搜索和分页
需求:实现黑马旅游的酒店搜索功能,完成关键字搜索和分页
2.1.需求分析
在项目的首页,有一个大大的搜索框,还有分页按钮
点击搜索按钮,可以看到浏览器控制台发出了请求
请求参数如下
由此可以知道,我们这个请求的信息如下
- 请求方式:POST
- 请求路径:
/hotel/list
- 请求参数:JSON 对象,包含 4 个字段:
key
:搜索关键字page
:页码size
:每页大小sortBy
:排序,目前暂不实现
- 返回值:分页查询,需要返回分页结果 PageResult,包含两个属性:
total
:总条数List<HotelDoc>
:当前页的数据
因此,我们实现业务的流程如下
- 步骤一:定义实体类,接收前端请求:请求参数的 JSON 对象
- 步骤二:编写 controller,接收页面的请求,调用 IHotelService 的 search 方法
- 步骤三:编写业务实现,定义 IHotelService 中的 search 方法,利用 RestHighLevelClient 中的 match 查询实现搜索、分页
2.2.定义实体类
实体类有两个,一个是前端的请求参数实体,一个是服务端应该返回的响应结果实体。
2.2.1.请求参数
前端请求的 json 结构如下
{"key": "搜索关键字","page": 1,"size": 3,"sortBy": "default"
}
因此,我们在 cn.itcast.hotel.pojo
包下定义一个实体类
src/main/java/cn/itcast/hotel/pojo/RequestParams.java
package cn.itcast.hotel.pojo;import lombok.Data;@Data
public class RequestParams {private String key;private Integer page;private Integer size;private String sortBy;
}
2.2.2.返回值
分页查询,需要返回分页结果 PageResult,包含两个属性
total
:总条数List<HotelDoc>
:当前页的数据
因此,我们在 cn.itcast.hotel.pojo
中定义返回结果
src/main/java/cn/itcast/hotel/pojo/PageResult.java
package cn.itcast.hotel.pojo;import lombok.Data;import java.util.List;@Data
public class PageResult {private Long total;private List<HotelDoc> hotels;public PageResult() {}public PageResult(Long total, List<HotelDoc> hotels) {this.total = total;this.hotels = hotels;}
}
2.3.定义 controller
定义一个 HotelController,声明查询接口,满足下列要求:
- 请求方式:Post
- 请求路径:
/hotel/list
- 请求参数:对象,类型为 RequestParam
- 返回值:PageResult,包含两个属性
Long total
:总条数List<HotelDoc> hotels
:酒店数据
因此,我们在 cn.itcast.hotel.web
中定义 HotelController
src/main/java/cn/itcast/hotel/web/HotelController.java
@RestController
@RequestMapping("/hotel")
public class HotelController {@Autowiredprivate IHotelService hotelService;// 搜索酒店数据@PostMapping("/list")public PageResult search(@RequestBody RequestParams params){return hotelService.search(params);}
}
2.4.实现搜索业务
我们在 controller 调用了 IHotelService,并没有实现该方法。
因此下面我们就在 IHotelService 中定义方法,并且去实现业务逻辑。
2.4.1.在接口中定义方法
在 cn.itcast.hotel.service
中的 IHotelService
接口中定义一个方法
src/main/java/cn/itcast/hotel/service/IHotelService.java
/*** 根据关键字搜索酒店信息* * @param params 请求参数对象,包含用户输入的关键字 * @return 酒店文档列表*/
PageResult search(RequestParams params);
2.4.2.注入 es 客户端组件
实现搜索业务,肯定离不开 RestHighLevelClient,我们需要把它注册到 Spring 中作为一个 Bean。
在 cn.itcast.hotel
中的HotelDemoApplication
中声明这个 Bean
src/main/java/cn/itcast/hotel/HotelDemoApplication.java
@Bean
public RestHighLevelClient client(){return new RestHighLevelClient(RestClient.builder(HttpHost.create("http://192.168.150.101:9200")));
}
2.4.3.实现业务逻辑
在 cn.itcast.hotel.service.impl
中的 HotelService
中实现 search 方法
src/main/java/cn/itcast/hotel/service/impl/HotelService.java
@Autowired
private RestHighLevelClient client;
@Override
public PageResult search(RequestParams params) {try {// 1.准备 RequestSearchRequest request = new SearchRequest("hotel");// 2.准备 DSL// 2.1.queryString key = params.getKey();if (key == null || "".equals(key)) {request.source().query(QueryBuilders.matchAllQuery());} else {request.source().query(QueryBuilders.matchQuery("all", key));}// 2.2.分页int page = params.getPage();int size = params.getSize();request.source().from((page - 1) * size).size(size);// 3.发送请求SearchResponse response = client.search(request, RequestOptions.DEFAULT);// 4.解析响应return handleResponse(response);} catch (IOException e) {throw new RuntimeException(e);}
}
// 结果解析
private PageResult handleResponse(SearchResponse response) {// 4.解析响应SearchHits searchHits = response.getHits();// 4.1.获取总条数long total = searchHits.getTotalHits().value;// 4.2.文档数组SearchHit[] hits = searchHits.getHits();// 4.3.遍历List<HotelDoc> hotels = new ArrayList<>();for (SearchHit hit : hits) {// 获取文档 sourceString json = hit.getSourceAsString();// 反序列化HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);// 放入集合hotels.add(hotelDoc);}// 4.4.封装返回return new PageResult(total, hotels);
}
3.酒店结果过滤
需求:添加品牌、城市、星级、价格等过滤功能
3.1.需求分析
在页面搜索框下面,会有一些过滤项
传递的参数如图
包含的过滤条件有
brand
:品牌值city
:城市minPrice~maxPrice
:价格范围starName
:星级
我们需要做两件事情
- 修改请求参数的对象 RequestParams,接收上述参数
- 修改业务逻辑,在搜索条件之外,添加一些过滤条件
3.2.修改实体类
修改在 cn.itcast.hotel.pojo
包下的实体类 RequestParams
src/main/java/cn/itcast/hotel/pojo/RequestParams.java
@Data
public class RequestParams {private String key;private Integer page;private Integer size;private String sortBy;/* 下面是新增的过滤条件参数 */private String city;private String brand;private String starName;private Integer minPrice;private Integer maxPrice;
}
3.3.修改搜索业务
在 HotelService 的 search 方法中,只有一个地方需要修改:requet.source().query( ... )
其中的查询条件。
在之前的业务中,只有 match 查询,根据关键字搜索,现在要添加条件过滤,包括:
- 品牌过滤:是 keyword 类型,用 term 查询
- 星级过滤:是 keyword 类型,用 term 查询
- 价格过滤:是数值类型,用 range 查询
- 城市过滤:是 keyword 类型,用 term 查询
多个查询条件组合,肯定是 boolean 查询来组合:
- 关键字搜索放到 must 中,参与算分
- 其它过滤条件放到 filter 中,不参与算分
因为条件构建的逻辑比较复杂,这里先封装为一个函数
src/main/java/cn/itcast/hotel/service/impl/HotelService.java
buildBasicQuery(params, request);
补充:封装方法快捷键:
Ctrl + Alt + M
方法 buildBasicQuery 的代码如下
src/main/java/cn/itcast/hotel/service/impl/HotelService.java
private void buildBasicQuery(RequestParams params, SearchRequest request) {// 1.构建 BooleanQueryBoolQueryBuilder boolQuery = QueryBuilders.boolQuery();// 2.关键字搜索String key = params.getKey();if (key == null || "".equals(key)) {boolQuery.must(QueryBuilders.matchAllQuery());} else {boolQuery.must(QueryBuilders.matchQuery("all", key));}// 3.城市条件if (params.getCity() != null && !params.getCity().equals("")) {boolQuery.filter(QueryBuilders.termQuery("city", params.getCity()));}// 4.品牌条件if (params.getBrand() != null && !params.getBrand().equals("")) {boolQuery.filter(QueryBuilders.termQuery("brand", params.getBrand()));}// 5.星级条件if (params.getStarName() != null && !params.getStarName().equals("")) {boolQuery.filter(QueryBuilders.termQuery("starName", params.getStarName()));}// 6.价格if (params.getMinPrice() != null && params.getMaxPrice() != null) {boolQuery.filter(QueryBuilders.rangeQuery("price").gte(params.getMinPrice()).lte(params.getMaxPrice()));}// 7.放入 sourcerequest.source().query(boolQuery);
}
4.周边的酒店
4.1.需求分析
在酒店列表页的右侧,有一个小地图,点击地图的定位按钮,地图会找到你所在的位置
并且,在前端会发起查询请求,将你的坐标发送到服务端
我们要做的事情就是基于这个 location 坐标,然后按照距离对周围酒店排序。实现思路如下
- 修改 RequestParams 参数,接收 location 字段
- 修改 search 方法业务逻辑,如果 location 有值,添加根据
geo_distance
排序的功能
4.2.修改实体类
修改在cn.itcast.hotel.pojo
包下的实体类RequestParams:
src/main/java/cn/itcast/hotel/pojo/RequestParams.java
package cn.itcast.hotel.pojo;import lombok.Data;@Data
public class RequestParams {private String key;private Integer page;private Integer size;private String sortBy;private String city;private String brand;private String starName;private Integer minPrice;private Integer maxPrice;//当前的地理坐标private String location;
}
4.3.距离排序 API
我们以前学习过排序功能,包括两种:
- 普通字段排序
- 地理坐标排序
我们只讲了普通字段排序对应的 java 写法。地理坐标排序只学过 DSL 语法。
距离排序与普通字段的排序有所差异,具体情况如下:
GET /indexName/_search
{"query": {"match_all": {}},"sort": [{"price": "asc" },{"_geo_distance" : {"FIELD" : "纬度,经度","order" : "asc","unit" : "km"}}]
}
对应的 java 代码示例
4.4.添加距离排序
在 cn.itcast.hotel.service.impl
的 HotelService
的 search
方法中,添加一个排序功能
src/main/java/cn/itcast/hotel/service/impl/HotelService.java
完整代码
@Override
public PageResult search(RequestParams params) {try {// 1.准备RequestSearchRequest request = new SearchRequest("hotel");// 2.准备DSL// 2.1.querybuildBasicQuery(params, request);// 2.2.分页int page = params.getPage();int size = params.getSize();request.source().from((page - 1) * size).size(size);// 2.3.排序String location = params.getLocation();if (location != null && !location.equals("")) {request.source().sort(SortBuilders.geoDistanceSort("location", new GeoPoint(location)).order(SortOrder.ASC).unit(DistanceUnit.KILOMETERS));}// 3.发送请求SearchResponse response = client.search(request, RequestOptions.DEFAULT);// 4.解析响应return handleResponse(response);} catch (IOException e) {throw new RuntimeException(e);}
}
4.5.排序距离显示
4.5.1.解析
重启服务后,测试功能
发现确实可以实现对我附近酒店的排序,不过并没有看到酒店到底距离我多远,这该怎么办?
排序完成后,页面还要获取我附近每个酒店的具体距离值,这个值在响应结果中是独立的
因此,我们在结果解析阶段,除了解析source部分以外,还要得到sort部分,也就是排序的距离,然后放到响应结果中。
我们要做两件事:
- 修改 HotelDoc,添加排序距离字段,用于页面显示
- 修改 HotelService 类中的 handleResponse 方法,添加对 sort 值的获取
4.5.2.实体类添加距离字段
修改 HotelDoc 类,添加距离字段
src/main/java/cn/itcast/hotel/pojo/HotelDoc.java
package cn.itcast.hotel.pojo;import lombok.Data;
import lombok.NoArgsConstructor;@Data
@NoArgsConstructor
public class HotelDoc {private Long id;private String name;private String address;private Integer price;private Integer score;private String brand;private String city;private String starName;private String business;private String location;private String pic;// 排序时的 距离值private Object distance;public HotelDoc(Hotel hotel) {this.id = hotel.getId();this.name = hotel.getName();this.address = hotel.getAddress();this.price = hotel.getPrice();this.score = hotel.getScore();this.brand = hotel.getBrand();this.city = hotel.getCity();this.starName = hotel.getStarName();this.business = hotel.getBusiness();this.location = hotel.getLatitude() + ", " + hotel.getLongitude();this.pic = hotel.getPic();}
}
4.5.3.添加排序业务
修改 HotelService 中的 handleResponse 方法
src/main/java/cn/itcast/hotel/service/impl/HotelService.java
// 获取排序值
Object[] sortValues = hit.getSortValues();
if (sortValues.length > 0) {Object sortValue = sortValues[0];hotelDoc.setDistance(sortValue);
}
重启后测试,发现页面能成功显示距离了
5.酒店竞价排名
需求:让指定的酒店在搜索结果中排名置顶
5.1.需求分析
要让指定酒店在搜索结果中排名置顶,效果如图
页面会给指定的酒店添加广告标记。
那怎样才能让指定的酒店排名置顶呢?
我们之前学习过的 function_score
查询可以影响算分,算分高了,自然排名也就高了。
而 function_score
包含 3 个要素:
- 过滤条件:哪些文档要加分
- 算分函数:如何计算
function score
- 加权方式:
function score
与query score
如何运算
这里的需求是:让指定酒店排名靠前。
因此我们需要给这些酒店添加一个标记,这样在过滤条件中就可以根据这个标记来判断,是否要提高算分。
比如,我们给酒店添加一个字段:isAD,Boolean 类型:
true
:是广告false
:不是广告
这样 function_score
包含 3 个要素就很好确定了:
- 过滤条件:判断 isAD 是否为 true
- 算分函数:我们可以用最简单暴力的 weight,固定加权值
- 加权方式:可以用默认的相乘,大大提高算分
因此,业务的实现步骤包括
- 给 HotelDoc 类添加 isAD 字段,Boolean 类型
- 挑选几个你喜欢的酒店,给它的文档数据添加 isAD 字段,值为 true
- 修改 search 方法,添加 function score 功能,给 isAD 值为 true 的酒店增加权重
5.2.修改 HotelDoc 实体
给 cn.itcast.hotel.pojo
包下的 HotelDoc 类添加 isAD 字段
src/main/java/cn/itcast/hotel/pojo/HotelDoc.java
private Boolean isAD;
5.3.添加广告标记
接下来,我们挑几个酒店,添加 isAD 字段,设置为 true
# 事实上这个值(1902197537 )是没有的
POST /hotel/_update/1902197537
{"doc": {"isAD": true}
}
POST /hotel/_update/2056126831
{"doc": {"isAD": true}
}
POST /hotel/_update/1989806195
{"doc": {"isAD": true}
}
POST /hotel/_update/2056105938
{"doc": {"isAD": true}
}
然后就报错了。
究其原因是视频里创建索引库的时候,并没有创建 isAD 这个字段。
参考博客:https://blog.csdn.net/weixin_44757863/article/details/120959505
只需在 kibana 控制台执行(追加该字段)代码即可
# 给索引库新增一个叫 isAD 的字段,类型是布尔类型
PUT /hotel/_mapping
{"properties":{"isAD":{"type": "boolean"}}
}
# 给索引库 id 为 45845 的记录赋值,让其 isAD 字段为 true(用于测试广告竞价排名,该记录会靠前)
POST /hotel/_update/45845
{"doc": { "isAD":true}
}
GET hotel/_doc/45845
5.4.添加算法函数查询
接下来我们就要修改查询条件了。之前是用的 boolean 查询,现在要改成 function_socre
查询。
function_score
查询结构如下
对应的 JavaAPI 如下
我们可以将之前写的 boolean 查询作为原始查询条件放到 query 中,
接下来就是添加过滤条件、算分函数、加权模式了。所以原来的代码依然可以沿用。
修改 cn.itcast.hotel.service.impl
包下的 HotelService
类中的 buildBasicQuery
方法,添加算分函数查询:
src/main/java/cn/itcast/hotel/service/impl/HotelService.java
private void buildBasicQuery(RequestParams params, SearchRequest request) {// 1.构建BooleanQueryBoolQueryBuilder boolQuery = QueryBuilders.boolQuery();// 关键字搜索String key = params.getKey();if (key == null || "".equals(key)) {boolQuery.must(QueryBuilders.matchAllQuery());} else {boolQuery.must(QueryBuilders.matchQuery("all", key));}// 城市条件if (params.getCity() != null && !params.getCity().equals("")) {boolQuery.filter(QueryBuilders.termQuery("city", params.getCity()));}// 品牌条件if (params.getBrand() != null && !params.getBrand().equals("")) {boolQuery.filter(QueryBuilders.termQuery("brand", params.getBrand()));}// 星级条件if (params.getStarName() != null && !params.getStarName().equals("")) {boolQuery.filter(QueryBuilders.termQuery("starName", params.getStarName()));}// 价格if (params.getMinPrice() != null && params.getMaxPrice() != null) {boolQuery.filter(QueryBuilders.rangeQuery("price").gte(params.getMinPrice()).lte(params.getMaxPrice()));}// 2.算分控制FunctionScoreQueryBuilder functionScoreQuery =QueryBuilders.functionScoreQuery(// 原始查询,相关性算分的查询boolQuery,// function score的数组new FunctionScoreQueryBuilder.FilterFunctionBuilder[]{// 其中的一个function score 元素new FunctionScoreQueryBuilder.FilterFunctionBuilder(// 过滤条件QueryBuilders.termQuery("isAD", true),// 算分函数ScoreFunctionBuilders.weightFactorFunction(10))});request.source().query(functionScoreQuery);
}
# SpringCloudDay07
6.多条件的数据聚合
6.1.业务需求
需求:搜索页面的品牌、城市等信息不应该是在页面写死,而是通过聚合索引库中的酒店数据得来的
分析
目前,页面的城市列表、星级列表、品牌列表都是写死的,并不会随着搜索结果的变化而变化。
但是用户搜索条件改变时,搜索结果会跟着变化。
例如
用户搜索 “东方明珠”,那搜索的酒店肯定是在上海东方明珠附近。
因此,城市只能是上海,此时城市列表中就不应该显示北京、深圳、杭州这些信息了。
也就是说,搜索结果中包含哪些城市,页面就应该列出哪些城市;搜索结果中包含哪些品牌,页面就应该列出哪些品牌。
那么如何得知搜索结果中包含哪些品牌?如何得知搜索结果中包含哪些城市?
使用聚合功能,利用 Bucket 聚合,对搜索结果中的文档基于品牌分组、基于城市分组,就能得知包含哪些品牌、哪些城市了。
因为是对搜索结果聚合,因此聚合是限定范围的聚合,也就是说聚合的限定条件跟搜索文档的条件一致。
查看浏览器可以发现,请求参数与之前 search 时的 RequestParam 完全一致,即请求参数与搜索文档的参数完全一致。
这是在限定聚合时的文档范围。
返回值类型就是页面要展示的最终结果
结果是一个 Map 结构:
key
是字符串,城市、星级、品牌、价格value
是集合,例如多个城市的名称
6.2.业务实现
在 cn.itcast.hotel.web
包的 HotelController
中添加一个方法,遵循下面的要求:
- 请求方式:
POST
- 请求路径:
/hotel/filters
- 请求参数:
RequestParams
,与搜索文档的参数一致 - 返回值类型:
Map<String, List<String>>
这里调用了 IHotelService 中的 getFilters 方法,但尚未实现。
src/main/java/cn/itcast/hotel/web/HotelController.java
@PostMapping("filters")
public Map<String, List<String>> getFilters(@RequestBody RequestParams params){return hotelService.getFilters(params);
}
在cn.itcast.hotel.service.IHotelService
中定义新方法
src/main/java/cn/itcast/hotel/service/IHotelService.java
/*** 查询城市、星级、品牌的聚合结果** @return 聚合结果,格式:{“城市”:[“上海”],“品牌”:[“如家”,“希尔顿”]}*/
Map<String, List<String>> filters(RequestParams params);
在cn.itcast.hotel.service.impl.HotelService
中实现该方法
src/main/java/cn/itcast/hotel/service/impl/HotelService.java
@Override
public Map<String, List<String>> filters(RequestParams params) {try {// 1.准备RequestSearchRequest request = new SearchRequest("hotel");// 2.准备DSL// 2.1.querybuildBasicQuery(params, request);// 2.2.设置sizerequest.source().size(0);// 2.3.聚合buildAggregation(request);// 3.发出请求SearchResponse response = client.search(request, RequestOptions.DEFAULT);// 4.解析结果Map<String, List<String>> result = new HashMap<>();Aggregations aggregations = response.getAggregations();// 4.1.根据品牌名称,获取品牌结果List<String> brandList = getAggByName(aggregations, "brandAgg");result.put("品牌", brandList);// 4.2.根据品牌名称,获取品牌结果List<String> cityList = getAggByName(aggregations, "cityAgg");result.put("城市", cityList);// 4.3.根据品牌名称,获取品牌结果List<String> starList = getAggByName(aggregations, "starAgg");result.put("星级", starList);return result;} catch (IOException e) {throw new RuntimeException(e);}
}
获取聚合名称
private void buildAggregation(SearchRequest request) {request.source().aggregation(AggregationBuilders.terms("brandAgg").field("brand").size(100));request.source().aggregation(AggregationBuilders.terms("cityAgg").field("city").size(100));request.source().aggregation(AggregationBuilders.terms("starAgg").field("starName").size(100));
}
封装聚合条件
private List<String> getAggByName(Aggregations aggregations, String aggName) {// 4.1.根据聚合名称获取聚合结果Terms brandTerms = aggregations.get(aggName);// 4.2.获取bucketsList<? extends Terms.Bucket> buckets = brandTerms.getBuckets();// 4.3.遍历List<String> brandList = new ArrayList<>();for (Terms.Bucket bucket : buckets) {// 4.4.获取keyString key = bucket.getKeyAsString();brandList.add(key);}return brandList;
}
7.实现酒店搜索框自动补全
此时我们的 hotel 索引库还没有设置拼音分词器,需要修改索引库中的配置。
但是我们知道索引库是无法修改的,只能删除然后重新创建。
另外,我们需要添加一个字段,用来做自动补全,将 brand、suggestion、city 等都放进去,作为自动补全的提示。
因此,总结一下,我们需要做的事情包括:
- 修改 hotel 索引库结构,设置自定义拼音分词器
- 修改索引库的 name、all 字段,使用自定义分词器
- 索引库添加一个新字段 suggestion,类型为 completion 类型,使用自定义的分词器
- 给 HotelDoc 类添加 suggestion 字段,内容包含 brand、business
- 重新导入数据到 hotel 库
7.1.修改酒店映射结构
先删除之前创建的索引库
DELETE /hotel
再创建新的索引库(映射结构发生变化)
// 酒店数据索引库
PUT /hotel
{"settings": {"analysis": {"analyzer": {"text_anlyzer": {"tokenizer": "ik_max_word","filter": "py"},"completion_analyzer": {"tokenizer": "keyword","filter": "py"}},"filter": {"py": {"type": "pinyin","keep_full_pinyin": false,"keep_joined_full_pinyin": true,"keep_original": true,"limit_first_letter_length": 16,"remove_duplicated_term": true,"none_chinese_pinyin_tokenize": false}}}},"mappings": {"properties": {"id":{"type": "keyword"},"name":{"type": "text","analyzer": "text_anlyzer","search_analyzer": "ik_smart","copy_to": "all"},"address":{"type": "keyword","index": false},"price":{"type": "integer"},"score":{"type": "integer"},"brand":{"type": "keyword","copy_to": "all"},"city":{"type": "keyword"},"starName":{"type": "keyword"},"business":{"type": "keyword","copy_to": "all"},"location":{"type": "geo_point"},"pic":{"type": "keyword","index": false},"all":{"type": "text","analyzer": "text_anlyzer","search_analyzer": "ik_smart"},"suggestion":{"type": "completion","analyzer": "completion_analyzer"}}}
}
7.2.修改 HotelDoc 实体
HotelDoc 中要添加一个字段,用来做自动补全,内容可以是酒店品牌、城市、商圈等信息。
按照自动补全字段的要求,最好是这些字段的数组。
因此我们在 HotelDoc 中添加一个 suggestion 字段,类型为 List<String>
,然后将 brand、city、business 等信息放到里面。
代码如下:
src/main/java/cn/itcast/hotel/pojo/HotelDoc.java
package cn.itcast.hotel.pojo;import lombok.Data;
import lombok.NoArgsConstructor;import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;@Data
@NoArgsConstructor
public class HotelDoc {private Long id;private String name;private String address;private Integer price;private Integer score;private String brand;private String city;private String starName;private String business;private String location;private String pic;private Object distance;private Boolean isAD;private List<String> suggestion;public HotelDoc(Hotel hotel) {this.id = hotel.getId();this.name = hotel.getName();this.address = hotel.getAddress();this.price = hotel.getPrice();this.score = hotel.getScore();this.brand = hotel.getBrand();this.city = hotel.getCity();this.starName = hotel.getStarName();this.business = hotel.getBusiness();this.location = hotel.getLatitude() + ", " + hotel.getLongitude();this.pic = hotel.getPic();// 组装 suggestionif(this.business.contains("/")){// business 有多个值,需要切割String[] arr = this.business.split("/");// 添加元素this.suggestion = new ArrayList<>();this.suggestion.add(this.brand);Collections.addAll(this.suggestion, arr);}else {this.suggestion = Arrays.asList(this.brand, this.business);}}
}
7.3.重新导入
重新执行之前编写的导入数据功能,可以看到新的酒店数据中包含了 suggestion
相关的导入功能在 src/test/java/cn/itcast/hotel/HotelDocumentTest.java
中的 testBulkRequest()
方法中实现了。
GET /hotel/_search
{"query": {"match_all": {}}
}
GET /hotel/_search
{"suggest": {"suggestions": {"text": "s","completion": {"field": "suggestion","skip_duplicates": true,"size": 10}}}
}
7.4.自动补全查询的 JavaAPI
之前我们学习了自动补全查询的 DSL,而没有学习对应的 JavaAPI,这里给出一个示例
而自动补全的结果也比较特殊,解析的代码如下
src/test/java/cn/itcast/hotel/HotelSearchTest.java
/*** 自动补全查询** @throws IOException*/
@Test
void testSuggest() throws IOException {//1.准备 RequestSearchRequest request = new SearchRequest("hotel");//2.准备 DSLrequest.source().suggest(new SuggestBuilder().addSuggestion("suggestions",SuggestBuilders.completionSuggestion("suggestion").prefix("h").skipDuplicates(true).size(10)));//3.发送请求SearchResponse response = client.search(request, RequestOptions.DEFAULT);//4.解析结果//System.out.println(response);handleCompletionResponse(response);
}
/*** 处理补全结果** @param response*/
private void handleCompletionResponse(SearchResponse response) {//4.处理结果Suggest suggest = response.getSuggest();//4.1.根据名称获取补全结果CompletionSuggestion suggestion = suggest.getSuggestion("suggestions");//4.2.获取 options 并遍历for (CompletionSuggestion.Entry.Option option : suggestion.getOptions()) {//4.3.获取一个 option 的 text ,也就是补全的词条String text = option.getText().string();System.out.println(text);}
}
7.5.实现搜索框自动补全
查看前端页面,可以发现当我们在输入框键入时,前端会发起 ajax 请求
返回值是补全词条的集合,类型为 List<String>
- 在
cn.itcast.hotel.web
包下的HotelController
中添加新接口,接收新的请求
src/main/java/cn/itcast/hotel/web/HotelController.java
@GetMapping("suggestion")
public List<String> getSuggestions(@RequestParam("key") String prefix) {return hotelService.getSuggestions(prefix);
}
- 在
cn.itcast.hotel.service
包下的IhotelService
中添加方法
src/main/java/cn/itcast/hotel/service/IHotelService.java
List<String> getSuggestions(String prefix);
- 在
cn.itcast.hotel.service.impl.HotelService
中实现该方法
src/main/java/cn/itcast/hotel/service/impl/HotelService.java
@Override
public List<String> getSuggestions(String prefix) {try {// 1.准备 RequestSearchRequest request = new SearchRequest("hotel");// 2.准备 DSLrequest.source().suggest(new SuggestBuilder().addSuggestion("suggestions",SuggestBuilders.completionSuggestion("suggestion").prefix(prefix).skipDuplicates(true).size(10)));// 3.发起请求SearchResponse response = client.search(request, RequestOptions.DEFAULT);// 4.解析结果Suggest suggest = response.getSuggest();// 4.1.根据补全查询名称,获取补全结果CompletionSuggestion suggestions = suggest.getSuggestion("suggestions");// 4.2.获取 optionsList<CompletionSuggestion.Entry.Option> options = suggestions.getOptions();// 4.3.遍历List<String> list = new ArrayList<>(options.size());for (CompletionSuggestion.Entry.Option option : options) {String text = option.getText().toString();list.add(text);}return list;} catch (IOException e) {throw new RuntimeException(e);}
}
8.实现数据同步
elasticsearch 中的酒店数据来自于 mysql 数据库,因此 mysql 数据发生改变时,elasticsearch 也必须跟着改变。
这个就是 elasticsearch 与 mysql 之间的数据同步。
在微服务中,负责酒店管理(操作 MySQL)的业务与负责酒店搜索(操作 ElasticSearch)的业务可能在两台不同的微服务上。
那么此时的数据同步应该如何实现呢?
常见的数据同步方案有三种:同步调用、异步通知、监听 binlog
案例:基于
MQ
来实现MySQL
与ElsaticSearch
数据同步
8.1.思路
利用课前资料提供的 hotel-admin
项目作为酒店管理的微服务。
当酒店数据发生增、删、改时,要求对 elasticsearch 中数据也要完成相同操作。
步骤
- 导入 课前资料 提供的
hotel-admin
项目,启动并测试酒店数据的 CRUD - 声明 exchange、queue、RoutingKey
- 在
hotel-admin
中的增、删、改业务中完成消息发送 - 在
hotel-demo
中完成消息监听,并更新 elasticsearch 中数据 - 启动并测试数据同步功能
8.2.导入 demo
导入 课前资料 提供的 hotel-admin
项目
运行后,访问 http://localhost:8099
其中包含了酒店的 CRUD 功能
hotel-admin
项目下的 src/main/java/cn/itcast/hotel/web/HotelController.java
@PostMapping
public void saveHotel(@RequestBody Hotel hotel){hotelService.save(hotel);
}@PutMapping()
public void updateById(@RequestBody Hotel hotel){if (hotel.getId() == null) {throw new InvalidParameterException("id 不能为空");}hotelService.updateById(hotel);
}@DeleteMapping("/{id}")
public void deleteById(@PathVariable("id") Long id) {hotelService.removeById(id);
}
8.3.声明交换机、队列
MQ 结构如图
8.3.1.配置文件
引入依赖
在 hotel-admin
、hotel-demo
中引入 RabbitMQ 的依赖
pom.xml
<!-- AMQP -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
配置 RabbitMQ 地址
在 hotel-admin
、hotel-demo
项目中的 src/main/resources/application.yaml
下配置 RabbitMQ 的地址
spring:rabbitmq:host: 192.168.150.101port: 5672username: itcastpassword: 123456virtual-host: /
这里补充一句,记得开启 RabbitMQ 服务
使用 docker start [你使用 Docker 创建的 RabbitMQ 容器的名称]
启动即可
docker start mq
8.3.2.声明队列交换机的名称
在 hotel-admin
和 hotel-demo
中的 cn.itcast.hotel.constatnts
包下新建一个类 MqConstants
src/main/java/cn/itcast/hotel/constants/MqConstants.java
package cn.itcast.hotel.constatnts;public class MqConstants {/*** 交换机*/
public final static String HOTEL_EXCHANGE = "hotel.topic";
/*** 监听新增和修改的队列*/
public final static String HOTEL_INSERT_QUEUE = "hotel.insert.queue";
/*** 监听删除的队列*/
public final static String HOTEL_DELETE_QUEUE = "hotel.delete.queue";
/*** 新增或修改的 RoutingKey*/
public final static String HOTEL_INSERT_KEY = "hotel.insert";
/*** 删除的 RoutingKey*/
public final static String HOTEL_DELETE_KEY = "hotel.delete";
}
8.3.3.声明队列交换机
一般都是在消费者中声明交换机、队列的。
故选择在 hotel-demo
项目中定义配置类(声明队列、交换机)
一般来说,有两种方式来声明交换机和队列的绑定关系、以及队列和交换机的对象
两种方式:1.基于注解的方式;2.基于 Bean 的方式
这里是基于 Bean 的方式来声明队列交换机。资料中提供的最终代码则是基于注解来声明队列交换机的。
src/main/java/cn/itcast/hotel/config/MqConfig.java
package cn.itcast.hotel.config;import cn.itcast.hotel.constants.MqConstants;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;@Configuration
public class MqConfig {@Beanpublic TopicExchange topicExchange(){return new TopicExchange(MqConstants.HOTEL_EXCHANGE, true, false);}@Beanpublic Queue insertQueue(){return new Queue(MqConstants.HOTEL_INSERT_QUEUE, true);}@Beanpublic Queue deleteQueue(){return new Queue(MqConstants.HOTEL_DELETE_QUEUE, true);}@Beanpublic Binding insertQueueBinding(){return BindingBuilder.bind(insertQueue()).to(topicExchange()).with(MqConstants.HOTEL_INSERT_KEY);}@Beanpublic Binding deleteQueueBinding(){return BindingBuilder.bind(deleteQueue()).to(topicExchange()).with(MqConstants.HOTEL_DELETE_KEY);}
}
8.4.发送 MQ 消息
在 hotel-admin
中的增、删、改业务中分别发送 MQ 消息
src/main/java/cn/itcast/hotel/web/HotelController.java
完整代码
package cn.itcast.hotel.web;import cn.itcast.hotel.constants.MqConstants;
import cn.itcast.hotel.pojo.Hotel;
import cn.itcast.hotel.pojo.PageResult;
import cn.itcast.hotel.service.IHotelService;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;import java.security.InvalidParameterException;@RestController
@RequestMapping("hotel")
public class HotelController {@Autowiredprivate IHotelService hotelService;@Autowiredprivate RabbitTemplate rabbitTemplate;@GetMapping("/{id}")public Hotel queryById(@PathVariable("id") Long id) {return hotelService.getById(id);}@GetMapping("/list")public PageResult hotelList(@RequestParam(value = "page", defaultValue = "1") Integer page,@RequestParam(value = "size", defaultValue = "1") Integer size) {Page<Hotel> result = hotelService.page(new Page<>(page, size));return new PageResult(result.getTotal(), result.getRecords());}@PostMappingpublic void saveHotel(@RequestBody Hotel hotel) {hotelService.save(hotel);//交换机、RoutingKey、要发送的内容rabbitTemplate.convertAndSend(MqConstants.HOTEL_EXCHANGE, MqConstants.HOTEL_INSERT_KEY, hotel.getId());}@PutMapping()public void updateById(@RequestBody Hotel hotel) {if (hotel.getId() == null) {throw new InvalidParameterException("id 不能为空");}hotelService.updateById(hotel);rabbitTemplate.convertAndSend(MqConstants.HOTEL_EXCHANGE, MqConstants.HOTEL_INSERT_KEY, hotel.getId());}@DeleteMapping("/{id}")public void deleteById(@PathVariable("id") Long id) {hotelService.removeById(id);rabbitTemplate.convertAndSend(MqConstants.HOTEL_EXCHANGE, MqConstants.HOTEL_DELETE_KEY, id);}
}
8.5.接收 MQ 消息
hotel-demo
接收到 MQ 消息要做的事情包括
- 新增消息:根据传递的 hotel 的 id 查询 hotel 信息,然后新增一条数据到索引库
- 删除消息:根据传递的 hotel 的 id 删除索引库中的一条数据
8.5.1.新增和删除的业务
首先在 hotel-demo
的 cn.itcast.hotel.service
包下的 IHotelService 中编写新增、删除业务的方法
src/main/java/cn/itcast/hotel/service/IHotelService.java
void deleteById(Long id);void insertById(Long id);
8.5.2.业务实现
给 hotel-demo
中的 cn.itcast.hotel.service.impl
包下的 HotelService 中实现业务
src/main/java/cn/itcast/hotel/service/impl/HotelService.java
@Override
public void deleteById(Long id) {try {// 1.准备 RequestDeleteRequest request = new DeleteRequest("hotel", id.toString());// 2.发送请求client.delete(request, RequestOptions.DEFAULT);} catch (IOException e) {throw new RuntimeException(e);}
}
@Override
public void insertById(Long id) {try {// 0.根据 id 查询酒店数据Hotel hotel = getById(id);// 转换为文档类型HotelDoc hotelDoc = new HotelDoc(hotel);// 1.准备 Request 对象IndexRequest request = new IndexRequest("hotel").id(hotel.getId().toString());// 2.准备 Json 文档request.source(JSON.toJSONString(hotelDoc), XContentType.JSON);// 3.发送请求client.index(request, RequestOptions.DEFAULT);} catch (IOException e) {throw new RuntimeException(e);}
}
8.5.3.编写监听器
在 hotel-demo
中的 cn.itcast.hotel.mq
包新增一个类
src/main/java/cn/itcast/hotel/mq/HotelListener.java
package cn.itcast.hotel.mq;import cn.itcast.hotel.constants.MqConstants;
import cn.itcast.hotel.service.IHotelService;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;@Component
public class HotelListener {@Autowiredprivate IHotelService hotelService;/*** 监听酒店新增或修改的业务* @param id 酒店id*/@RabbitListener(queues = MqConstants.HOTEL_INSERT_QUEUE)public void listenHotelInsertOrUpdate(Long id){hotelService.insertById(id);}/*** 监听酒店删除的业务* @param id 酒店id*/@RabbitListener(queues = MqConstants.HOTEL_DELETE_QUEUE)public void listenHotelDelete(Long id){hotelService.deleteById(id);}
}
8.6.测试同步功能
直接访问 虚拟机的IP地址:15672
队列
交换机
点击上方的 hotel.topic
,查看交换机和队列绑定关系
将酒店价格由 2688 改为 2888
查询此条数据的 id
将价格修改为 2888
查看交换机的 hotel.topic
情况
回到 http://localhost:8089/
页面,发现数据修改的数据同步成功
删除数据、修改的数据同步同理,此处不作演示。 (主要是懒得截图)
学习笔记:SpringCloud 微服务技术栈_实用篇②_黑马旅游案例相关推荐
- 学习笔记:SpringCloud 微服务技术栈_实用篇①_基础知识
若文章内容或图片失效,请留言反馈.部分素材来自网络,若不小心影响到您的利益,请联系博主删除. 前言 学习视频链接 SpringCloud + RabbitMQ + Docker + Redis + 搜 ...
- 学习笔记:SpringCloud 微服务技术栈_高级篇⑤_可靠消息服务
若文章内容或图片失效,请留言反馈.部分素材来自网络,若不小心影响到您的利益,请联系博主删除. 前言 学习视频链接 SpringCloud + RabbitMQ + Docker + Redis + 搜 ...
- 详解springcloud微服务技术栈(一)
1.微服务技术栈 2.认识微服务 2.1.微服务架构演变 2.2.springcloud(Spring Cloud) SpringCloud和SpringBoot的版本兼容关系如下: 2.3.微服务拆 ...
- SpringCloud学习笔记(二):微服务概述、微服务和微服务架构、微服务优缺点、微服务技术栈有哪些、SpringCloud是什么...
从技术维度理解: 微服务化的核心就是将传统的一站式应用,根据业务拆分成一个一个的服务,彻底 地去耦合,每一个微服务提供单个业务功能的服务,一个服务做一件事, 从技术角度看就是一种小而独立的处理过程,类 ...
- SpringCloud一、前提概述、相关微服务和微服务架构理论知识、微服务技术栈有哪些、
①前提概述.微服务架构springcloud的相关学习. 前提知识+相关说明 1.目前,我们学习到最后的微服务架构SpringCloud,基本上需要熟悉以前的学习内容和知识:springmvc.spr ...
- 微服务技术栈:API网关中心,落地实现方案
本文源码:GitHub·点这里 || GitEE·点这里 一.服务网关简介 1.外观模式 客户端与各个业务子系统的通信必须通过一个统一的外观对象进行,外观模式提供一个高层次的接口,使得子系统更易于使用 ...
- 微服务技术栈:流量整形算法,服务熔断与降级
本文源码:GitHub·点这里 || GitEE·点这里 一.流量控制 1.基本概念 流量控制的核心作用是限制流出某一网络的某一连接的流量与突发,使这类报文以比较均匀的速度流动发送,达到保护系统相对稳 ...
- SpringCloud微服务技术实践与总结(基础篇)
1.认识微服务 1.1.单体架构 单体架构:将业务的所有功能集中在一个项目中开发,打成一个包部署. 单体架构的优缺点如下: 优点: 架构简单.部署成本低 缺点: 耦合度高(维护困难.升级困难) 1.2 ...
- Day2:SpringCloud入门学习——传智播客学习笔记【微服务电商】
SpringCloud 0.学习目标 ·了解系统架构的演变 ·了解RPC与Http的区别 ·掌握HttpClient的简单使用 ·知道什么是SpringCloud ·独立搭建Eureka注册中心 ·独 ...
最新文章
- jsch设置代理_Java使用JSch组件实现SSH协议代理服务
- java线程池游戏代码,Java游戏起步:(一)线程与线程池-JSP教程,Java技巧及代码...
- 软件工程期末考试复习(四)
- 阿里开发者们的第15个感悟:做一款优秀大数据引擎,要找准重点解决的业务场景
- python自动化操作应用程序错误_web自动化中踩过的低级错误坑(python+selenium)
- 异常记录——使用Mybatis报BindingException
- 区块链会计案例_区块链在会计领域的应用分析与研究
- linux ios文件是否存在,Linux如何读取iOS镜像文件
- 医院子母钟时钟系统方案
- ArchLinux安装错误”Errors occured, no packages were upgraded. ⇒ ERROR: Failed to install packages to new“
- 如何将excel里的数据导入到mysql中
- 将windows 8安装到U盘随身带!
- 陌陌发布新版 增加阅后即焚和短视频功能
- Visual Studio 2022调节字体大小
- html5立体照片墙效果,HTML5特效可以 14种jQuery超酷3D网格照片墙动画特效源码
- 高级计算机职称论文自述,教师评职称自述
- 我的一点自学心得[摘]
- SAPNoteSAR格式解压_SAP刘梦_新浪博客
- 如何成为云计算解决方案架构师
- 中小企业办公无线网络覆盖解决方案