• 若文章内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系博主删除。

前言


  • 学习视频链接

    • 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,接收页面的请求,调用 IHotelServicesearch 方法
  • 步骤三:编写业务实现,定义 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.修改搜索业务


HotelServicesearch 方法中,只有一个地方需要修改: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.implHotelServicesearch 方法中,添加一个排序功能

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 scorequery score 如何运算

这里的需求是:让指定酒店排名靠前。

因此我们需要给这些酒店添加一个标记,这样在过滤条件中就可以根据这个标记来判断,是否要提高算分

比如,我们给酒店添加一个字段:isADBoolean 类型:

  • true:是广告
  • false:不是广告

这样 function_score 包含 3 个要素就很好确定了:

  • 过滤条件:判断 isAD 是否为 true
  • 算分函数:我们可以用最简单暴力的 weight,固定加权值
  • 加权方式:可以用默认的相乘,大大提高算分

因此,业务的实现步骤包括

  1. HotelDoc 类添加 isAD 字段,Boolean 类型
  2. 挑选几个你喜欢的酒店,给它的文档数据添加 isAD 字段,值为 true
  3. 修改 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 索引库还没有设置拼音分词器,需要修改索引库中的配置。

但是我们知道索引库是无法修改的,只能删除然后重新创建。

另外,我们需要添加一个字段,用来做自动补全,将 brandsuggestioncity 等都放进去,作为自动补全的提示。


因此,总结一下,我们需要做的事情包括:

  1. 修改 hotel 索引库结构,设置自定义拼音分词器
  2. 修改索引库的 nameall 字段,使用自定义分词器
  3. 索引库添加一个新字段 suggestion,类型为 completion 类型,使用自定义的分词器
  4. HotelDoc 类添加 suggestion 字段,内容包含 brandbusiness
  5. 重新导入数据到 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>,然后将 brandcitybusiness 等信息放到里面。

代码如下:

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>


  1. 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);
}

  1. cn.itcast.hotel.service 包下的 IhotelService 中添加方法

src/main/java/cn/itcast/hotel/service/IHotelService.java

List<String> getSuggestions(String prefix);

  1. 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 也必须跟着改变。

这个就是 elasticsearchmysql 之间的数据同步


在微服务中,负责酒店管理(操作 MySQL)的业务与负责酒店搜索(操作 ElasticSearch)的业务可能在两台不同的微服务上。

那么此时的数据同步应该如何实现呢?

常见的数据同步方案有三种:同步调用、异步通知、监听 binlog


案例基于 MQ 来实现 MySQLElsaticSearch 数据同步


8.1.思路


利用课前资料提供的 hotel-admin 项目作为酒店管理的微服务。

当酒店数据发生增、删、改时,要求对 elasticsearch 中数据也要完成相同操作。


步骤

  • 导入 课前资料 提供的 hotel-admin 项目,启动并测试酒店数据的 CRUD
  • 声明 exchangequeueRoutingKey
  • 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-adminhotel-demo 中引入 RabbitMQ 的依赖

pom.xml

<!-- AMQP -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

配置 RabbitMQ 地址

hotel-adminhotel-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-adminhotel-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 消息要做的事情包括

  • 新增消息:根据传递的 hotelid 查询 hotel 信息,然后新增一条数据到索引库
  • 删除消息:根据传递的 hotelid 删除索引库中的一条数据

8.5.1.新增和删除的业务


首先在 hotel-democn.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 微服务技术栈_实用篇②_黑马旅游案例相关推荐

  1. 学习笔记:SpringCloud 微服务技术栈_实用篇①_基础知识

    若文章内容或图片失效,请留言反馈.部分素材来自网络,若不小心影响到您的利益,请联系博主删除. 前言 学习视频链接 SpringCloud + RabbitMQ + Docker + Redis + 搜 ...

  2. 学习笔记:SpringCloud 微服务技术栈_高级篇⑤_可靠消息服务

    若文章内容或图片失效,请留言反馈.部分素材来自网络,若不小心影响到您的利益,请联系博主删除. 前言 学习视频链接 SpringCloud + RabbitMQ + Docker + Redis + 搜 ...

  3. 详解springcloud微服务技术栈(一)

    1.微服务技术栈 2.认识微服务 2.1.微服务架构演变 2.2.springcloud(Spring Cloud) SpringCloud和SpringBoot的版本兼容关系如下: 2.3.微服务拆 ...

  4. SpringCloud学习笔记(二):微服务概述、微服务和微服务架构、微服务优缺点、微服务技术栈有哪些、SpringCloud是什么...

    从技术维度理解: 微服务化的核心就是将传统的一站式应用,根据业务拆分成一个一个的服务,彻底 地去耦合,每一个微服务提供单个业务功能的服务,一个服务做一件事, 从技术角度看就是一种小而独立的处理过程,类 ...

  5. SpringCloud一、前提概述、相关微服务和微服务架构理论知识、微服务技术栈有哪些、

    ①前提概述.微服务架构springcloud的相关学习. 前提知识+相关说明 1.目前,我们学习到最后的微服务架构SpringCloud,基本上需要熟悉以前的学习内容和知识:springmvc.spr ...

  6. 微服务技术栈:API网关中心,落地实现方案

    本文源码:GitHub·点这里 || GitEE·点这里 一.服务网关简介 1.外观模式 客户端与各个业务子系统的通信必须通过一个统一的外观对象进行,外观模式提供一个高层次的接口,使得子系统更易于使用 ...

  7. 微服务技术栈:流量整形算法,服务熔断与降级

    本文源码:GitHub·点这里 || GitEE·点这里 一.流量控制 1.基本概念 流量控制的核心作用是限制流出某一网络的某一连接的流量与突发,使这类报文以比较均匀的速度流动发送,达到保护系统相对稳 ...

  8. SpringCloud微服务技术实践与总结(基础篇)

    1.认识微服务 1.1.单体架构 单体架构:将业务的所有功能集中在一个项目中开发,打成一个包部署. 单体架构的优缺点如下: 优点: 架构简单.部署成本低 缺点: 耦合度高(维护困难.升级困难) 1.2 ...

  9. Day2:SpringCloud入门学习——传智播客学习笔记【微服务电商】

    SpringCloud 0.学习目标 ·了解系统架构的演变 ·了解RPC与Http的区别 ·掌握HttpClient的简单使用 ·知道什么是SpringCloud ·独立搭建Eureka注册中心 ·独 ...

最新文章

  1. jsch设置代理_Java使用JSch组件实现SSH协议代理服务
  2. java线程池游戏代码,Java游戏起步:(一)线程与线程池-JSP教程,Java技巧及代码...
  3. 软件工程期末考试复习(四)
  4. 阿里开发者们的第15个感悟:做一款优秀大数据引擎,要找准重点解决的业务场景
  5. python自动化操作应用程序错误_web自动化中踩过的低级错误坑(python+selenium)
  6. 异常记录——使用Mybatis报BindingException
  7. 区块链会计案例_区块链在会计领域的应用分析与研究
  8. linux ios文件是否存在,Linux如何读取iOS镜像文件
  9. 医院子母钟时钟系统方案
  10. ArchLinux安装错误”Errors occured, no packages were upgraded. ⇒ ERROR: Failed to install packages to new“
  11. 如何将excel里的数据导入到mysql中
  12. 将windows 8安装到U盘随身带!
  13. 陌陌发布新版 增加阅后即焚和短视频功能
  14. Visual Studio 2022调节字体大小
  15. html5立体照片墙效果,HTML5特效可以 14种jQuery超酷3D网格照片墙动画特效源码
  16. 高级计算机职称论文自述,教师评职称自述
  17. 我的一点自学心得[摘]
  18. SAPNoteSAR格式解压_SAP刘梦_新浪博客
  19. 如何成为云计算解决方案架构师
  20. 中小企业办公无线网络覆盖解决方案

热门文章

  1. Clark变换与Park
  2. JavaWEB-04 项目案例(1)
  3. jQuery DOM元素的遍历
  4. [收集]仿163邮箱的JS编辑器
  5. yy自动语音接待机器人_YY语音最新应用教程 场控机器人
  6. 【算法】分治策略:芯片测试
  7. 前端开发:JS中关于正则表达式的使用汇总
  8. lerna 生成自定义日志changelog
  9. 计算机及统计学,统计学中及计算机视觉中的各种 距离 汇总。。。
  10. vue里面变量名前面加三个点代表什么意思?