Springcloud篇9-Elasticsearch-3(数据聚合、自动补全、数据同步、集群)

一、数据聚合

1.1 聚合的分类

聚合可以实现对文档数据的统计、分析、运算(MySQL中的聚合函数有avg,max,min,sum,一般还要结合group by 分组使用)。常见的有三类:

(1)桶(Bucket)聚合 :用来对文档做分组。

TermAggregation:按文档字段值(该字段不能分词,不能是text)分组

Date Histogram:按照日期阶梯分组,例如一周为一组,或者一月为一组

(2)度量(Metric)聚合 :用以计算一些值,比如最大值、最小值、平均值

Avg:求平均值

Max:求最大值

Min:求最小值

Stats:同时求max、min、avg、sum等。

(3)管道(pipeline)聚合:其它聚合的结果为基础做聚合

1.2 DSL实现

还是博文springcloud篇7中添加的hotel索引:

bash 复制代码
# 酒店的mapping
PUT /hotel
{
  "mappings":{
    "properties":{
      "id":{
        "type":"keyword"
      },
      "name":{
        "type":"text",
        "analyzer":"ik_max_word",
        "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":"ik_max_word"
      }
    }
  }
}

1.2.1 实现Bucket聚合

例子:统计所有数据中的酒店品牌有几种,此时可以根据酒店品牌的名称做聚合。

bash 复制代码
# 聚合功能
GET /hotel/_search
{
  "size":0,
  "aggs":{
    "brandAgg":{
      "terms":{
        "field":"brand",
        "size":10
      }
    }
  }
}
补充1.Bucket聚合-聚合结果排序

默认情况下,Bucket聚合会统计Bucket内的文档数量,记为_count,并且按照_count降序排序。

可以修改结果排序方式:

bash 复制代码
# 聚合功能,自定义排序规则
GET /hotel/_search
{
  "size":0,
  "aggs":{
    "brandAgg":{
      "terms":{
        "field":"brand",
        "size":20,
        "order":{
          "_count":"asc"
        }
      }
    }
  }
}
补充2.Bucket聚合-限定聚合范围

默认情况下,Bucket聚合是对索引库的所有文档做聚合,可以限定要聚合的文档范围,只要添加query条件即可。

bash 复制代码
# 聚合功能,限定聚合范围
GET /hotel/_search
{
  "query":{
    "range":{
      "price":{
        "lte":200
      }
    }
  },
  "size":0,
  "aggs":{
    "brandAgg":{
      "terms":{
        "field":"brand",
        "size":20,
        "order":{
          "_count":"asc"
        }
      }
    }
  }
}

1.2.2 实现Metric聚合

需与bucket聚合结合使用。

例子,获取每个品牌的用户评分的min、max、avg等值。

从上图中可以看出,聚合可以嵌套,brandAgg聚合内部嵌套了一个score_stats聚合。

bash 复制代码
# 嵌套聚合metric
GET /hotel/_search
{
  "size":0,
  "aggs":{
    "brandAgg":{
      "terms":{
        "field":"brand",
        "size":20
      },
      "aggs":{
        "scoreAgg":{
          "stats":{
            "field":"score"
          }
        }
      }
    }
  }
}

再根据聚合的计算结果排序:

bash 复制代码
# 嵌套聚合metric
GET /hotel/_search
{
  "size":0,
  "aggs":{
    "brandAgg":{
      "terms":{
        "field":"brand",
        "size":20,
        "order":{
          "scoreAgg.avg":"desc"
        }
      },
      "aggs":{
        "scoreAgg":{
          "stats":{
            "field":"score"
          }
        }
      }
    }
  }
}

1.3 RestAPI实现聚合

获取查询结果:

聚合结果解析:

java 复制代码
    @Test
    void testAggregation() throws IOException {
        //1.创建Request对象
        SearchRequest request=new SearchRequest("hotel");
        //2.准备DSL语句
        //2.1 设置size
        request.source().size(0);
        //2.2 聚合
        request.source().aggregation(AggregationBuilders.terms("brandAgg").field("brand").size(10));
        //3.发送请求
        SearchResponse response = client.search(request, RequestOptions.DEFAULT);
        System.out.println(response);
        //4.解析结果
        Aggregations aggregations = response.getAggregations();
        //4.1根据聚合名称获取聚合结果
        Terms brandTerms = aggregations.get("brandAgg");
        //4.2 获取buckets
        List<? extends Terms.Bucket> buckets = brandTerms.getBuckets();
        //4.3 遍历
        for(Terms.Bucket bucket : buckets){
           String key = bucket.getKeyAsString();
            System.out.println(key);
        }
    }

1.4 多条件聚合


如上图所示,前端页面的筛选条件(城市、星级、品牌和价格)的选项并不是前端写死的,而是要通过后台搜索聚合结果得到。

java 复制代码
public interface IHotelService extends IService<Hotel> {
    PageResult search(RequestParams params);
    Map<String, List<String>> filters();
}
java 复制代码
@Override
    public Map<String, List<String>> filters() {
        try {
            //1.创建Request对象
            SearchRequest request=new SearchRequest("hotel");
            //2.准备DSL语句
            //2.1 设置size
            request.source().size(0);
            //2.2 聚合
            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));

            //3.发送请求
            SearchResponse response = client.search(request, RequestOptions.DEFAULT);
            
            //4.解析结果
            Map<String, List<String>> result = new HashMap<>();
            Aggregations aggregations = response.getAggregations();
            List<String> brandList = getAggByName(aggregations,"brandAgg");
            result.put("品牌", brandList);
            List<String> cityList = getAggByName(aggregations,"cityAgg");
            result.put("城市", cityList);
            List<String> starList = getAggByName(aggregations,"starAgg");
            result.put("星级", starList);
            return result;
        } catch (IOException e) {
            throw new RuntimeException(e);
        } 
    }

    private static List<String> getAggByName(Aggregations aggregations,String aggName) {
        //4.1根据聚合名称获取聚合结果
        Terms brandTerms = aggregations.get(aggName);
        //4.2 获取buckets
        List<? extends Terms.Bucket> buckets = brandTerms.getBuckets();
        //4.3 遍历
        List<String> brandList = new ArrayList<>();
        for(Terms.Bucket bucket : buckets){
            String key = bucket.getKeyAsString();
            brandList.add(key);
        }
        return brandList;
    }
java 复制代码
@SpringBootTest
class HotelDemoApplicationTests {

    @Autowired
    private IHotelService hotelService;

    @Test
    void contextLoads() {
        Map<String, List<String>> filter=hotelService.filters();
        System.out.println(filter);
    }


}

1.5 带过滤条件的聚合


注意:因为筛选项的聚合结果与用户输入有关,所以接口的入参与用户搜索的接口的入参一致。

代码修改如下:

java 复制代码
    @PostMapping("/filters")
    public Map<String, List<String>> getFilters(@RequestBody RequestParams params ){
        return hotelService.filters(params);

    }
java 复制代码
public interface IHotelService extends IService<Hotel> {
    PageResult search(RequestParams params);
    Map<String, List<String>> filters(RequestParams params);
}
java 复制代码
@Override
    public Map<String, List<String>> filters(RequestParams params) {
        try {
            //1.创建Request对象
            SearchRequest request=new SearchRequest("hotel");
            //2.准备DSL语句
            //2.0 query
            buildBasicQuery(params, request);
            //2.1 设置size
            request.source().size(0);
            //2.2 聚合
            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));

            //3.发送请求
            SearchResponse response = client.search(request, RequestOptions.DEFAULT);
            //4.解析结果
            Map<String, List<String>> result = new HashMap<>();
            Aggregations aggregations = response.getAggregations();
            List<String> brandList = getAggByName(aggregations,"brandAgg");
            result.put("品牌", brandList);
            List<String> cityList = getAggByName(aggregations,"cityAgg");
            result.put("城市", cityList);
            List<String> starList = getAggByName(aggregations,"starAgg");
            result.put("星级", starList);
            return result;
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    private static List<String> getAggByName(Aggregations aggregations,String aggName) {
        //4.1根据聚合名称获取聚合结果
        Terms brandTerms = aggregations.get(aggName);
        //4.2 获取buckets
        List<? extends Terms.Bucket> buckets = brandTerms.getBuckets();
        //4.3 遍历
        List<String> brandList = new ArrayList<>();
        for(Terms.Bucket bucket : buckets){
            String key = bucket.getKeyAsString();
            brandList.add(key);
        }
        return brandList;
    }

    private static void buildBasicQuery(RequestParams params, SearchRequest request) {
        //1.准备DSL语句
        //1.1 query
        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 && !"".equals(params.getCity())){
            boolQuery.filter(QueryBuilders.termQuery("city", params.getCity()));
        }
        //品牌条件
        if(params.getBrand() !=null && !"".equals(params.getBrand())){
            boolQuery.filter(QueryBuilders.termQuery("brand", params.getBrand()));
        }
        //星级条件
        if(params.getStarName() !=null && !"".equals(params.getStarName())){
            boolQuery.filter(QueryBuilders.termQuery("brand", params.getStarName()));
        }
        //价格条件
        if(params.getMinPrice() !=null && !"".equals(params.getMinPrice()) &&
                params.getMaxPrice() !=null && !"".equals(params.getMaxPrice())){
            boolQuery.filter(QueryBuilders.rangeQuery("price").gte(params.getMinPrice()).lte(params.getMaxPrice()));
        }

        //2.算分控制
        FunctionScoreQueryBuilder functionScoreQuery=
                QueryBuilders.functionScoreQuery(
                        boolQuery,//原始查询,相关性算分的查询
                        new FunctionScoreQueryBuilder.FilterFunctionBuilder[]{//function score数组
                              new FunctionScoreQueryBuilder.FilterFunctionBuilder(//其中的一个function score元素
                                      QueryBuilders.termQuery("isAD",true),
                                      ScoreFunctionBuilders.weightFactorFunction(10)
                              )
                        });
        request.source().query(functionScoreQuery);
    }



二、自动补全

2.1 拼音分词器

要实现根据字母做补全,就必须对文档按照拼音分词 。在GitHub上有elasticsearch的拼音分词器插件,地址:https://github.com/medcl/elasticsearch-analysis-pinyin

找到学习资料中已解压的拼音分词器,将解压的ik文件夹里的东西上传到查询到的插件目录/var/lib/docker/volumes/es-plugins/_data。直接上传可能打不开这个目录,可以先上传到一个目录再用mv指令。

bash 复制代码
mv xx /var/lib/docker/volumes/es-plugins/_data
docker restart es

2.2 自动补全查询

注意:2.1中的拼音分词器的结果只保留了每个字的拼音及句子首字母拼音组合的结果,但是没有保留中文分词,所以还需要对拼音分词器进行改进。

可以在创建索引库时,通过setting来配置自定义的analyzer(分词器):

这里没有定义character filter分词器(分词器的三部分不一定都要有)。

回到之前的拼音分词器结果,可以看到只有单个字的拼音,而没有单个词的拼音,可以在定义分词器的时候修改:

bash 复制代码
// 自定义拼音分词器
PUT /test
{
  "settings": {
    "analysis": {
      "analyzer": { 
        "my_analyzer": { 
          "tokenizer": "ik_max_word",
          "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
        }
      }
    }
  }
}


bash 复制代码
# 自定义拼音分词器
PUT /test
{
  "settings": {
    "analysis": {
      "analyzer": { 
        "my_analyzer": { 
          "tokenizer": "ik_max_word",
          "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": {
      "name":{
        "type":"text",
        "analyzer":"my_analyzer"
      }
    }
  }
}
bash 复制代码
POST /test/_analyze
{
  "text":["如家酒店还不错"],
  "analyzer":"my_analyzer"
}

在举一个查询的例子:

bash 复制代码
POST /test/_doc/1
{
  "id": 1,
  "name": "狮子"
}
POST /test/_doc/2
{
  "id": 2,
  "name": "虱子"
}
bash 复制代码
GET /test/_search
{
  "query": {
    "match": {
      "name": "shizi"
    }
  }
}
bash 复制代码
GET /test/_search
{
  "query": {
    "match": {
      "name": "狮子喜欢什么"
    }
  }
}

注意:
拼音分词器适合在创建倒排索引的时候使用,但不能在搜索的时候使用。

因此,字段在创建倒排索引时应该用my_analyzer分词器;字段在搜索时应该使用ik_smart分词器。


bash 复制代码
# 自定义拼音分词器
PUT /test
{
  "settings": {
    "analysis": {
      "analyzer": { 
        "my_analyzer": { 
          "tokenizer": "ik_max_word",
          "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": {
      "name":{
        "type":"text",
        "analyzer":"my_analyzer",
        "search_analyzer":"ik_smart"
      }
    }
  }
}

2.3 completion suggester查询

elasticsearch提供了Completion Suggester查询来实现自动补全功能。这个查询会匹配以用户输入内容开头的词条并返回。

为了提高补全查询的效率,对于文档中字段的类型有一些约束:

(1)参与补全查询的字段必须是completion类型。

(2)字段的内容一般是用来补全的多个词条形成的数组。

查询语法如下:

一个例子如下:

bash 复制代码
// 自动补全的索引库
PUT test2
{
  "mappings": {
    "properties": {
      "title":{
        "type": "completion"
      }
    }
  }
}
// 示例数据
POST test2/_doc
{
  "title": ["Sony", "WH-1000XM3"]
}
POST test2/_doc
{
  "title": ["SK-II", "PITERA"]
}
POST test2/_doc
{
  "title": ["Nintendo", "switch"]
}
bash 复制代码
// 自动补全查询
POST /test2/_search
{
  "suggest": {
    "title_suggest": {
      "text": "s", // 关键字
      "completion": {
        "field": "title", // 补全字段
        "skip_duplicates": true, // 跳过重复的
        "size": 10 // 获取前10条结果
      }
    }
  }
}

2.4 实战-酒店数据自动补全

2.4.0 数据库改造

bash 复制代码
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"
      }
    }
  }
}

注意,hotel索引添加了一个字段suggestion。

java 复制代码
@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();
        this.suggestion = Arrays.asList(this.brand, this.business);
    }
}


还有一个细节需要处理:

java 复制代码
@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();
        if(this.business.contains("/")){
            String[] split = this.business.split("/");
            this.suggestion = new ArrayList<>();
            this.suggestion.add(this.brand);
            Collections.addAll(this.suggestion, split);

        }else{
            this.suggestion = Arrays.asList(this.brand, this.business);
        }
    }
}



java 复制代码
GET /hotel/_search
{
  "suggest":{
    "suggestions":{
      "text":"h",
      "completion":{
        "field":"suggestion",
        "skip_duplicates":true,
        "size":10
      }
    }
  }
}

2.4.1 理论:RestAPI实现自动补全



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);
        System.out.println(response);
        //4.解析结果
        Suggest suggest = response.getSuggest();
        //4.1 根据补全查询名称,获取补全结果
        CompletionSuggestion suggestions = suggest.getSuggestion("suggestions");
        //4.2获取options
        List<CompletionSuggestion.Entry.Option> options = suggestions.getOptions();
        //4.3遍历
        for(CompletionSuggestion.Entry.Option option : options){
            String text=option.getText().toString();
            System.out.println(text);
        }
    }

2.4.2 前端页面实现



java 复制代码
    @GetMapping("/suggestion")
    public List<String> getSuggestions(@RequestParam("key") String prefix ){
        return hotelService.getSuggestion(prefix);

    }
java 复制代码
List<String> getSuggestion(String prefix);
java 复制代码
@Override
    public List<String> getSuggestion(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<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);
        }
    }

重启服务测试:

三、数据同步

3.1 三种方案

数据同步与业务无关,但是非常重要,面试也经常问。

elasticsearch中的酒店数据来自于mysql数据库,因此mysql数据发生变化时,elasticsearch也必须跟着改变,这个就是elasticsearch与mysql之间的数据同步。

在微服务中,负责酒店管理(操作mysql)的业务与负责酒店搜索(操作elasticsearch)的业务可能在两个不同的微服务上,数据该如何实现同步。
(1)方案一:同步调用

写mysql、调用根据索引库接口和更新elasticsearch先后执行,耗时较长。
(二)方案二:异步通知

(三)监听binlog

mysql的binlog默认是关闭的。开启后,binlog可以记录mysql的增删改查操作,可以利用类似于canal这样的中间件监听binlog,一旦发生binlog变化则通知酒店数据变更情况。但是开启binlog对于mysql性能影响大,增加数据库负担,且实现负责都高。

3.2 MQ方案实现

3.2.1 导入项目



数据库即为博文《Springcloud篇7-Elasticsearch-1》与《Springcloud篇8-Elasticsearch-2》的酒店数据。

3.2.2 声明exchange,queue,RoutingKey

虽然对mysql中的数据有增删改操作,但对于elasticsearch都是往索引库里插入数据(id不存在是新增,id存在是改,可以把增和改合成一个业务),删除也是一个业务,即一共两个业务,两个消息队列。

注意:是在《Springcloud篇7-Elasticsearch-1》与《Springcloud篇8-Elasticsearch-2》的hotel-demo项目中修改。

xml 复制代码
        <!-- amqp -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>
yaml 复制代码
  rabbitmq:
    host: 10.xx.xx.12
    port: 5672
    username: itcast
    password: 123321
    virtual-host: /
java 复制代码
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";
}
java 复制代码
@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);
    }
}

3.2.3 hotel-admin服务的消息发送





java 复制代码
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<Hotel> result = hotelService.page(new Page<>(page, size));

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

    @PostMapping
    public void saveHotel(@RequestBody Hotel hotel){

        hotelService.save(hotel);
        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);

    }
}

3.2.4 hotel-demo的消息监听

java 复制代码
@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);
    }

}
java 复制代码
    void insertById(Long id);

    void deleteById(Long id);
java 复制代码
@Override
    public void insertById(Long id) {
        try {
            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);
        }
    }

    @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);
        }
    }

3.2.5 启动服务测试

启动服务前确保rabbitmq服务启动:







相关推荐
真上帝的左手5 小时前
13. 搜索引擎-ES-ES集群
elasticsearch·搜索引擎·jenkins
济南java开发,求内推5 小时前
ES升级至:8.15.3
elasticsearch
真上帝的左手5 小时前
13. 搜索引擎-ES-自动补全
elasticsearch·搜索引擎
真上帝的左手5 小时前
13. 搜索引擎-ES-DSL(Domain Specific Language)
elasticsearch·搜索引擎·数学建模
serendipity_hky14 小时前
【SpringCloud | 第4篇】Gateway网关统一入口
spring·spring cloud·微服务·gateway
AI逐月19 小时前
Git 彻底清除历史记录
大数据·git·elasticsearch
中国胖子风清扬1 天前
Spring AI Alibaba + Ollama 实战:基于本地 Qwen3 的 Spring Boot 大模型应用
java·人工智能·spring boot·后端·spring·spring cloud·ai
小安同学iter1 天前
天机学堂-优惠券功能-day09(七)
java·spring cloud·微服务·jenkins·优惠券·天机学堂
管理大亨1 天前
ELK + Redis Docker 企业级部署落地方案
大数据·运维·elk·elasticsearch·docker·jenkins