云游大计:Spring Cloud 微服务之旅

176 阅读18分钟

介绍微服务

微服务是一种经过改良过好架构设计的分布式架构方案。
特征有以下几点:

  • 单一职责:微服务拆分粒度更小,每一个服务都对应唯一的业务能力,做到单一职责,避免重复开发。
  • 面向服务:微服务对外暴露业务接口。
  • 自治:团队独立、技术独立、数据独立、部署独立。
  • 隔离性强:服务调用做到好隔离、容错、降级、避免出现级联问题。

缺点:架构非常复杂、运维、监控、部署难度提高。

SpringCloud 组件

图片.png

Ribbon

负载均衡

图片.png

图片.png

Ribbon 通过 IRule 决定使用哪种负载均衡策略:

图片.png

图片.png

Ribbon 负载均衡配置方式:

作用域:全局

@SpringBootApplication
public class OrderApplication {

    public static void main(String[] args) {
        SpringApplication.run(OrderApplication.class, args);
    }

    /**
     * 
     * @return
     */
    @Bean
    public IRule randomRule() {
        return new RandomRule();
    }
}    

作用域:单个服务

userservice:
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 负载均衡规则

饥饿加载

Ribbon 默认是采用懒加载,即第一次访问时才会去创建LoadBalanceClient,请求时间会很长。 而饥饿加载则会在项目启动时创建,降低第一次访问的耗时,通过下面配置开启饥饿加载:

ribbon:
  eager-load:
    enabled: true # 开启饥饿加载
    clients: # 指定饥饿加载的服务名称
      - userservice

Nacos 注册中心

GitHub 的 Release 下载页:
单节点启动命令:sh startup.sh -m standalone

父工程:

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-alibaba-dependencies</artifactId>
    <version>2.2.5.RELEASE</version>
    <type>pom</type>
    <scope>import</scope>
</dependency>

客户端:

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

yml配置:

spring:
  cloud:
    nacos:
      server-addr: nacos:8848 # nacos服务地址

Nacos 服务集群配置

举例同时在上海,广州部署 User、Order 集群服务,让广州的 Order 服务调用广州 User 服务。

  1. 需要在 yml 配置(User、Order 同样配置)
spring:
  cloud:
    nacos:
     server-addr: nacos:8848 # nacos服务地址
     discovery:
       cluster-name: GZ # 配置集群名称,也就是机房位置,例如:GZ,广州

图片.png

  1. 修改 Ribbon 负载均衡策略:
userservice:
  ribbon:
    NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule # 负载均衡规则

跨集群请求 NacosRule 会报警告。

Nacos 权重负载均衡

Nacos 提供了权重配置来控制访问频率,权重越大则访问频率越高

  1. 在 Nacos 控制台可以设置实例的权重值,首先选中实例后面的编辑按钮
图片.png 图片.png

Nacos namespace

Nacos 中服务存储和数据存储的最外层都是一个名为 namespace 的东西,用来做最外层隔离

图片.png

修改 yml 配置:

spring:
  cloud:
    nacos:
      server-addr: nacos:8848 # nacos服务地址
      discovery:
        cluster-name: GZ # 配置集群名称,也就是机房位置,例如:HZ,杭州
        namespace: dbf6ce8b-96c8-4156-8769-24f1a8748913 # dev环境
        ephemeral: false # 是否是临时实例

对比 Nacos Eureka

Nacos 与 Eureka 的共同点

  • 支持服务注册和服务拉取。
  • 都支持服务提供者心跳方式做健康检测。

Nacos 与 Eureka 的区别

  • Nacos 支持服务端主动检测提供者状态:临时实例采用心跳模式,非临时实例采用主动检测模式。
  • 临时实例心跳不正常会被剔除,非临时实例则不会被剔除。
  • Nacos 支持服务列表变更的消息推送模式,服务列表更新更及时。
  • Nacos 集群默认采用 AP 方式,当集群中存在非临时实例时,采用 CP 模式;Eureka 采用 AP 方式。

注释:AP强调的是一致性,CP强调一致性同时强调数据库可用性

Nacos 统一配置管理

配置更改热更新

配置管理服务

图片.png

resource 目录添加一个 bootstrap.yml 文件,这个文件是引导文件,优先级高于application.yml

spring:
  application:
    name: userservice
  profiles:
    active: dev # 环境
  cloud:
    nacos:
      config:
        server-addr: localhost:8848 # nacos地址
        file-extension: yaml # 文件后缀名
        namespace: ee095b45-cb75-4ff8-b8bd-16873644fce1
        group: DEFAULT_GROUP

方式一:在@Value注入的变量所在类上添加注解@RefreshScope

图片.png

方式二:使用@ConfigurationProperties注解

图片.png

Nacos 集群搭建

图片.png

主要以下三点:

  • Nacos 集群并初始化数据库表。
  • 修改集群配置(节点信息)、数据库配置。
  • 分别启动多个 Nacos 节点。

OpenFeign

Feign 日志级别配置

图片.png

配置 Feign 日志有两种方式:

第一种方式:配置文件

feign:  
  client:
    config: 
      userservice: # 针对某个微服务的配置
        loggerLevel: FULL #  日志级别 
feign:  
  client:
    config: 
      default: # 这里用default就是全局配置,如果是写服务名称,则是针对某个微服务的配置
        loggerLevel: FULL #  日志级别 

第二种方式:Java 代码方式,需要先声明一个 Bean

public class DefaultFeignConfiguration {
    @Bean
    public Logger.Level logLevel(){
        return Logger.Level.BASIC;
    }
}

作用域单个服务

@FeignClient(value = "userservice")
public interface UserClient {

    @GetMapping("/user/{id}")
    User findById(@PathVariable("id") Long id);
}

作用域全局

@SpringBootApplication
@EnableFeignClients(clients = UserClient.class,defaultConfiguration = DefaultFeignConfiguration.class)
public class OrderApplication {

    public static void main(String[] args) {
        SpringApplication.run(OrderApplication.class, args);
    }
}    

日志最好用 basicnone

使用 HTTPClient 连接池

Feign 默认使用 URLConnection 不支持连接池

<!--引入HttpClient依赖-->
<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-httpclient</artifactId>
</dependency>
feign:
  httpclient:
    enabled: true # 支持HttpClient的开关
    max-connections: 200 # 最大连接数
    max-connections-per-route: 50 # 单个路径的最大连接数

Gateway 网关

任何请求先进网关

图片.png

网关功能主要有以下几点:

  • 身份认证和权限校验。
  • 服务路由、负载均衡。
  • 请求限流。

SpringCloud 中网关的实现包括两种:gateway、zuul
Zuul 是基于 Servlet 的实现,属于阻塞式编程。而 SpringCloudGateway 则是基于 Spring5 中提供的 WebFlux,属于响应式编程的实现,具备更好的性能。

搭建网关

  1. 引入相关依赖
<!--nacos服务注册发现依赖-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--网关gateway依赖-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
  1. 添加配置
spring:
  application:
    name: gateway
  cloud:
    nacos:
      server-addr: nacos:8848 # nacos地址
    gateway:
      routes:
        - id: user-service # 路由标示,必须唯一
          uri: lb://userservice # 路由的目标地址
          predicates: # 路由断言,判断请求是否符合规则
            - Path=/user/** # 路径断言,判断路径是否是以/user开头,如果是则符合
        - id: order-service
          uri: lb://orderservice
          predicates:
            - Path=/order/**

服务注册到服务中心,网关表通过路径规则代理到服务,网关拿到服务名去注册中心找对应的地址,然后负载均衡,发送请求。

图片.png

路由断言工厂 Route Predicate Factory

我们在配置文件中写的断言规则只是字符串,这些字符串会被 PredicateFactory 读取并处理,转变为路由判断的条件

例如: Path=/user/** 是按照路径匹配,这个规则是由org.springframework.cloud.gateway.handler.predicate.PathRoutePredicateFactory 类来处理的。
像这样的断言工厂在 SpringCloudGateway 还有十几个。

图片.png

路由过滤器 GatewayFilter

GatewayFilter 是网关中提供的一种过滤器,可以对进入网关的请求和微服务返回的响应做处理:

图片.png

在 Gateway 中修改 application.yml 文件,给 userservice 的路由添加过滤器:

spring:
  application:
    name: gateway
  cloud:
    nacos:
      server-addr: localhost:8848 # nacos地址
    gateway:
      routes:
        - id: user-service # 路由标示,必须唯一
          uri: lb://userservice # 路由的目标地址
          predicates: # 路由断言,判断请求是否符合规则
            - Path=/user/** # 路径断言,判断路径是否是以/user开头,如果是则符合
          filters: # 过滤器
            - AddRequestHeader=X-Request-red, blue # 添加请求头

如果要对所有的路由都生效,则可以将过滤器工厂写到 default 下。格式如下:

spring:
  application:
    name: gateway
  cloud:
    nacos:
      server-addr: localhost:8848 # nacos地址
    gateway:
      routes:
        - id: user-service # 路由标示,必须唯一
          uri: lb://userservice # 路由的目标地址
          predicates: # 路由断言,判断请求是否符合规则
            - Path=/user/** # 路径断言,判断路径是否是以/user开头,如果是则符合
          filters: # 过滤器
            - AddRequestHeader=Truth, Itcast is freaking awesome! # 添加请求头
        - id: order-service
          uri: lb://orderservice
          predicates:
            - Path=/order/**
      default-filters:
        - AddRequestHeader=X-Request-red, blue # 添加请求头

全局过滤器 GlobalFilter

全局过滤器的作用也是处理一切进入网关的请求和微服务响应,与 GatewayFilter 的作用一样。
区别在于 GatewayFilter 通过配置定义,处理逻辑是固定的。而 GlobalFilter 的逻辑需要自己写代码实现。

定义方式是实现 GlobalFilter 接口。

@Component
public class AuthorizeFilter implements GlobalFilter {

    /**
     *  处理当前请求,有必要的话通过{@link GatewayFilterChain}将请求交给下一个过滤器处理
     * @param exchange 请求上下文,里面可以获取Request、Response等信息
     * @param chain 用来把请求委托给下一个过滤器
     * @return {@code Mono<Void>} 返回标示当前过滤器业务结束
     */
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 1.获取请求参数
        ServerHttpRequest request = exchange.getRequest();
        MultiValueMap<String, String> params = request.getQueryParams();
        // 2.获取参数中的 authorization 参数
        String auth = params.getFirst("authorization");
        // 3.判断参数值是否等于 admin
        if ("admin".equals(auth)) {
            // 4.是,放行
            return chain.filter(exchange);
        }
        // 5.否,拦截
        // 5.1.设置状态码
        exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
        // 5.2.拦截请求
        return exchange.getResponse().setComplete();
    }
}

过滤器执行顺序,数字越小执行权限越高。

@Order(-1)
@Component
public class AuthorizeFilter implements GlobalFilter {
}

过滤器执行顺序

请求进入网关会碰到三类过滤器:当前路由的过滤器、DefaultFilter、GlobalFilter
请求路由后,会将当前路由过滤器和 DefaultFilter、GlobalFilter,合并到一个过滤器链(集合)中,排序后依次执行每个过滤器。

图片.png

每一个过滤器都必须指定一个 int 类型的 order 值,order 值越小,优先级越高,执行顺序越靠前。
GlobalFilter 通过实现 Ordered 接口,或者添加 @Order 注解来指定 order 值,由我们自己指定。
路由过滤器和 defaultFilterorderSpring 指定,默认是按照声明顺序从1递增。
当过滤器的 order 值一样时,会按照 defaultFilter > 路由过滤器 > GlobalFilter 的顺序执行。

跨域问题处理

跨域问题:浏览器禁止请求的发起者与服务端发生跨域 ajax 请求,请求被浏览器拦截的问题。

网关处理跨域采用的同样是 CORS 方案。

spring:
  application:
    name: gateway
  cloud:
    gateway:
      globalcors: # 全局的跨域处理
        add-to-simple-url-handler-mapping: true # 解决options请求被拦截问题
        corsConfigurations:
          '[/**]':
            allowedOrigins:  # 允许哪些网站的跨域请求
              - "http://localhost:8090"
              - "http://www.baidu.com"
              allowedMethods: # 允许的跨域ajax的请求方式
                - "GET"
                - "POST"
                - "DELETE"
                - "PUT"
                - "OPTIONS"
              allowedHeaders: "*" # 允许在请求中携带的头信息
                allowCredentials: true # 是否允许携带cookie            
              maxAge: 360000 # 这次跨域检测的有效期

Spring AMQP

应用间消息通信的一种协议,与语言和平台无关。

  1. 引入相关依赖
<!--AMQP依赖,包含RabbitMQ-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
  1. 修改配置文件
spring:
  rabbitmq:
    host: 192.168.*.* # rabbitMQ的ip地址
    port: 5672 # 端口
    username: root
    password: root
    virtual-host: /
  1. 发送消息

利用 RabbitTemplateconvertAndSend 方法。

@Autowired
private RabbitTemplate rabbitTemplate;

public void sendMessage2SimpleQueue() {
    String queueName = "simple.queue";
    String message = "hello, spring amqp!";
    rabbitTemplate.convertAndSend(queueName, message);
}
  1. 接收消息 在 consumer 服务中编写 application.yml,添加 MQ 连接信息。
spring:
  rabbitmq:
    host: 192.168.*.* # rabbitMQ的ip地址
    port: 5672 # 端口
    username: root
    password: root
    virtual-host: /
@Component
public class SpringRabbitListener {

     @RabbitListener(queues = "simple.queue")
     public void listenSimpleQueue(String msg) {
         System.out.println("消费者接收到simple.queue的消息:【" + msg + "】");
     }
 }

Work、Queue 工作队列

Work、Queue,两者工作队列,可以提高消息处理速度,避免队列消息堆积。

图片.png
@RabbitListener(queues = "simple.Work")
public void listenWorkQueue1(String msg) throws InterruptedException {
    System.out.println("消费者1接收到消息:【" + msg + "】" + LocalTime.now());
    Thread.sleep(20);
}

@RabbitListener(queues = "simple.Work")
public void listenWorkQueue2(String msg) throws InterruptedException {
    System.err.println("消费者2........接收到消息:【" + msg + "】" + LocalTime.now());
    Thread.sleep(200);
}

Work 模式会有消息预取功能通过配置修改预取数量。

spring:
  rabbitmq:
    host: 192.168.*.* # rabbitMQ的ip地址
    port: 5672 # 端口
    username: root
    password: root
    virtual-host: /
    listener:
      simple:
        prefetch: 5

发布( Publish )、订阅( Subscribe )

发布订阅模式与之前案例的区别就是允许将同一消息发送给多个消费者。实现方式是加入了exchange(交换机)。

常见 exchange 类型包括:

  • Fanout:广播
  • Direct:路由
  • Topic:话题

图片.png

注意:exchange 负责消息路由,而不是存储,路由失败则消息丢失。

发布订阅-Fanout Exchange

Fanout Exchange 会将接收到的消息广播到每一个跟其绑定的 Queue

  1. 声明交换机。
@Configuration
public class FanoutConfig {
    // summer.fanout
    @Bean
    public FanoutExchange fanoutExchange(){
        return new FanoutExchange("summer.fanout");
    }

    // fanout.queue1
    @Bean
    public Queue fanoutQueue1(){
        return new Queue("fanout.queue1");
    }

    // 绑定队列1到交换机
    @Bean
    public Binding fanoutBinding1(Queue fanoutQueue1, FanoutExchange fanoutExchange){
        return BindingBuilder
                .bind(fanoutQueue1)
                .to(fanoutExchange);
    }

    // fanout.queue2
    @Bean
    public Queue fanoutQueue2(){
        return new Queue("fanout.queue2");
    }

    // 绑定队列2到交换机
    @Bean
    public Binding fanoutBinding2(Queue fanoutQueue2, FanoutExchange fanoutExchange){
        return BindingBuilder
                .bind(fanoutQueue2)
                .to(fanoutExchange);
    }

}
  1. 发送消息
public void sendFanoutExchange() {
    // 交换机名称
    String exchangeName = "summer.fanout";
    // 消息
    String message = "hello, every one!";
    // 发送消息
    rabbitTemplate.convertAndSend(exchangeName, "", message);
}
  1. 消息接收
@RabbitListener(queues = "fanout.queue1")
public void listenFanoutQueue1(String msg) {
    System.out.println("消费者接收到fanout.queue1的消息:【" + msg + "】");
}
@RabbitListener(queues = "fanout.queue2")
public void listenFanoutQueue2(String msg) {
    System.out.println("消费者接收到fanout.queue2的消息:【" + msg + "】");
}

发布订阅-DirectExchange

Direct Exchange 会将接收到的消息根据规则路由到指定的 Queue,因此称为路由模式(routes)。

  • 每一个Queue都与Exchange设置一个BindingKey
  • 发布者发送消息时,指定消息的RoutingKey
  • Exchange将消息路由到BindingKey与消息RoutingKey一致的队列。
图片.png
  1. 利用注解绑定交换机
@RabbitListener(bindings = @QueueBinding(
        value = @Queue(name = "direct.queue1"),
        exchange = @Exchange(name = "summer.direct", type = ExchangeTypes.DIRECT),
        key = {"red", "blue"}
))
public void listenDirectQueue1(String msg){
    System.out.println("消费者接收到direct.queue1的消息:【" + msg + "】");
}

@RabbitListener(bindings = @QueueBinding(
        value = @Queue(name = "direct.queue2"),
        exchange = @Exchange(name = "summer.direct", type = ExchangeTypes.DIRECT),
        key = {"red", "yellow"}
))
public void listenDirectQueue2(String msg){
    System.out.println("消费者接收到direct.queue2的消息:【" + msg + "】");
}
  1. 发送消息
public void sendDirectExchange() {
   // 交换机名称
   String exchangeName = "summer.direct";
   // 消息
   String message = "hello, red!";
   // 发送消息
   rabbitTemplate.convertAndSend(exchangeName, "red", message);
}

发布订阅-TopicExchange

TopicExchangeDirectExchange 类似,区别在于 routingKey 必须是多个单词的列表,并且以.分割。

QueueExchange 指定 BindingKey 时可以使用通配符:

  • #:代指0个或多个单词
  • *:代指一个单词

图片.png

  1. 利用注解绑定交换机
@RabbitListener(bindings = @QueueBinding(
        value = @Queue(name = "topic.queue1"),
        exchange = @Exchange(name = "summer.topic", type = ExchangeTypes.TOPIC),
        key = "china.#"
))
public void listenTopicQueue1(String msg){
    System.out.println("消费者接收到topic.queue1的消息:【" + msg + "】");
}

@RabbitListener(bindings = @QueueBinding(
        value = @Queue(name = "topic.queue2"),
        exchange = @Exchange(name = "summer.topic", type = ExchangeTypes.TOPIC),
        key = "#.weather"
))
public void listenTopicQueue2(String msg){
    System.out.println("消费者接收到topic.queue2的消息:【" + msg + "】");
}
  1. 发送消息
public void sendTopicExchange() {
    // 交换机名称
    String exchangeName = "summer.topic";
    // 消息
    String message = "今天天气不错,我的心情好极了!";
    // 发送消息
    rabbitTemplate.convertAndSend(exchangeName, "china.weather", message);
}

消息转换器

Spring 的对消息对象的处理是由:
org.springframework.amqp.support.converter.MessageConverter来处理的。而默认实现是 SimpleMessageConverter,基于 JDKObjectOutputStream 完成序列化。

如果要修改只需要定义一个 MessageConverter 类型的 Bean 即可。推荐用 JSON 方式序列化,步骤如下:

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
</dependency>
@Bean
public MessageConverter messageConverter(){
    return new Jackson2JsonMessageConverter();
}

elasticsearch

elasticsearch 是一款非常强大的开源搜索引擎,可以帮助我们从海量数据中快速找到需要的内容。
elasticsearch 结合 kibanaLogstashBeats,也就是 elastic stack(ELK)。被广泛应用在日志数据分析、实时监控等领域。

图片.png

elasticsearch 采用倒排索引

  • 文档(document):每条数据就是一个文档。
  • 词条(term):文档按照语义分成的词语。

倒排索引先找词在找 id

图片.png

倒排索引中包含两部分内容:

  • 词条词典(Term Dictionary):记录所有词条,以及词条与倒排列表(Posting List)之间的关系,会给词条创建索引,提高查询和插入效率。
  • 倒排列表(Posting List):记录词条所在的文档 id、词条出现频率 、词条在文档中的位置等信息。
    • 文档id:用于快速获取文档。
    • 词条频率(TF):文档在词条出现的次数,用于评分。

elasticsearch 文档存储

elasticsearc 是面向文档存储的,可以是数据库中的一条商品数据,一个订单信息。
文档数据会被序列化为 json 格式后存储在 elasticsearch 中。

图片.png

elasticsearch 索引(Index)

  • 索引(index):相同类型的文档的集合。
  • 映射(mapping):索引中文档的字段约束信息,类似表的结构约束。

图片.png

elasticsearch mapping属性

mapping 是对索引库中文档的约束,常见的 mapping 属性包括:

  • index:是否创建索引,默认为 true。
  • analyzer:使用哪种分词器。
  • properties:该字段的子字段。
  • type:字段数据类型,常见的简单类型有:
    • 字符串:text(可分词的文本)、keyword(精确值,例如:品牌、国家、ip 地址)
    • 数值:long、integer、short、byte、double、float、
    • 布尔:boolean
    • 日期:date
    • 对象:object

RestClient

ES 官方提供了各种不同语言的客户端,用来操作 ES。这些客户端的本质就是组装 DSL 语句,通过http 请求发送给 ES。

官方文档地址:www.elastic.co/guide/en/el…

引入相关依赖

<!--elasticsearch-->
<dependency>
    <groupId>org.elasticsearch.client</groupId>
    <artifactId>elasticsearch-rest-high-level-client</artifactId>
</dependency>

因为 SpringBoot 默认的 ES 版本是7.6.2,所以我们需要覆盖默认的 ES 版本:

<properties>
    <java.version>1.8</java.version>
    <elasticsearch.version>7.12.1</elasticsearch.version>
</properties>
RestHighLevelClient client = new RestHighLevelClient(RestClient.builder(
        HttpHost.create("http://192.168.150.101:9200")));

创建索引库

// 1.创建Request对象
CreateIndexRequest request = new CreateIndexRequest("hotel");
// 2.请求参数,MAPPING_TEMPLATE是静态常量字符串,内容是创建索引库的DSL语句
request.source(MAPPING_TEMPLATE, XContentType.JSON);
// 3.发起请求
client.indices().create(request, RequestOptions.DEFAULT);

删除索引库代码如下:

client.indices().delete(request, RequestOptions.DEFAULT);

判断索引库是否存在

// 1.创建Request对象
GetIndexRequest request = new GetIndexRequest("hotel");
// 2.发起请求
boolean exists = client.indices().exists(request, RequestOptions.DEFAULT);

添加 doc 数据

// 1.准备Request
IndexRequest request = new IndexRequest("hotel").id(1);
// 2.准备请求参数DSL,其实就是文档的JSON字符串
request.source(json, XContentType.JSON);
// 3.发送请求
client.index(request, RequestOptions.DEFAULT);

获取 doc 数据

// 1.准备Request      // GET /hotel/_doc/{id}
GetRequest request = new GetRequest("hotel", "1");
// 2.发送请求
GetResponse response = client.get(request, RequestOptions.DEFAULT);
// 3.解析响应结果
String json = response.getSourceAsString();

删除 doc 数据

// 1.准备Request      // DELETE /hotel/_doc/{id}
DeleteRequest request = new DeleteRequest("hotel", "61083");
// 2.发送请求
client.delete(request, RequestOptions.DEFAULT);

修改 doc 数据

// 1.准备Request
UpdateRequest request = new UpdateRequest("hotel", "1");
// 2.准备参数
request.doc("price", "870");
// 3.发送请求
client.update(request, RequestOptions.DEFAULT);

批量导入数据

// 1.准备Request
BulkRequest request = new BulkRequest();
// 2.准备参数
for (Hotel hotel : list) {
    // 2.1.转为HotelDoc
    HotelDoc hotelDoc = new HotelDoc(hotel);
    // 2.2.转json
    String json = JSON.toJSONString(hotelDoc);
    // 2.3.添加请求
    request.add(new IndexRequest("hotel").id(hotel.getId().toString()).source(json, XContentType.JSON));
}
// 3.发送请求
client.bulk(request, RequestOptions.DEFAULT);

文档查询

Elasticsearch 提供了基于 JSON的 DSL(Domain Specific  Language)来定义查询。常见的查询类型包括:

  • 查询所有:查询出所有数据,一般测试用。例如:match_all
  • 全文检索(full text)查询:利用分词器对用户输入内容分词,然后去倒排索引库中匹配。例如: match_querymulti_match_query

精确查询:根据精确词条值查找数据,一般是查找 keyword、数值、日期、boolean 等类型字段。

  • ids
  • range
  • term

地理(geo)查询:根据经纬度查询。

  • geo_distance
  • geo_bounding_box

复合(compound)查询:复合查询可以将上述各种查询条件组合起来,合并查询条件。

  • bool
  • function_score

查询出所有数据

// 1.准备request
SearchRequest request = new SearchRequest("hotel");
// 2.准备请求参数
request.source().query(QueryBuilders.matchAllQuery());
// 3.发送请求,得到响应
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 4.结果解析
SearchHits searchHits = response.getHits();
// 4.1.总条数
long total = searchHits.getTotalHits().value;
System.out.println("总条数:" + total);
// 4.2.获取文档数组
SearchHit[] hits = searchHits.getHits();
// 4.3.遍历
for (SearchHit hit : hits) {
    // 4.4.获取source
    String json = hit.getSourceAsString();
    // 4.5.反序列化,非高亮的
    HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
    // 4.6.处理高亮结果
    // 1)获取高亮map
    Map<String, HighlightField> map = hit.getHighlightFields();
    // 2)根据字段名,获取高亮结果
    HighlightField highlightField = map.get("name");
    // 3)获取高亮结果字符串数组中的第1个元素
    String hName = highlightField.getFragments()[0].toString();
    // 4)把高亮结果放到HotelDoc中
    hotelDoc.setName(hName);
    // 4.7.打印
    System.out.println(hotelDoc);
}

全文检索查询

全文检索的 matchmulti_match 查询与 match_all 的 API 基本一致。差别是查询条件,也就是 Query 的部分。

同样是利用QueryBuilders提供的方法:
单字段查询

QueryBuilders.matchQuery("all", "如家");

多字段查询

QueryBuilders.multiMatchQuery("如家", "name", "business");

精确查询

词条查询

QueryBuilders.termQuery("city", "杭州"); 

范围查询

QueryBuilders.rangeQuery("price").gte(100).lte(150);

复合查询-boolean query

// 创建布尔查询
 BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
// must
boolQuery.must(QueryBuilders.termQuery("city", "杭州"));
// filter
boolQuery.filter(QueryBuilders.rangeQuery("price").lte(250));

排序和分页

int page = 2,size = 5;

// 1.准备request
SearchRequest request = new SearchRequest("hotel");
// 2.准备请求参数
// 2.1.query
request.source()
        .query(QueryBuilders.matchAllQuery());
// 2.2.排序sort
request.source().sort("price", SortOrder.ASC);
// 2.3.分页 from\size
request.source().from((page - 1) * size).size(size);

// 3.发送请求,得到响应
SearchResponse response = client.search(request, RequestOptions.DEFAULT);

高亮

// 高亮
request.source().highlighter(new HighlightBuilder().field("name").requireFieldMatch(false));

数据聚合

聚合的种类

可以实现对文档数据的统计、分析、运算。聚合常见的有三类:

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

  • TermAggregation:按照文档字段值分组。
  • Date Histogram:按照日期阶梯分组,例如:一周为一组,或者一月为一组。

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

  • Avg:求平均值。
  • Max:求最大值。
  • Min:求最小值。
  • Stats:同时求 max、min、avg、sum 等。

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

// 1.准备请求
SearchRequest request = new SearchRequest("hotel");
// 2.请求参数
// 2.1.size 文档数据
request.source().size(0);
// 2.2.聚合
request.source().aggregation(
        AggregationBuilders.terms("brandAgg").field("brand").size(20));
// 3.发出请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 4.解析结果
Aggregations aggregations = response.getAggregations();
// 4.1.根据聚合名称,获取聚合结果
Terms brandAgg = aggregations.get("brandAgg");
// 4.2.获取buckets
List<? extends Terms.Bucket> buckets = brandAgg.getBuckets();
// 4.3.遍历
for (Terms.Bucket bucket : buckets) {
    String brandName = bucket.getKeyAsString();
    System.out.println("brandName = " + brandName);
    long docCount = bucket.getDocCount();
    System.out.println("docCount = " + docCount);
}

自动补全

要实现根据字母做补全,就必须对文档按照拼音分词。在 GitHub 上恰好有 Elasticsearch 的拼音分词插件。

地址:github.com/medcl/elast…

自定义分词器

elasticsearch 中分词器(analyzer)的组成包含三部分:

  • character filters:在tokenizer 之前对文本进行处理。例如:删除字符、替换字符。
  • tokenizer:将文本按照一定的规则切割成词条(term)。例如:keyword,就是不分词;还有ik_smart。
  • tokenizer filter:将 tokenizer 输出的词条做进一步处理。例如:大小写转换、同义词处理、拼音处理等。

为了避免搜索到同音字,搜索时不要使用拼音分词器

// 1.准备请求
SearchRequest request = new SearchRequest("hotel");
// 2.请求参数
request.source().suggest(new SuggestBuilder()
        .addSuggestion(
                "hotelSuggest",
                SuggestBuilders
                        .completionSuggestion("suggestion")
                        .size(10)
                        .skipDuplicates(true)
                        .prefix("s")
        ));
// 3.发出请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 4.解析
Suggest suggest = response.getSuggest();
// 4.1.根据名称获取结果
CompletionSuggestion suggestion = suggest.getSuggestion("hotelSuggest");
// 4.2.获取options
for (CompletionSuggestion.Entry.Option option : suggestion.getOptions()) {
    // 4.3.获取补全的结果
    String str = option.getText().toString();
    System.out.println(str);
}

数据同步

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

方式一:同步调用

  • 优点:实现简单,粗暴。
  • 缺点:业务耦合度高。

方式二:异步通知

  • 优点:低耦合,实现难度一般。
  • 缺点:依赖 mq 的可靠性。

方式三:监听 binlog

  • 优点:完全解除服务间耦合。
  • 缺点:开启 binlog 增加数据库负担、实现复杂度高。