栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

学习笔记:SpringCloud 微服务技术栈

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

学习笔记:SpringCloud 微服务技术栈


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

文章目录
  • 前言
  • 0.微服务技术栈导学
  • # SpringCloudDay06
  • 1.项目简述
  • 2.酒店搜索和分页
    • 2.1.需求分析
    • 2.2.定义实体类
      • 2.2.1.请求参数
      • 2.2.2.返回值
    • 2.3.定义 controller
    • 2.4.实现搜索业务
      • 2.4.1.在接口中定义方法
      • 2.4.2.注入 es 客户端组件
      • 2.4.3.实现业务逻辑
  • 3.酒店结果过滤
    • 3.1.需求分析
    • 3.2.修改实体类
    • 3.3.修改搜索业务
  • 4.周边的酒店
    • 4.1.需求分析
    • 4.2.修改实体类
    • 4.3.距离排序 API
    • 4.4.添加距离排序
    • 4.5.排序距离显示
      • 4.5.1.解析
      • 4.5.2.实体类添加距离字段
      • 4.5.3.添加排序业务
  • 5.酒店竞价排名
    • 5.1.需求分析
    • 5.2.修改 HotelDoc 实体
    • 5.3.添加广告标记
    • 5.4.添加算法函数查询
  • # SpringCloudDay07
  • 6.多条件的数据聚合
    • 6.1.业务需求
    • 6.2.业务实现
  • 7.实现酒店搜索框自动补全
    • 7.1.修改酒店映射结构
    • 7.2.修改 HotelDoc 实体
    • 7.3.重新导入
    • 7.4.自动补全查询的 JavaAPI
    • 7.5.实现搜索框自动补全
  • 8.实现数据同步
    • 8.1.思路
    • 8.2.导入 demo
    • 8.3.声明交换机、队列
      • 8.3.1.配置文件
      • 8.3.2.声明队列交换机的名称
      • 8.3.3.声明队列交换机
    • 8.4.发送 MQ 消息
    • 8.5.接收 MQ 消息
      • 8.5.1.新增和删除的业务
      • 8.5.2.业务实现
      • 8.5.3.编写监听器
    • 8.6.测试同步功能


前言
  • 学习视频链接
    • SpringCloud + RabbitMQ + Docker + Redis + 搜索 + 分布式,史上最全面的 SpringCloud 微服务技术栈课程 | 黑马程序员 Java 微服务
  • 学习资料链接
    • https://pan.baidu.com/s/169SFtYEvel44hRJhmFTRTQ(提取码:1234)

  • 写这篇博客旨在制作笔记,巩固知识。同时方便个人在线阅览,回顾知识。
  • 博客的内容主要来自视频内容和资料中提供的学习笔记。

本博客主要是对【学习笔记: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:当前页的数据

因此,我们实现业务的流程如下

  • 步骤一:定义实体类,接收前端请求:请求参数的 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:当前页的数据

因此,我们在 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 hotels;

    public PageResult() {
    }

    public PageResult(Long total, List hotels) {
        this.total = total;
        this.hotels = hotels;
    }
}

2.3.定义 controller

定义一个 HotelController,声明查询接口,满足下列要求:

  • 请求方式:Post
  • 请求路径:/hotel/list
  • 请求参数:对象,类型为 RequestParam
  • 返回值:PageResult,包含两个属性
    • Long total:总条数
    • List hotels:酒店数据

因此,我们在 cn.itcast.hotel.web 中定义 HotelController

src/main/java/cn/itcast/hotel/web/HotelController.java

@RestController
@RequestMapping("/hotel")
public class HotelController {

    @Autowired
    private 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

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.准备 Request
        SearchRequest request = new SearchRequest("hotel");
        // 2.准备 DSL
        // 2.1.query
        String 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 hotels = new ArrayList<>();
    for (SearchHit hit : hits) {
        // 获取文档 source
        String 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.构建 BooleanQuery
    BoolQueryBuilder 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.放入 source
    request.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.准备Request
        SearchRequest request = new SearchRequest("hotel");
        // 2.准备DSL
        // 2.1.query
        buildBasicQuery(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,固定加权值
  • 加权方式:可以用默认的相乘,大大提高算分

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

  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.构建BooleanQuery
    BoolQueryBuilder 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>

这里调用了 IHotelService 中的 getFilters 方法,但尚未实现。

src/main/java/cn/itcast/hotel/web/HotelController.java

@PostMapping("filters")
public Map> getFilters(@RequestBody RequestParams params){
    return hotelService.getFilters(params);
}

在cn.itcast.hotel.service.IHotelService 中定义新方法

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

Map> filters(RequestParams params);

在cn.itcast.hotel.service.impl.HotelService 中实现该方法

src/main/java/cn/itcast/hotel/service/impl/HotelService.java

@Override
public Map> filters(RequestParams params) {
    try {
        // 1.准备Request
        SearchRequest request = new SearchRequest("hotel");
        // 2.准备DSL
        // 2.1.query
        buildBasicQuery(params, request);
        // 2.2.设置size
        request.source().size(0);
        // 2.3.聚合
        buildAggregation(request);
        // 3.发出请求
        SearchResponse response = client.search(request, RequestOptions.DEFAULT);
        // 4.解析结果
        Map> result = new HashMap<>();
        Aggregations aggregations = response.getAggregations();
        // 4.1.根据品牌名称,获取品牌结果
        List brandList = getAggByName(aggregations, "brandAgg");
        result.put("品牌", brandList);
        // 4.2.根据品牌名称,获取品牌结果
        List cityList = getAggByName(aggregations, "cityAgg");
        result.put("城市", cityList);
        // 4.3.根据品牌名称,获取品牌结果
        List 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 getAggByName(Aggregations aggregations, String aggName) {
    // 4.1.根据聚合名称获取聚合结果
    Terms brandTerms = aggregations.get(aggName);
    // 4.2.获取buckets
    List buckets = brandTerms.getBuckets();
    // 4.3.遍历
    List brandList = new ArrayList<>();
    for (Terms.Bucket bucket : buckets) {
        // 4.4.获取key
        String key = bucket.getKeyAsString();
        brandList.add(key);
    }
    return brandList;
}

7.实现酒店搜索框自动补全

此时我们的 hotel 索引库还没有设置拼音分词器,需要修改索引库中的配置。

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

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


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

  1. 修改 hotel 索引库结构,设置自定义拼音分词器
  2. 修改索引库的 name、all 字段,使用自定义分词器
  3. 索引库添加一个新字段 suggestion,类型为 completion 类型,使用自定义的分词器
  4. 给 HotelDoc 类添加 suggestion 字段,内容包含 brand、business
  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,然后将 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 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();
        // 组装 suggestion
        if(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

@Test
void testSuggest() throws IOException {
    //1.准备 Request
    SearchRequest request = new SearchRequest("hotel");
    //2.准备 DSL
    request.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);
}
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


  1. 在 cn.itcast.hotel.web 包下的 HotelController 中添加新接口,接收新的请求

src/main/java/cn/itcast/hotel/web/HotelController.java

@GetMapping("suggestion")
public List 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 getSuggestions(String prefix);

  1. 在 cn.itcast.hotel.service.impl.HotelService 中实现该方法

src/main/java/cn/itcast/hotel/service/impl/HotelService.java

@Override
public List getSuggestions(String prefix) {
    try {
        // 1.准备 Request
        SearchRequest request = new SearchRequest("hotel");
        // 2.准备 DSL
        request.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.获取 options
        List options = suggestions.getOptions();
        // 4.3.遍历
        List 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



    org.springframework.boot
    spring-boot-starter-amqp


配置 RabbitMQ 地址

在 hotel-admin、hotel-demo 项目中的 src/main/resources/application.yaml 下配置 RabbitMQ 的地址

spring:
	rabbitmq:
	  host: 192.168.150.101
	  port: 5672
	  username: itcast
	  password: 123456
	  virtual-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";

public final static String HOTEL_INSERT_KEY = "hotel.insert";

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 {
    @Bean
    public TopicExchange topicExchange(){
        return new TopicExchange(MqConstants.HOTEL_EXCHANGE, true, false);
    }

    @Bean
    public Queue insertQueue(){
        return new Queue(MqConstants.HOTEL_INSERT_QUEUE, true);
    }

    @Bean
    public Queue deleteQueue(){
        return new Queue(MqConstants.HOTEL_DELETE_QUEUE, true);
    }

    @Bean
    public Binding insertQueueBinding(){
        return BindingBuilder.bind(insertQueue()).to(topicExchange()).with(MqConstants.HOTEL_INSERT_KEY);
    }

    @Bean
    public 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 {

    @Autowired
    private IHotelService hotelService;

    @Autowired
    private 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 result = hotelService.page(new Page<>(page, size));

        return new PageResult(result.getTotal(), result.getRecords());
    }

    @PostMapping
    public 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.准备 Request
        DeleteRequest 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 {

    @Autowired
    private IHotelService hotelService;

    
    @RabbitListener(queues = MqConstants.HOTEL_INSERT_QUEUE)
    public void listenHotelInsertOrUpdate(Long id){
        hotelService.insertById(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/ 页面,发现数据修改的数据同步成功


删除数据、修改的数据同步同理,此处不作演示。 (主要是懒得截图)


暑期编程PK赛 得CSDN机械键盘等精美礼品!
转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/1016191.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号