Java微服务技术栈(1)

1. 全篇综述

Java微服务技术栈不像Java web笔记和SSM一样了,微服务技术栈涉及到的第三方技术太多了,为了方便以后的查找,不能再像Java web笔记和SSM笔记一样,着重记原理,代码记得少。这部分原理和代码基本处于同一比重,好了,让我们以下面这张图,开始我们的微服务技术栈之旅吧
Java微服务技术栈(1)_第1张图片

2. 微服务的头部:服务信息的管理

2.1 eureka注册中心

我们设想,部署在两台服务器上的不同业务,想要互相进行业务的访问可以怎么办?正如访问页面的跳转一般,在Java后台,我们也可以通过RestTemplete发起HTTP请求,并接收返回包。RestTemplete是智能的,你仍然可以通过传入类的class属性,来完成由SpringBoot框架返回的Json数据的自动赋值。上述过程可以总结为如下的代码:

// in order application, add the bean of restTemplete
@Bean
public RestTemplate restTemplate(){
        return new RestTemplate();
}

// in order service, autowire the bean and use the function of http request to view the service on other server
User user = restTemplate.getForObject("http://localhost:8081/user/" + order.getUserId(), User.class);

我们可以发现,虽然SpringBoot框架提供了RestTemplete这样的类来辅助进行多台服务器间的独立部署,但这种侵入式的编程实在是不雅观。比如一台服务器的项目发生迁移,地址改变,另一台服务器也必须重新部署。

因此,一个合理的思路就是:

建立一个专门的注册中心,注册中心的任务是:

  1. 为服务器的地址信息以及专用名建立字段,服务器间通过专用名向注册中心请求地址并相互访问
  2. 维护服务器存活信息,eureka要求服务器每隔30s进行心跳续约,保证注册中心中的服务器都是可访问的

这就是eureka完成注册管理工作的原理,而具体的使用方式则分为三个步骤

/***
1. 搭建EurekaServer
	* 引入eureka-server依赖
	* 添加@EnableEurekaServer注解
	* 在application中配置eureka地址
2. 服务注册
	* 引入eureka-client依赖
	* 在application中配置eureka地址
3. 服务发现
	* 给RestTemplete添加@LoadBalance注解
	* 用服务提供者的服务名远程调用
***/
// 服务注册
// in order,user,eureka application.yml
server:
  port: 10086 # 服务端口
spring:
  application:
    name: eurekaserver # eureka的服务名称
eureka:
  client:
    service-url:  # eureka的地址信息
      defaultZone: http://127.0.0.1:10086/eureka
2.2 nacos注册中心

nacos集成了eureca的思想和操作方式,由于其服务器启动于命令行,所以不需要在项目里添加服务器模块,只需要添加客户端依赖即可

nacos相比于eureca的优势主要体现在它对服务器的主动性:(1)对于消费者增加了主动推送变更信息的功能,当有服务器done机时,消费者可以更快知道;(2)对于生产者增加临时实例和非临时实例的区别,临时实例采用被动的心跳检测,并且在done机后剔除服务器,非临时实例采用主动的问询,在done机后不会主动剔除,而是等待服务器恢复正常

nacos的工作流程如下图所示(比eureka多了push线)
Java微服务技术栈(1)_第2张图片

2.3 ribbon负载均衡

由于微服务架构中,服务器由注册中心进行域名的管理,所以在进行负载均衡时,服务消费者需要和注册中心进行拉取才能调用负载均衡算法。因此,有关于ribbon负载均衡的内容也记录在Section 2中

负载均衡有很多相关算法,如常见的随机选择,轮循选择,或者是基于地域与权重的优先度选择等等

ribbon中集成了很多相关的负载均衡算法,通过IRule属性进行控制(因此,一种修改默认负载均衡算法的方法就是直接填充一个返回值类型为IRule的Bean),有关于ribbon集成的负载均衡算法可以参看下面的博客:Ribbon七种负载均衡算法

除了通过添加Bean外,还可以在application.yml中,为特定的域名服务器新增IRule规则修改,从而实现精准控制

userservice:
  ribbon:
    NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule  # 负载均衡规则
2.4 nacos配置中心

nacos还实现了一个配置管理中心,服务器可以把需要进行热更新的配置信息放在nacos里,当该配置发生变化的时候,nacos会主动通告与该配置关联的服务器,如果服务器端读取时设置了热更新,那么配置信息就可以直接应用于服务器

在nacos中添加配置文件需要设置Data ID,这个值必须唯一且按照标准格式命名:

[service name]-[profile].[yaml or properties] # 第一种nacos配置文件,精准关联不同环境的服务
[service name].[yaml or properties] # 第二种nacos配置文件,关联所有环境的同名服务
配置信息优先级:第一种>第二种(范围越小,优先级越高) 远端>本地(热更新高于静态配置)

首先要明确,热更新文件是建立在nacos中,但对热更新文件的关联是来自于服务器端的bootstrap.yaml配置。因此nacos只会向完全匹配的服务器推送对应的配置更新。服务器端bootstrap.yaml配置内容为:

spring:
  application:
    name: [service_name]
  profiles:
    active: [profile] # 环境
  cloud:
    nacos:
      server-addr: [nacos_addr]:[port] # nacos地址
      config:
        file-extension: yaml # 文件后缀名

# 读取顺序bootstrap>nacos远端配置文件>application,因此在bootstrap进行配置可以让项目知道nacos的地址信息
2.5 nacos的集群

nacos的集群,或者说集群这个概念本身就很有意思,今天借着Section 2.5,我们就来浅析一下集群和单点所不同的工作。

首先,我们根据下图所示建立了一个3节点组成的nacos集群
Java微服务技术栈(1)_第3张图片
要理解集群,首先必须认识到等价节点的信息一致性,与单点工作不同,单点是独立存储运行信息,而在集群中,等价节点能接收到的信息是一致的。如上图所示,三个nacos节点连在同一个数据库集群且访问无限制区别,所以这三个节点是等价的。

信息一致性的好处就是,nginx做负载均衡的时候,只需要根据负载均衡规则选择执行任务的节点,而不需要担心它是否能执行这个任务。

我们结合Section 2.2中nacos注册中心的业务来进一步说明集群的工作:

  • nacos client处理Section 2.2中对于所有关联服务器的pull以及push任务(也可能存在对服务器返回数据的存储任务等等),生成任务序列
  • 任务序列发放至nginx,由nginx根据负载均衡向所有节点分配任务
  • 节点接收任务指令并执行任务,向服务器发送信息

总结:

  • nacos client就像检测口,接收请求(其实本质就是nginx部署时的服务器,这里为了便于理解而分开阐述)
  • nginx就像传送带,分发进入的“包裹”
  • 各个等价节点就像处理机器,处理包裹并传出

nacos集群的建立不难,首先是三个nacos节点的部署,然后在nginx中声明监听和反向代理的ip地址即可,需要注意的是,nacos节点部署时需要对application.properties中数据库进行配置,以及在cluster.conf中声明集群所有节点ip地址才能运行start命令

nginx的配置内容如下:

	upstream nacos-cluster {
		server [nacos_server_1_name:port];
		server [nacos_server_2_name:port];
		server [nacos_server_3_name:port];
	}

	server {
		listen       80;
		server_name  [cluster_server_name];

		location /nacos {
			proxy_pass http://nacos-cluster;
		}
	}

附一个批量启动的cmd代码,这样就不用苦哈哈手动启动了:

D:
cd D:\nacos1\bin
start startup.cmd
cd D:\nacos2\bin
start startup.cmd
cd D:\nacos3\bin
start startup.cmd
cd D:\nginx-1.18.0
start nginx.exe

顺便扩展一下,如果对nginx感兴趣的可以移步我的另一篇博客:nginx原理浅述与配置总结

2.6 服务器间的HTTP请求

服务消费者在获取到生产者地址后,还需要通过客户端来发送HTTP请求,以完成消费任务

基础的方式如Section 2.1中所介绍的,使用RestTemplete类来发送HTTP请求。但由于其在编写复杂请求的URL路径上的困难,我们更倾向于使用与SpringMVC有相同逻辑的feign来实现客户端请求

为了实现代码的复用,消费被抽取为一个独立的feign-api来管理,消费者可调用该jar包来选择对应的消费功能;同时,为了减少每一次请求都重新建立TCP的三次握手和四次挥手,我们采用连接池来优化feign的性能

上述功能在具体实现上,体现为如下步骤:

  • 独立的feign-api创建
    • 根据SpringMVC controller层代码规范,书写客户端访问代码,注意为类添加@FeignClient(value = [service_name]) 的注解
    • 可以选择建立config文件夹,编写config控制类,通过Bean将返回的config对象注入Spring容器,为消费者提供默认的配置信息
  • 消费者调用feign-api,生成FeignClient的Bean实例
    • 在SpringBoot启动类上添加@EnableFeignClients(clients = {[client_name].class}, defaultConfiguration = [config_name].class)注解,生成符合[config_name][client_name]的Bean实例
    • 在application.yml配置启用连接池
    • 自动装配[client_name]的Bean实例并进行消费

一些相关的代码如下:
feign-api的前置依赖:


<dependency>
    <groupId>org.springframework.cloudgroupId>
    <artifactId>spring-cloud-starter-openfeignartifactId>
dependency>

<dependency>
    <groupId>io.github.openfeigngroupId>
    <artifactId>feign-httpclientartifactId>
dependency>

消费者中进行feign连接池的启用配置:

feign:
  httpclient:
    enabled: true # 支持HttpClient的开关
    max-connections: 200 # 最大连接数
    max-connections-per-route: 50 # 单个路径的最大连接数

3. 微服务的门户:Gatway

3.1 路由

gatway设置了10多种路由工厂,可以设置不同的条件,只有满足筛选条件的才由Gateway路由到对应的服务器地址,路由工厂的详细说明可以参看官方中文文档:gateway路由断言工厂

由于需要路由到服务器,所以gateway模块也需要在nacos中进行注册

这里我们简单举一个路径断言工厂在application.yml中的配置方案:

spring:
  cloud:
    gateway:
      routes:
      - id: path_route # 唯一标识id
        uri: lb://orderservice # 要路由的nacos中的服务器名
        predicates:
        - Path=/order/{segment} # 请求URI路径需要满足的条件
3.2 过滤

过滤分为默认过滤器(default-filters),相关路由过滤器(filters),全局过滤器(GlobalFilter),它们都是加载在路由之后的执行器,如下图所示:
Java微服务技术栈(1)_第4张图片
其中,默认过滤器和相关路由过滤器只是影响的路由范围不同,但底层实现都是各种各样的GatewayFilter工厂所创建的GatewayFilter实例。工厂的详细描述可以参见:GatewayFilter工厂。

而GlobalFilter是一个提供filter方法的接口,filter方法如下:

// exchange是融合了request信息和response信息的实例,chain自动移动的过滤器链
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        log.info("custom global filter");
        return chain.filter(exchange);
    }

依赖exchange,我们可以做很多过滤逻辑,例如,想对没有登陆的用户返回登陆服务器的重定向信息,只需要向exchange的response写内容返回即可

		 ServerHttpResponse response = exchange.getResponse();
		 String redirectUrl = [主机名]/[login];
		 logger.info("bmg 重定向到URL: {}", redirectUrl);
		 response.getHeaders().set(HttpHeaders.LOCATION, redirectUrl);
		 //303状态码表示由于请求对应的资源存在着另一个URI,应使用GET方法定向获取请求的资源
		 response.setStatusCode(HttpStatus.SEE_OTHER);
		 response.getHeaders().add("Content-Type", "text/plain;charset=UTF-8");
		 return response.setComplete();

过滤器间首先以优先级做排序,其中默认过滤器和相关路由过滤器都是在各自的集合内以1为优先级顺延的;在优先级相同的情况下,再按照默认过滤器->相关路由过滤器->全局过滤器排列。

3.3 跨域

跨域指的是浏览器在访问某个URL中的ajax请求时,发现请求的URL路径与当前正在访问的URL要么域名不同,要么端口不同。这种情况下浏览器会拒绝本次ajax请求,这就是跨域问题

要解决跨域问题,只需要浏览器在访问某个URL时,由对应的服务器告知浏览器该URL访问可以跨域即可,这就是CORS

在Gateway中,通过配置形式实现了CORS,配置详情可参照:Gateway CORS

4. 微服务的容器:Docker

Docker个人觉得是微服务技术栈里很有创新的一个功能了,它针对各种服务的依赖不同,编译测试环境不同而导致的部署困难,无法跨环境(例如当服务的依赖编译自Ubuntu,就无法迁移到Centos,因为调用函数不同),提出了很巧妙的解决方案:

  1. 将一个个的服务封装为镜像,镜像包含服务所需的所有依赖,这里的依赖不仅指第三方工具,还指工具所依赖的系统函数。
  2. 通过容器启动镜像,可以不依赖于当前系统的函数,而是直接通过封装的系统函数去调用内核。即不论编译成镜像时的环境如何,启动该镜像的容器都是可以直接运行在任何Linux系统的

Docker的解决方案中有两个重要的内容,镜像以及启动镜像的容器,下面我们将一一进行介绍

4.1 镜像

镜像在docker中就是服务封装的别称,一个镜像代表着一个部署后可独立自主运行的服务。而镜像部分最关键的知识就是镜像的生成。只有了解镜像是如何生成的,我们才能把我们自己的微服务给部署到docker中

镜像就是用来告诉docker要怎么来启动打包的服务:

  • 基础镜像层:告知docker需要依赖的操作系统,方便docker打包相关的系统函数库
  • 顺序构建层:按照依赖的先后顺序,分层告知docker应该按照什么顺序去导入(或构建)包(以及依赖、配置)
  • 入口层:镜像运行的入口,包括启动脚本和访问端口

以Java项目构建镜像为例,一个可行的Dockerfile的结构一般和下面类似:

# 指定基础镜像
FROM ubuntu:16.04
# 配置环境变量,JDK的安装目录
ENV JAVA_DIR=/usr/local

# 拷贝jdk和java项目的包
COPY ./jdk8.tar.gz $JAVA_DIR/
COPY ./docker-demo.jar /tmp/app.jar

# 安装JDK
RUN cd $JAVA_DIR \
 && tar -xf ./jdk8.tar.gz \
 && mv ./jdk1.8.0_144 ./java8

# 配置环境变量
ENV JAVA_HOME=$JAVA_DIR/java8
ENV PATH=$PATH:$JAVA_HOME/bin

# 暴露端口
EXPOSE 8090
# 入口,java项目的启动命令
ENTRYPOINT java -jar /tmp/app.jar

由于环境变量配置,JDK导入和安装等等都是重复性工作,所以Docker中又提供了一个快速配置的镜像,引入该镜像后,Dockerfile只需要配置拷贝java项目和入口即可,可以很好的简化镜像生成

FROM java:8-alpine
COPY ./app.jar /tmp/app.jar
EXPOSE 8090
ENTRYPOINT java -jar /tmp/app.jar
4.2 容器

容器是启动镜像的平台,内部是一个简化版的linux系统(即只包含了镜像运行所需的函数库与文件夹)

一般来说,通过docker run命令启动的容器默认位于子网bridge,默认启动的各容器可以直接通过名称进行相互之间的访问(不用向外暴露端口)

还可以通过docker-compose批量启动容器,docker-compose会自动创建虚拟子网并把批启动的容器放在同一子网管辖,因此它们也可以通过名称互相访问

容器的子网是可以通过docker network命令改变的,也可以在docker-compose启动时选择子网,从而保证启动方式不同的各个容器处于同一子网下,这方面可移步我的另一篇博客:通过docker-compose完成负载均衡的nacos集群,里面有更细的原理和实验展示

除了同一服务器上部署容器时的名称访问需求外,对于在不同服务器上进行的集群部署,我们也希望它们之间可以实现名称访问,从而避免外露端口,以及在服务器IP发生改变时不需要修改调用代码。这方面需要依赖于docker-swarm技术,在上述博客中我也有介绍相关的文章

5. 微服务的分布式转发:RabbitMQ

5.1 RabbitMQ原理简介

RabbitMQ是一个生产者和消费者模型,通过消息队列实现了生产者和消费者之间的异步运行,消费者可以直接将消息丢到消息队列中,再由与消息队列绑定的消费者依据设定的预请求长度来分配消息。消息队列的关键点是:消息是一次性的,消费完即清除

为了解除消息队列的局限性,RabbitMQ实现了转发器,生产者只需要关注要发布到的转发器,转发器根据关键词将消息复制并转发到多个对应的消息队列,再由与消息队列绑定的消费者进行消费

RabbitMQ实现发布消息与消费消息的异步执行,可以达到解耦,异步,削峰的目的,具体来说就是:

  • 在生产者面临大量并发数据时,可以将其不间断地丢给转发器,而不需要逐条处理,因此对于发布者而言,单条数据的处理时间降低,其并发能力大大提升
  • 转发器将数据转存至消息队列,消费者可以依据自身实力平稳消费,而不需要担心CPU的过载问题,降低了消费者面对大量并发数据时崩溃的概率(削峰)
  • 生产者不需要关心消费者的进度,消费者也不用绑定发布者,一方的修改一般不会引起另一方的重新编译和部署(消费者的处理结果也可以通过逆向创建一个转发器和消息队列告知生产者,从而实现完全解耦)

Java微服务技术栈(1)_第5张图片

5.2 SpringAMQP介绍

SpringAMQP是对RabbitMQ的封装,简化了转发器和消息队列的生成,消息的发布和接收,其使用步骤为:

  • 在pom文件中导入amqp的起步依赖
  • 在application中配置Rabbit的地址,用户名以及密码等信息
  • 在主函数中配置MessageConverter的替代Bean(可以用Jackson),从而保证对于message实现json数据和java对象的自动转化
  • 利用自动装配在生产者中引入RabbitTemplate,利用convertAndSend函数即可发布消息;同时,在消费者中通过@RabbitListener注解即可完成消息队列与消费者函数的绑定

但是SpringAMQP有一个缺点,它在使用@RabbitListener注解自动生成转发器,Routekey以及消息队列时,不会自动删除原有的转发器和消息队列(即使代码中已经没有使用),而只会创造新的。这使得我们不可以直接利用删除配置来完成RabbitMQ的更新,这是非常容易出现失误的地方

所以如果想要批量清除原有配置,进入RabbitMQ容器后,使用以下命令:

[root@centos7 ~]# docker exec -it server-rabbit /bin/bash
root@my-rabbit:/# rabbitmqctl stop_app
root@my-rabbit:/# rabbitmqctl reset
root@my-rabbit:/# rabbitmqctl start_app

6. 微服务的分布式搜索库:Elasticsearch

6.1 原理解析

Mysql的CRUD操作都是依据其顺序增加的索引进行的,在Mysql中,顺序索引通过btree数来进行管理,实现n - > log n的性能优化。

但事实上,在百万,千万级的数据下,顺序索引的查找匹配速度远远满足不了需求;除此之外,Mysql的每一次的查找都是磁盘匹配,磁盘读写速度远慢于内存读写

编程界有一个永恒不变的真理:

当你面对一个难题时,为它加一层新的架构也许就解决问题了

正如名言所云,Elasticsearch对海量数据的搜索优化正是来于加一层:

既然海量数据太大,是绝无可能放入内存进行读写,那么可以提前对海量数据提取term并生成term dict;如果term dict还是太大,还可以对term dict再加一层抽取,生成term index。最后,term index放入内存中,进行快速读取

上述的过程被称为倒排索引,这是Elasticsearch的核心思想,该过程构建后,数据库的索引结构大致变为下面的样式[1]:
Java微服务技术栈(1)_第6张图片

第一层抽取放在了内存中,可根据查询关键词快速定位term dict(由于内存的读写速度在100MB/s,即使加上逻辑处理,这个过程也会被控制在微秒级),然后提取完全匹配的dict对应的Posting List,并对多个Posting List进行“与”合并

这里的“与”合并是有说法的,如果按照遍历的思想去做,其复杂度为 O ( N 2 ) O(N^2) O(N2)

而Elasticsearch支持着两种合并方式:

  • 使用skip list数据结构进行遍历
  • 使用bitset数据结构,进行二进制的AND操作

如果Posting List缓存到了内存,Elasticsearch使用bitset进行合并,其复杂度只有 O ( 1 ) O(1) O(1),反之则利用skip list进行合并,复杂度为 O ( N l o g N ) O(NlogN) O(NlogN)

举个简单的skip list用例,如下图所示的3个Posting List。我们可以选出最短的一个list来做主遍历,把其他两个list转为skip list[2],在这两个skip list上,从上一次访问点开始,用skip list从高level向低level逐步匹配,直至匹配成功或是小于主遍历的值(该过程还可以将不同的skip list放到不同线程上,用并行来进行加速)
Java微服务技术栈(1)_第7张图片

6.2 搜索介绍

上一小节只是梳理清楚了Elasticsearch为搜索打下的索引基础,基于此,Elasticsearch的匹配速度是远远超过传统的关系型数据库的。但对于搜索这个问题本身还没有涉及,本小节就搜索这一问题的核心三问,脱开Elasticsearch的束缚而直指搜索的本质,当然,这一过程中也会伴随着一些Elasticsearch对搜索的实践

核心三问:

  1. 搜索是怎么进行的
  2. 返回多少搜索结果,匹配关键词有哪些
  3. 返回结果按照什么顺序展示

只要梳理清楚了核心三问,你对于搜索的本质就有了更深入的了解

  • Q1:搜索是怎么进行的?
    • A1:我们一般进行的是关键词搜索,即系统根据用户所提供的关键词来一步步地缩小匹配范围,这个过程中,建立有序的关键词队列是关键,这里的“序”指的是该关键词提供的作用,一般分为:
      • 全文检索查询:即用户的输入是一段包含关键词的字符串,系统首先自动提取关键词,然后与专用的信息检索字段(一般是多个关键词的整合)做匹配
      • 精确查询:即用户指定一个精确查询的值或范围,要求系统必须/选择性(不)包含指定的关键词,系统与对应的字段进行精确地匹配

在Elasticsearch中,提供了各种API来创建对应的有序关键词队列。与全文检索查询相关的是: m a t c h _ a l l , m a t c h match\_all, match match_all,match,与精确查询相关的是: t e r m , r a n g e term,range term,range
在组合多个查询条件时, m u s t , s h o u l d must,should must,should分别代表逻辑与和逻辑或,它们将参与到算分中; m u s t _ n o t , f i l t e r must\_not,filter must_not,filter分别代表与非和与,但特点是不参与算分。
精确查询进行TF-IDF计算是低效的,因此一般通过 m u s t _ n o t , f i l t e r must\_not,filter must_not,filter直接禁止精确查找进行算分过程,可以大大加快查询速度

  • Q2:返回多少搜索结果,匹配关键词有哪些?
    • A2:限制搜索结果返回数的原因是海量数据的搜索没有必要罗列所有的结果,我们关心的可能仅仅是TopN个,因此我们需要可控的调整搜索返回结果。这就是搜索结果的分页;而除此之外,一个好的搜索行为应该可以明确地标出搜索引擎匹配该条搜索结果的原因,即匹配关键词的高亮显示

在Elasticsearch中,搜索结果的分页由 f r o m from from s i z e size size提供,分别代表分页的起始条数和每页条数;而搜索匹配词的高亮则由 h i g h l i g h t highlight highlight提供,需要人工指定哪些关键词匹配后需要做高亮显示,且仅仅是把高亮文本另起一行放在了创建的 h i g h l i g h t highlight highlight中,需要人工提取并替换 s o u r c e source source中的对应字段

  • Q3:返回结果按照什么顺序展示?
    • A3:
      • 方式1:由于全文检索查询时可以设定多个关键词,因此排序时必须有特殊的规则来整合各个条目的不同字段下关键词的匹配结果。最为传统的是 T F − I D F TF-IDF TFIDF算法,进阶算法则是Elasticsearch中采用的 B M 25 − T F BM25-TF BM25TF,相关文章那就太多了,想了解的可以移步[1]。整合匹配结果后,给出了各个条目的相关性评分,返回结果按照相关性由高到低的顺序展示
      • 方式2:在方式1的基础上,可以自定义打分函数。除计算相关性评分外,还可以根据字段的一些特殊需求(如交了广告费)设置额外的得分函数并与相关性评分融合,从而以人工干预的相关性顺序展示
      • 方式3:除了相关性计算和自定义打分函数外,还可以对某个字段或对某几个字段【以某个优先级】进行排序,然后按照排序的顺序展示

在Elasticsearch中,默认对全文检索查询实现了 B M 25 − T F BM25-TF BM25TF评分,也可以通过将 m a t c h match match包裹在 f u n c t i o n _ s c o r e function\_score function_score下来为打分结果增加人工干预函数,最终辅以精确查询的筛选返回结果展示的顺序;如果指定了 s o r t sort sort字段,Elasticsearch将不再计算相关度,而是按照 s o r t sort sort进行结果的返回

相关的代码实践,我将在下一篇博客中进行细致的记录

如果对搜推有所兴趣,个人也有一篇项目导向的博客,可以作为了解[3]

扩展:聚合与自动补全

为了丰富搜索的功能,提升用户的使用体验,搜索一般都会搭配着聚合功能以及自动补全功能

  • 聚合功能:聚合一般分为对字段的值或值的录入时间分布的聚合,包括词频统计,最大值、最小值、平均值等等,目的是体现某个字段的值分布规律;由此可以延伸出第二种聚合,它基于第一种的统计结果,对多个字段的值分布进行统计性研究

在elastsearch中,把聚合分为桶聚合(按照字段的值或值的录入时间进行统计并按顺序返回);度量聚合(按照字段的值进行统计值的计算);管道聚合(其他聚合的结果汇总之后再进行新的聚合)
聚合功能很好地满足了我对term dictionary如何获取的好奇心(但对于分词形成的term dictionary仍无法得到)

  • 自动补全功能:搜索时,无论用户输入的是首字母拼音,还是全拼,或是汉字,都可以给出相关的“联想”,来帮助用户快速定位想要搜索的内容,这就是自动补全功能。一个在最少的输入下联想出用户需求词条的自动补全功能才是最完美的自动补全功能

elastisearch提供了一个自动补全功能的函数——suggest,但其自动补全时,所有匹配结果的score都是1.0,因此其返回顺序不一定是我们所希望的顺序,可以在得到返回顺序后,通过定制化排序函数来增强自动补全的效果

扩展:elasticsearch集群

一个成熟的微服务,需要自己完成集群(大雾)

虽是玩笑,但微服务的出发点确实是为了利用集群的分布是思维来应对高并发的场景,因此,便捷的集群配置、管理就成了衡量一个微服务技术栈好坏的标准答案

在这方面,elasticsearch是目前所有介绍的技术栈里做的最好的:

  • 方便的部署:只需要在discovery.zen.ping.unicast.hosts配置中声明集群的所有节点IP与端口,然后在cluster.name配置中声明集群名称即可完成互联

  • 灵活的任务:每个节点都可以通过node.*=true or false来配置节点角色,从而分配节点任务,各司其职(master,data,ingest,coordinating)

  • 智能的分片:节点分开了,数据也要分开存储在各个节点上,为了避免节点宕机而出现数据丢失,每个节点都会额外存储其他节点的分片备份。此外,在分片时,也会通过 h a s h ( _ r o u t i n g ) % s h a r d s hash(\_routing) \% shards hash(_routing)%shards来保证分片数据的均匀分布

    • 在一个数据节点(data)宕机后,集群的处理方式是:
      • 立即启动该节点分片的备份,将备份升级为主分片,此时集群的主分片数据仍是完整的,集群状态为yellow,代表如果继续宕机节点,有可能出现永久性的数据丢失(假如每个节点仅备份一份分片)
      • 在等待宕机节点超时后,判断节点无法恢复,集群会将该节点所备份的分片重新从对应主分片中提取并存放至存活节点,集群状态恢复为green,此时即使继续宕机节点,数据也是完整的
      • 在超时时间内宕机节点恢复,集群会将宕机节点对应的主分片和备份分片进行更新,然后再降级之前升级为主分片的对应分片
    • 在当选主节点(master)宕机后,集群的处理方式是:
      • 从备选主节点中执行选举,只有选票大于等于 ( m a s t e r _ n u m b e r + 1 ) / 2 (master\_number+1)/2 (master_number+1)/2的选举节点才当选,在只有当选主节点出问题时,该方案可以避免当选主节点“假死”的脑裂情形产生(但并没有完全消除)
扩展:elasticsearch和mysql的同步方案

教程中给出了三种elasticsearch和mysql做数据同步的方案:

  • 在JAVA业务层执行mysql更新成功后,调用elasticsearch相关的JAVA业务层做相同更新
    • 优点:不需要额外的中间件配置
    • 缺点:紧耦合,单步更新时间延长 ,必须考虑时序问题,由mysql在确定事务提交成功后,才执行elasti更新
  • 添加RabbitMQ中间件,在JAVA业务层执行mysql更新成功后,发布RabbitMQ的Publisher消息,elasticsearch相关的JAVA业务层监听消息队列并做相同更新
    • 优点:MQ是成熟高效的中间件,除了消息结构必须保持一致外,基本做到松耦合
    • 缺点:必须考虑时序问题,由mysql在确定事务提交成功后,才执行elasticsearch更新
  • 利用canal中间件,监听Mysql的binlog并在binlog更新后通知elasticsearch相关的JAVA业务层做相同更新
    • 优点:完全松耦合,由于直接监听binlog,因此不需要考虑时序问题
    • 缺点:配置最为复杂

需要注意的是,在上述三个方案中我都在强调着时序问题,这是因为mysql是有事务管理的,而elasticsearch没有。因此,当JAVA业务层或mysql出现问题从而导致事务回滚时,数据的更新不会记录在mysql中,如果不注意时序,那么就可能出现elasticsearch更新了,而mysql未更新的情况

反之,如果是elasticsearch相关的JAVA业务层出现问题,此时的情况是mysql更新,而elasticsearch未更新或部分更新。如果可以通过代码恢复,就重新执行,否则只能重新从mysql中执行数据导入

在支持事务的数据库和不支持事务的数据库联合使用时,保证前者的数据安全性永远重于后者

你可能感兴趣的:(Java笔记随录,java,微服务,eureka)