本文章完整视频和资料
链接:https://pan.baidu.com/s/1uekXKwT_dWYSAOREsBNirg?pwd=achf 
 
认识微服务 微服务是一种经过良好架构设计的分布式架构方案,微服务的特征有:
单一职责:微服务拆分粒度小,每一个服务都对应唯一的业务能力,做到单一职责,避免重复开发 
面向服务:微服务对外暴露业务接口 
自治:团队独立,技术独立,数据独立,部署独立 
隔离性强:微服务调用做好隔离,容错,降级,避免出现级联问题 
 
各架构设计的特点 
单体式架构
简单方便,高度耦合,扩展性差,适合小型项目。例如:学生管理系统 
 
 
分布式架构特点
松耦合,扩展性好,但架构复杂,难度大。适合大型互联网项目。例如:京东,淘宝 
 
 
微服务:一种良好的分布式架构方案
优点:拆分粒度更小,服务更独立、耦合度更低 
缺点:架构非常复杂,运维、监控、部署难度提高 
 
 
 
微服务技术对比 
微服务中父pom文件 在这个文件中管理下面子工程的依赖版本号,然后子工程中引入依赖不需要写版本号
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 <?xml version="1.0" encoding="UTF-8"?> <project  xmlns ="http://maven.apache.org/POM/4.0.0"           xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance"           xsi:schemaLocation ="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" >     <modelVersion > 4.0.0</modelVersion >      <groupId > cn.itcast.demo</groupId >      <artifactId > cloud-demo</artifactId >      <version > 1.0</version >      <modules >          <module > user-service</module >          <module > order-service</module >      </modules >      <packaging > pom</packaging >      <parent >          <groupId > org.springframework.boot</groupId >          <artifactId > spring-boot-starter-parent</artifactId >          <version > 2.3.9.RELEASE</version >          <relativePath />      </parent >      <properties >          <project.build.sourceEncoding > UTF-8</project.build.sourceEncoding >          <project.reporting.outputEncoding > UTF-8</project.reporting.outputEncoding >          <java.version > 1.8</java.version >          <spring-cloud.version > Hoxton.SR10</spring-cloud.version >          <mysql.version > 5.1.47</mysql.version >          <mybatis.version > 2.1.1</mybatis.version >      </properties >      <dependencyManagement >          <dependencies >                           <dependency >                  <groupId > org.springframework.cloud</groupId >                  <artifactId > spring-cloud-dependencies</artifactId >                  <version > ${spring-cloud.version}</version >                  <type > pom</type >                  <scope > import</scope >              </dependency >                           <dependency >                  <groupId > mysql</groupId >                  <artifactId > mysql-connector-java</artifactId >                  <version > ${mysql.version}</version >              </dependency >                           <dependency >                  <groupId > org.mybatis.spring.boot</groupId >                  <artifactId > mybatis-spring-boot-starter</artifactId >                  <version > ${mybatis.version}</version >              </dependency >          </dependencies >      </dependencyManagement >      <dependencies >          <dependency >              <groupId > org.projectlombok</groupId >              <artifactId > lombok</artifactId >          </dependency >      </dependencies >  </project > 
EureKa注册中心 使用场景 假如我们的服务提供者user-service部署了多个实例,如图:
大家思考几个问题:
order-service在发起远程调用的时候,该如何得知user-service实例的ip地址和端口? 
有多个user-service实例地址,order-service调用时该如何选择? 
order-service如何得知某个user-service实例是否依然健康,是不是已经宕机? 
 
Eureka的结构和作用 这些问题都需要利用SpringCloud中的注册中心来解决,其中最广为人知的注册中心就是Eureka,其结构如下:
回答之前的各个问题。
问题1:order-service如何得知user-service实例地址?
获取地址信息的流程如下:
user-service服务实例启动后,将自己的信息注册到eureka-server(Eureka服务端)。这个叫服务注册 
eureka-server保存服务名称到服务实例地址列表的映射关系 
order-service根据服务名称,拉取实例地址列表。这个叫服务发现或服务拉取 
 
问题2:order-service如何从多个user-service实例中选择具体的实例?
order-service从实例列表中利用负载均衡算法选中一个实例地址 
向该实例地址发起远程调用 
 
问题3:order-service如何得知某个user-service实例是否依然健康,是不是已经宕机?
user-service会每隔一段时间(默认30秒)向eureka-server发起请求,报告自己状态,称为心跳 
当超过一定时间没有发送心跳时,eureka-server会认为微服务实例故障,将该实例从服务列表中剔除 
order-service拉取服务时,就能将故障实例排除了 
 
注意:一个微服务,既可以是服务提供者,又可以是服务消费者,因此eureka将服务注册、服务发现等功能统一封装到了eureka-client端
 
因此,接下来我们动手实践的步骤包括:
搭建eureka-server 创建eureka-serve服务 在父工程上右键新建 Module
选择 Maven 创建
输入服务名称,点击创建即可
引入eureka依赖 1 2 3 4 5 <dependency >     <groupId > org.springframework.cloud</groupId >      <artifactId > spring-cloud-starter-netflix-eureka-server</artifactId >  </dependency > 
添加启动类 1 2 3 4 5 6 7 @EnableEurekaServer @SpringBootApplication public  class  EurekaApplication      public  static  void  main (String[] args)           SpringApplication.run(EurekaApplication.class,args);     } } 
编写配置文件 1 2 3 4 5 6 7 8 9 10 11 server:   port:  10086  spring:   application:      name:  eurekaserver  eureka:   client:      service-url:        defaultZone:  http://127.0.0.1:10086/eureka   
访问页面 启动 EurekaApplication ,本地访问 127.0.0.1:10086
服务注册 我们将 user-server 注册到 eureka-server 中
添加依赖 1 2 3 4 5 <dependency >     <groupId > org.springframework.cloud</groupId >      <artifactId > spring-cloud-starter-netflix-eureka-client</artifactId >  </dependency > 
添加配置 1 2 3 4 5 6 7 spring:   application:      name:  orderserver   eureka:   client:      service-url:        defaultZone:  http://127.0.0.1:10086/eureka  
启动多个实例 在服务名称上右键选择 Copy Configuration
修改新实例的端口号
现在 OrderServer 就会有两个实例
我们按照相同的方法吧 user-server 注册到 eureka-server 中
服务发现 目前我们打开 order-server 的 controller,调用获取订单的方法
可以看到只能获取到用户id,如果想要获取用户信息,则必须调用另外一个服务的接口
添加 RestTemplate 实例 在上下文添加 RestTemplate Bean,同时添加 @LoadBalanced 注解,这个注解的作用是开启负载均衡
1 2 3 4 5 @Bean @LoadBalanced public  RestTemplate restTemplate ()     return  new  RestTemplate(); } 
改写queryOrderById方法 
调用 http://localhost:8080/order/101  接口查看返回
这样我们不必关心另外一个服务的端口号,直接将服务名替换成ip地址和端口号即可
Ribbon负载均衡 负载均衡原理 SpringCloud底层其实是利用了一个名为Ribbon的组件,来实现负载均衡功能的。
那么我们发出的请求明明是http://userservice/user/1,怎么变成了http://localhost:8081的呢? 
请求图
负载均衡策略 负载均衡的规则都定义在IRule接口中,而IRule有很多不同的实现类:
不同规则的含义如下:
内置负载均衡规则类 规则描述  
 
RoundRobinRule 
简单轮询服务列表来选择服务器。它是Ribbon默认的负载均衡规则。 
 
AvailabilityFilteringRule 
对以下两种服务器进行忽略:   (1)在默认情况下,这台服务器如果3次连接失败,这台服务器就会被设置为“短路”状态。短路状态将持续30秒,如果再次连接失败,短路的持续时间就会几何级地增加。  (2)并发数过高的服务器。如果一个服务器的并发连接数过高,配置了AvailabilityFilteringRule规则的客户端也会将其忽略。并发连接数的上限,可以由客户端的..ActiveConnectionsLimit属性进行配置。   
 
WeightedResponseTimeRule 
为每一个服务器赋予一个权重值。服务器响应时间越长,这个服务器的权重就越小。这个规则会随机选择服务器,这个权重值会影响服务器的选择。 
 
ZoneAvoidanceRule 以区域可用的服务器为基础进行服务器的选择。使用Zone对服务器进行分类,这个Zone可以理解为一个机房、一个机架等。而后再对Zone内的多个服务做轮询。 
 
BestAvailableRule 
忽略那些短路的服务器,并选择并发数较低的服务器。 
 
RandomRule 
随机选择一个可用的服务器。 
 
RetryRule 
重试机制的选择逻辑 
 
默认的实现就是ZoneAvoidanceRule,是一种轮询方案
自定义负载均衡 方式一 在 orderserver 启动类中添加 IRule 实例
1 2 3 4 @Bean public  IRule randomRule ()     return  new  RandomRule(); } 
这种方式是全局生效的,从 orderserver 服务发出的请求都将采用随机策略来做负载均衡
方式二 1 2 3 userserver:    ribbon:      NFLoadBalancerRuleClassName:  com.netflix.loadbalancer.RandomRule   
这种方式只会让 userserver 的服务使用修改后的负载均衡策略
一般用默认的负载均衡规则,不做修改
 
饥饿加载 Ribbon默认是采用懒加载,即第一次访问时才会去创建LoadBalanceClient,请求时间会很长。
而饥饿加载则会在项目启动时创建,降低第一次访问的耗时,通过下面配置开启饥饿加载:
1 2 3 4 5 ribbon:   eager-load:      enabled:  true       clients:         -  userserver  
Nacso注册中心 安装并启动 Nacos 是阿里巴巴的产品,现在是SpringCloud Eureka 功能更加丰富,在国内受欢迎程度较高。
从官网下载并解压到非中文目录即可
启动方法:打开 bin 目录双击运行
运行地址:http://192.168.60.74:8848/nacos/index.html 
默认账号密码都是:nacos
服务注册 在父工程添加依赖
1 2 3 4 5 6 <dependency >     <groupId > com.alibaba.cloud</groupId >      <artifactId > spring-cloud-starter-alibaba-nacos-discovery</artifactId >      <version > 2.2.5.RELEASE</version >  </dependency > 
子工程添加依赖,同时注释掉 eureka
1 2 3 4 5 <dependency >     <groupId > com.alibaba.cloud</groupId >      <artifactId > spring-cloud-starter-alibaba-nacos-discovery</artifactId >  </dependency > 
修改配置文件
1 2 3 4 5 6 7 8 9 10 11 12 spring:   application:      name:  orderserver     cloud:      nacos:        server-addr:  localhost:8848  
查看 nacos 服务列表
服务多级存储模型 一个服务 可以有多个实例 ,例如我们的user-service,可以有:
127.0.0.1:8081 
127.0.0.1:8082 
127.0.0.1:8083 
 
假如这些实例分布于全国各地的不同机房,例如:
127.0.0.1:8081,在上海机房 
127.0.0.1:8082,在上海机房 
127.0.0.1:8083,在杭州机房 
 
Nacos就将同一机房内的实例 划分为一个集群 。
也就是说,user-service是服务,一个服务可以包含多个集群,如杭州、上海,每个集群下可以有多个实例,形成分级模型,如图:
设置集群 修改配置项,添加 cluster-name 属性
1 2 3 4 5 6 7 8 9 spring:   application:      name:  userserver    cloud:       nacos:        server-addr:  localhost:8848        discovery:          cluster-name:  HZ   
然后再复制一个实例配置,设置一个新的实例名为 SH
1 -Dserver.port=9092 -Dspring.cloud.nacos.discovery.cluster-name=SH 
当前启动的实例如下
刷新 nacos 面板
NacosRule负载均衡 首先也要给 orderserve 添加一个 HZ 集群
1 2 3 4 5 6 7 8 9 spring:   application:      name:  orderserver     cloud:      nacos:        server-addr:  localhost:8848        discovery:          cluster-name:  HZ   
设置负载均衡规则
1 2 3 userserver:    ribbon:      NFLoadBalancerRuleClassName:  com.alibaba.cloud.nacos.ribbon.NacosRule   
这样设置完成后将会优先访问同集群内的服务,如果同集群中的服务失效,则会访问其他集群中的服务,同时也会触发一条警告信息,提示当前同集群的服务出现故障
权重配置 实际部署中会出现这样的场景:
服务器设备性能有差异,部分实例所在机器性能较好,另一些较差,我们希望性能好的机器承担更多的用户请求。
但默认情况下NacosRule是同集群内随机挑选,不会考虑机器的性能问题。
因此,Nacos提供了权重配置来控制访问频率,权重越大则访问频率越高。
在nacos控制台,找到 userserver 的实例列表,点击编辑,即可修改权重:
注意 :如果权重修改为0,则该实例永远不会被访问
 
环境隔离 只有在相同环境下的服务才能被互相访问到
新建环境
给服务设置所属空间,添加 namespace 属性,值填写生成的id
1 2 3 4 5 6 7 8 9 10 spring:   application:      name:  userserver    cloud:       nacos:        server-addr:  localhost:8848        discovery:          cluster-name:  HZ           namespace:  34024135 -04a1-4cec-a909-08bf1d76c362   
然后将服务重启,查看nacos面板
默认的 public 空间就只剩下 orderserver 实例
dev空间只有 userserver 实例,两个空间中的实例不能互相调用
Nacos与Eureka的区别 Nacos的服务实例分为两种l类型:
配置一个服务实例为永久实例:
1 2 3 4 5 spring:   cloud:      nacos:        discovery:          ephemeral:  false   
Nacos和Eureka整体结构类似,服务注册、服务拉取、心跳等待,但是也存在一些差异:
Nacos与eureka的共同点
都支持服务注册和服务拉取 
都支持服务提供者心跳方式做健康检测 
 
Nacos与Eureka的区别
Nacos支持服务端主动检测提供者状态:临时实例采用心跳模式,非临时实例采用主动检测模式 
临时实例心跳不正常会被剔除,非临时实例则不会被剔除 
Nacos支持服务列表变更的消息推送模式,服务列表更新更及时 
Nacos集群默认采用AP方式,当集群中存在非临时实例时,采用CP模式;Eureka采用AP方式 
 
 
Nacos实现配置管理 统一配置管理 添加配置信息 首先点击配置列表
引入nacos配置依赖 这里提前在父工程设置的依赖版本
1 2 3 4 5 6 <!--nacos配置管理依赖--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> <version>${nacos.config.version}</version> </dependency> 
添加 bootstrap.yml bootstrap.yml 配置文件的读取时机要比 application.yml 文件提前,所以在这个配置文件中设置 nacos 服务地址和要读取的配置信息
1 2 3 4 5 6 7 8 9 10 11 spring:   application:      name:  userserver     profiles:      active:  dev     cloud:      nacos:        server-addr:  localhost:8848         config:          file-extension:  yaml   
记得要把 application.yml 配置文件中的重复配置删除掉,以及命名空间去掉
 
测试是否读取到 添加一个方法,使用 nacos 中的配置来完成日期格式化功能
通过 @Value 注解读取配置文件中的信息,然后实现日期格式化
1 2 3 4 5 6 7 @Value("${pattern.dateformat}") String dateformat; @GetMapping("now") public  String now ()     return  LocalDateTime.now().format(DateTimeFormatter.ofPattern(dateformat)); } 
访问:http://localhost:9091/user/now  接口查看
然后再访问:9090端口同样可以
实现配置热更新 方式一 通过 @RefreshScope 注解的方式来实现热更新,在通过 @Value 注解的所在类上添加这个注解来实现
方式二 使用 @ConfigurationProperties 注解代替 @Value 注解。
新建配置类,添加 @ConfigurationProperties(prefix = “pattern”) 注解,表示读取以 pattern 开头的配置文件,自动读取配置并且给相同名字的属性赋值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package  cn.itcast.user.config;import  lombok.Data;import  org.springframework.boot.context.properties.ConfigurationProperties;import  org.springframework.stereotype.Component;@Data @Component @ConfigurationProperties(prefix = "pattern") public  class  PatternProperties      String dateformat; } 
然后再代码中通过 PatternProperties 类获取配置文件中的值
1 2 3 4 5 6 7 @Autowired PatternProperties patternProperties; @GetMapping("now") public  String now ()     return  LocalDateTime.now().format(DateTimeFormatter.ofPattern(patternProperties.getDateformat())); } 
多环境配置共享 nacos中新建配置文件 当我们有多个环境时,比如有:dev、test、uat等环境都需要共同使用相同的配置时,我们可以来新建一个多环境共享的配置,配置方法为在
dev环境读取配置 在属性配置类中添加 name 属性
新增接口,返回当前的配置文件
1 2 3 4 @GetMapping("prop") public  PatternProperties patternProperties ()     return  patternProperties; } 
查看返回
新增test环境并读取配置 在服务名称上右键选择编辑配置
设置环境为 test 环境
启动服务查看接口返回的配置值
多环境配置共享优先级 我们在本地配置文件中也添加 name 属性
然后再 nacos 中的 dev 环境配置文件也添加 name 属性
然后重启服务,分别访问 test 环境和 dev 环境查看返回
由此得出如下优先级
Nacos集群搭建 1.集群结构图 官方给出的Nacos集群图:
其中包含3个nacos节点,然后一个负载均衡器代理3个Nacos。这里负载均衡器可以使用nginx。
我们计划的集群结构:
三个nacos节点的地址:
节点 
ip 
port 
 
 
nacos1 
192.168.150.1 
8845 
 
nacos2 
192.168.150.1 
8846 
 
nacos3 
192.168.150.1 
8847 
 
2.搭建集群 搭建集群的基本步骤:
搭建数据库,初始化数据库表结构 
下载nacos安装包 
配置nacos 
启动nacos集群 
nginx反向代理 
 
2.1.初始化数据库 Nacos默认数据存储在内嵌数据库Derby中,不属于生产可用的数据库。
官方推荐的最佳实践是使用带有主从的高可用数据库集群,主从模式的高可用数据库可以参考传智教育 的后续高手课程。
这里我们以单点的数据库为例来讲解。
首先新建一个数据库,命名为,而后导入下面的SQL:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 CREATE  TABLE  `config_info` (  `id` bigint (20 ) NOT  NULL  AUTO_INCREMENT COMMENT 'id' ,   `data_id` varchar (255 ) NOT  NULL  COMMENT 'data_id' ,   `group_id` varchar (255 ) DEFAULT  NULL ,   `content` longtext NOT  NULL  COMMENT 'content' ,   `md5` varchar (32 ) DEFAULT  NULL  COMMENT 'md5' ,   `gmt_create` datetime NOT  NULL  DEFAULT  CURRENT_TIMESTAMP  COMMENT '创建时间' ,   `gmt_modified` datetime NOT  NULL  DEFAULT  CURRENT_TIMESTAMP  COMMENT '修改时间' ,   `src_user` text COMMENT 'source user' ,   `src_ip` varchar (50 ) DEFAULT  NULL  COMMENT 'source ip' ,   `app_name` varchar (128 ) DEFAULT  NULL ,   `tenant_id` varchar (128 ) DEFAULT  ''  COMMENT '租户字段' ,   `c_desc` varchar (256 ) DEFAULT  NULL ,   `c_use` varchar (64 ) DEFAULT  NULL ,   `effect` varchar (64 ) DEFAULT  NULL ,   `type` varchar (64 ) DEFAULT  NULL ,   `c_schema` text,   PRIMARY  KEY (`id`),   UNIQUE  KEY `uk_configinfo_datagrouptenant` (`data_id`,`group_id`,`tenant_id`) ) ENGINE= InnoDB DEFAULT  CHARSET= utf8 COLLATE = utf8_bin COMMENT= 'config_info' ; CREATE  TABLE  `config_info_aggr` (  `id` bigint (20 ) NOT  NULL  AUTO_INCREMENT COMMENT 'id' ,   `data_id` varchar (255 ) NOT  NULL  COMMENT 'data_id' ,   `group_id` varchar (255 ) NOT  NULL  COMMENT 'group_id' ,   `datum_id` varchar (255 ) NOT  NULL  COMMENT 'datum_id' ,   `content` longtext NOT  NULL  COMMENT '内容' ,   `gmt_modified` datetime NOT  NULL  COMMENT '修改时间' ,   `app_name` varchar (128 ) DEFAULT  NULL ,   `tenant_id` varchar (128 ) DEFAULT  ''  COMMENT '租户字段' ,   PRIMARY  KEY (`id`),   UNIQUE  KEY `uk_configinfoaggr_datagrouptenantdatum` (`data_id`,`group_id`,`tenant_id`,`datum_id`) ) ENGINE= InnoDB DEFAULT  CHARSET= utf8 COLLATE = utf8_bin COMMENT= '增加租户字段' ; CREATE  TABLE  `config_info_beta` (  `id` bigint (20 ) NOT  NULL  AUTO_INCREMENT COMMENT 'id' ,   `data_id` varchar (255 ) NOT  NULL  COMMENT 'data_id' ,   `group_id` varchar (128 ) NOT  NULL  COMMENT 'group_id' ,   `app_name` varchar (128 ) DEFAULT  NULL  COMMENT 'app_name' ,   `content` longtext NOT  NULL  COMMENT 'content' ,   `beta_ips` varchar (1024 ) DEFAULT  NULL  COMMENT 'betaIps' ,   `md5` varchar (32 ) DEFAULT  NULL  COMMENT 'md5' ,   `gmt_create` datetime NOT  NULL  DEFAULT  CURRENT_TIMESTAMP  COMMENT '创建时间' ,   `gmt_modified` datetime NOT  NULL  DEFAULT  CURRENT_TIMESTAMP  COMMENT '修改时间' ,   `src_user` text COMMENT 'source user' ,   `src_ip` varchar (50 ) DEFAULT  NULL  COMMENT 'source ip' ,   `tenant_id` varchar (128 ) DEFAULT  ''  COMMENT '租户字段' ,   PRIMARY  KEY (`id`),   UNIQUE  KEY `uk_configinfobeta_datagrouptenant` (`data_id`,`group_id`,`tenant_id`) ) ENGINE= InnoDB DEFAULT  CHARSET= utf8 COLLATE = utf8_bin COMMENT= 'config_info_beta' ; CREATE  TABLE  `config_info_tag` (  `id` bigint (20 ) NOT  NULL  AUTO_INCREMENT COMMENT 'id' ,   `data_id` varchar (255 ) NOT  NULL  COMMENT 'data_id' ,   `group_id` varchar (128 ) NOT  NULL  COMMENT 'group_id' ,   `tenant_id` varchar (128 ) DEFAULT  ''  COMMENT 'tenant_id' ,   `tag_id` varchar (128 ) NOT  NULL  COMMENT 'tag_id' ,   `app_name` varchar (128 ) DEFAULT  NULL  COMMENT 'app_name' ,   `content` longtext NOT  NULL  COMMENT 'content' ,   `md5` varchar (32 ) DEFAULT  NULL  COMMENT 'md5' ,   `gmt_create` datetime NOT  NULL  DEFAULT  CURRENT_TIMESTAMP  COMMENT '创建时间' ,   `gmt_modified` datetime NOT  NULL  DEFAULT  CURRENT_TIMESTAMP  COMMENT '修改时间' ,   `src_user` text COMMENT 'source user' ,   `src_ip` varchar (50 ) DEFAULT  NULL  COMMENT 'source ip' ,   PRIMARY  KEY (`id`),   UNIQUE  KEY `uk_configinfotag_datagrouptenanttag` (`data_id`,`group_id`,`tenant_id`,`tag_id`) ) ENGINE= InnoDB DEFAULT  CHARSET= utf8 COLLATE = utf8_bin COMMENT= 'config_info_tag' ; CREATE  TABLE  `config_tags_relation` (  `id` bigint (20 ) NOT  NULL  COMMENT 'id' ,   `tag_name` varchar (128 ) NOT  NULL  COMMENT 'tag_name' ,   `tag_type` varchar (64 ) DEFAULT  NULL  COMMENT 'tag_type' ,   `data_id` varchar (255 ) NOT  NULL  COMMENT 'data_id' ,   `group_id` varchar (128 ) NOT  NULL  COMMENT 'group_id' ,   `tenant_id` varchar (128 ) DEFAULT  ''  COMMENT 'tenant_id' ,   `nid` bigint (20 ) NOT  NULL  AUTO_INCREMENT,   PRIMARY  KEY (`nid`),   UNIQUE  KEY `uk_configtagrelation_configidtag` (`id`,`tag_name`,`tag_type`),   KEY `idx_tenant_id` (`tenant_id`) ) ENGINE= InnoDB DEFAULT  CHARSET= utf8 COLLATE = utf8_bin COMMENT= 'config_tag_relation' ; CREATE  TABLE  `group_capacity` (  `id` bigint (20 ) unsigned NOT  NULL  AUTO_INCREMENT COMMENT '主键ID' ,   `group_id` varchar (128 ) NOT  NULL  DEFAULT  ''  COMMENT 'Group ID,空字符表示整个集群' ,   `quota` int (10 ) unsigned NOT  NULL  DEFAULT  '0'  COMMENT '配额,0表示使用默认值' ,   `usage` int (10 ) unsigned NOT  NULL  DEFAULT  '0'  COMMENT '使用量' ,   `max_size` int (10 ) unsigned NOT  NULL  DEFAULT  '0'  COMMENT '单个配置大小上限,单位为字节,0表示使用默认值' ,   `max_aggr_count` int (10 ) unsigned NOT  NULL  DEFAULT  '0'  COMMENT '聚合子配置最大个数,,0表示使用默认值' ,   `max_aggr_size` int (10 ) unsigned NOT  NULL  DEFAULT  '0'  COMMENT '单个聚合数据的子配置大小上限,单位为字节,0表示使用默认值' ,   `max_history_count` int (10 ) unsigned NOT  NULL  DEFAULT  '0'  COMMENT '最大变更历史数量' ,   `gmt_create` datetime NOT  NULL  DEFAULT  CURRENT_TIMESTAMP  COMMENT '创建时间' ,   `gmt_modified` datetime NOT  NULL  DEFAULT  CURRENT_TIMESTAMP  COMMENT '修改时间' ,   PRIMARY  KEY (`id`),   UNIQUE  KEY `uk_group_id` (`group_id`) ) ENGINE= InnoDB DEFAULT  CHARSET= utf8 COLLATE = utf8_bin COMMENT= '集群、各Group容量信息表' ; CREATE  TABLE  `his_config_info` (  `id` bigint (64 ) unsigned NOT  NULL ,   `nid` bigint (20 ) unsigned NOT  NULL  AUTO_INCREMENT,   `data_id` varchar (255 ) NOT  NULL ,   `group_id` varchar (128 ) NOT  NULL ,   `app_name` varchar (128 ) DEFAULT  NULL  COMMENT 'app_name' ,   `content` longtext NOT  NULL ,   `md5` varchar (32 ) DEFAULT  NULL ,   `gmt_create` datetime NOT  NULL  DEFAULT  CURRENT_TIMESTAMP ,   `gmt_modified` datetime NOT  NULL  DEFAULT  CURRENT_TIMESTAMP ,   `src_user` text,   `src_ip` varchar (50 ) DEFAULT  NULL ,   `op_type` char (10 ) DEFAULT  NULL ,   `tenant_id` varchar (128 ) DEFAULT  ''  COMMENT '租户字段' ,   PRIMARY  KEY (`nid`),   KEY `idx_gmt_create` (`gmt_create`),   KEY `idx_gmt_modified` (`gmt_modified`),   KEY `idx_did` (`data_id`) ) ENGINE= InnoDB DEFAULT  CHARSET= utf8 COLLATE = utf8_bin COMMENT= '多租户改造' ; CREATE  TABLE  `tenant_capacity` (  `id` bigint (20 ) unsigned NOT  NULL  AUTO_INCREMENT COMMENT '主键ID' ,   `tenant_id` varchar (128 ) NOT  NULL  DEFAULT  ''  COMMENT 'Tenant ID' ,   `quota` int (10 ) unsigned NOT  NULL  DEFAULT  '0'  COMMENT '配额,0表示使用默认值' ,   `usage` int (10 ) unsigned NOT  NULL  DEFAULT  '0'  COMMENT '使用量' ,   `max_size` int (10 ) unsigned NOT  NULL  DEFAULT  '0'  COMMENT '单个配置大小上限,单位为字节,0表示使用默认值' ,   `max_aggr_count` int (10 ) unsigned NOT  NULL  DEFAULT  '0'  COMMENT '聚合子配置最大个数' ,   `max_aggr_size` int (10 ) unsigned NOT  NULL  DEFAULT  '0'  COMMENT '单个聚合数据的子配置大小上限,单位为字节,0表示使用默认值' ,   `max_history_count` int (10 ) unsigned NOT  NULL  DEFAULT  '0'  COMMENT '最大变更历史数量' ,   `gmt_create` datetime NOT  NULL  DEFAULT  CURRENT_TIMESTAMP  COMMENT '创建时间' ,   `gmt_modified` datetime NOT  NULL  DEFAULT  CURRENT_TIMESTAMP  COMMENT '修改时间' ,   PRIMARY  KEY (`id`),   UNIQUE  KEY `uk_tenant_id` (`tenant_id`) ) ENGINE= InnoDB DEFAULT  CHARSET= utf8 COLLATE = utf8_bin COMMENT= '租户容量信息表' ; CREATE  TABLE  `tenant_info` (  `id` bigint (20 ) NOT  NULL  AUTO_INCREMENT COMMENT 'id' ,   `kp` varchar (128 ) NOT  NULL  COMMENT 'kp' ,   `tenant_id` varchar (128 ) default  ''  COMMENT 'tenant_id' ,   `tenant_name` varchar (128 ) default  ''  COMMENT 'tenant_name' ,   `tenant_desc` varchar (256 ) DEFAULT  NULL  COMMENT 'tenant_desc' ,   `create_source` varchar (32 ) DEFAULT  NULL  COMMENT 'create_source' ,   `gmt_create` bigint (20 ) NOT  NULL  COMMENT '创建时间' ,   `gmt_modified` bigint (20 ) NOT  NULL  COMMENT '修改时间' ,   PRIMARY  KEY (`id`),   UNIQUE  KEY `uk_tenant_info_kptenantid` (`kp`,`tenant_id`),   KEY `idx_tenant_id` (`tenant_id`) ) ENGINE= InnoDB DEFAULT  CHARSET= utf8 COLLATE = utf8_bin COMMENT= 'tenant_info' ; CREATE  TABLE  `users` (	`username` varchar (50 ) NOT  NULL  PRIMARY  KEY, 	`password` varchar (500 ) NOT  NULL , 	`enabled` boolean  NOT  NULL  ); CREATE  TABLE  `roles` (	`username` varchar (50 ) NOT  NULL , 	`role` varchar (50 ) NOT  NULL , 	UNIQUE  INDEX `idx_user_role` (`username` ASC , `role` ASC ) USING  BTREE ); CREATE  TABLE  `permissions` (    `role` varchar (50 ) NOT  NULL ,     `resource` varchar (255 ) NOT  NULL ,     `action` varchar (8 ) NOT  NULL ,     UNIQUE  INDEX `uk_role_permission` (`role`,`resource`,`action`) USING  BTREE ); INSERT  INTO  users (username, password, enabled) VALUES  ('nacos' , '$2a$10$EuWPZHzz32dJN7jexM34MOeYirDdFAZm2kuWj7VEOJhhZkDrxfvUu' , TRUE );INSERT  INTO  roles (username, role) VALUES  ('nacos' , 'ROLE_ADMIN' );
2.2.下载nacos nacos在GitHub上有下载地址:https://github.com/alibaba/nacos/tags,可以选择任意版本下载。 
本例中才用1.4.1版本:
2.3.配置Nacos 将这个包解压到任意非中文目录下,如图:
目录说明:
进入nacos的conf目录,修改配置文件cluster.conf.example,重命名为cluster.conf:
然后添加内容:
1 2 3 127.0.0.1:8845 127.0.0.1.8846 127.0.0.1.8847 
然后修改application.properties文件,添加数据库配置
1 2 3 4 5 6 7 spring.datasource.platform =mysql db.num =1 db.url.0 =jdbc:mysql://127.0.0.1:3306/nacos?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC db.user.0 =root db.password.0 =123 
2.4.启动 将nacos文件夹复制三份,分别命名为:nacos1、nacos2、nacos3
然后分别修改三个文件夹中的application.properties,
nacos1:
nacos2:
nacos3:
然后分别启动三个nacos节点:
2.5.nginx反向代理 找到课前资料提供的nginx安装包:
解压到任意非中文目录下:
修改conf/nginx.conf文件,配置如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 upstream  nacos-cluster {    server  127.0.0.1:8845 ; 	server  127.0.0.1:8846 ; 	server  127.0.0.1:8847 ; } server  {    listen        80 ;     server_name   localhost;     location  /nacos {         proxy_pass  http://nacos-cluster;     } } 
启动nginx
而后在浏览器访问:http://localhost/nacos即可。 
代码中application.yml文件配置如下:
1 2 3 4 spring:   cloud:      nacos:        server-addr:  localhost:80   
2.6.优化 
Feign远程调用 先来看我们以前利用RestTemplate发起远程调用的代码:
存在下面的问题:
•代码可读性差,编程体验不统一
•参数复杂URL难以维护
Feign是一个声明式的http客户端,官方地址:https://github.com/OpenFeign/feign 
其作用就是帮助我们优雅的实现http请求的发送,解决上面提到的问题。
Feign替代RestTemplate 首先在 orderserver 中添加依赖 1 2 3 4 5 <dependency >     <groupId > org.springframework.cloud</groupId >      <artifactId > spring-cloud-starter-openfeign</artifactId >  </dependency > 
启动类中添加注解 
编写Feign的客户端 1 2 3 4 5 6 @FeignClient("userserver")  public  interface  UserClient      @GetMapping("/user/{id}")      User getUser (@PathVariable("id")  Long id)  ; } 
这个客户端主要是基于SpringMVC的注解来声明远程调用的信息,比如:
服务名称:userservice 
请求方式:GET 
请求路径:/user/{id} 
请求参数:Long id 
返回值类型:User 
 
这样,Feign就可以帮助我们发送http请求,无需自己使用RestTemplate来发送了。
测试 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Service public  class  OrderService      @Autowired      OrderMapper orderMapper;     @Autowired      UserClient userClient;     public  Order queryOrderById (Long orderId)                    Order order = orderMapper.findById(orderId);                  User user = userClient.getUser(order.getUserId());                  order.setUser(user);                  return  order;     } } 
然后分别访问:http://localhost:8080/order/101、http://localhost:8080/order/102、http://localhost:8080/order/103、http://localhost:8080/order/104,来查看 
可以发现 Feign 自带了负载均衡
Feign自定义配置 Feign可以支持很多的自定义配置,如下表所示:
类型 
作用 
说明 
 
 
feign.Logger.Level 修改日志级别 
包含四种不同的级别:NONE、BASIC、HEADERS、FULL 
 
feign.codec.Decoder 
响应结果的解析器 
http远程调用的结果做解析,例如解析json字符串为java对象 
 
feign.codec.Encoder 
请求参数编码 
将请求参数编码,便于通过http请求发送 
 
feign. Contract 
支持的注解格式 
默认是SpringMVC的注解 
 
feign. Retryer 
失败重试机制 
请求失败的重试机制,默认是没有,不过会使用Ribbon的重试 
 
一般情况下,默认值就能满足我们使用,如果要自定义时,只需要创建自定义的@Bean覆盖默认Bean即可。
其中日志的级别分为四种:
NONE:不记录任何日志信息,这是默认值。 
BASIC:仅记录请求的方法,URL以及响应状态码和执行时间 
HEADERS:在BASIC的基础上,额外记录了请求和响应的头信息 
FULL:记录所有请求和响应的明细,包括头信息、请求体、元数据。 
 
修改日志输出 方式一 通过配置文件的方式来修改
我们可以针对于某个服务来设置,这样只会对访问这个服务时执行该级别的日志输出
1 2 3 4 5 feign:   client:      config:        userservice:           loggerLevel:  FULL   
也可以对全局微服务做修改
1 2 3 4 5 feign:   client:      config:        default:           loggerLevel:  FULL   
效果测试,修改前
修改后
方式二 也可以基于Java代码来修改日志级别,先声明一个类,然后声明一个Logger.Level的对象:
1 2 3 4 5 6 public  class  DefaultFeignConfiguration       @Bean      public  Logger.Level feignLogLevel ()  {         return  Logger.Level.BASIC;      } } 
如果要全局生效 ,将其放到启动类的 @EnableFeignClients 这个注解中:
1 @EnableFeignClients(defaultConfiguration = DefaultFeignConfiguration .class)  
如果是局部生效 ,则把它放到对应的 @FeignClient 这个注解中:
1 @FeignClient(value = "userservice", configuration = DefaultFeignConfiguration .class)  
Feign性能优化 Feign底层发起http请求,依赖于其它的框架。其底层客户端实现包括:
•URLConnection:默认实现,不支持连接池
•Apache HttpClient :支持连接池
•OKHttp:支持连接池
因此提高Feign的性能主要手段就是使用连接池 代替默认的URLConnection。
这里我们用Apache的HttpClient来演示。
1)引入依赖
在order-service的pom文件中引入Apache的HttpClient依赖:
1 2 3 4 5 <dependency >     <groupId > io.github.openfeign</groupId >      <artifactId > feign-httpclient</artifactId >  </dependency > 
2)配置连接池
在order-service的application.yml中添加配置:
1 2 3 4 5 6 7 8 9 feign:   client:      config:        default:           loggerLevel:  BASIC     httpclient:      enabled:  true       max-connections:  200       max-connections-per-route:  50   
总结,Feign的优化:
1.日志级别尽量用basic
2.使用HttpClient或OKHttp代替URLConnection
① 引入feign-httpClient依赖
② 配置文件开启httpClient功能,设置连接池参数
Feign最佳实践 继承方式 一样的代码可以通过继承来共享:
1)定义一个API接口,利用定义方法,并基于SpringMVC注解做声明。
2)Feign客户端和Controller都集成改接口
优点:
缺点:
抽取方式 将Feign的Client抽取为独立模块,并且把接口有关的POJO、默认的Feign配置都放到这个模块中,提供给所有消费者使用。
例如,将UserClient、User、Feign的默认配置都抽取到一个feign-api包中,所有微服务引用该依赖包,即可直接使用。
基于抽取方式的实现 首先新建一个 feign-api Module,并且导入 feign 依赖
1 2 3 4 5 <dependency >     <groupId > org.springframework.cloud</groupId >      <artifactId > spring-cloud-starter-openfeign</artifactId >  </dependency > 
然后将之前的 Client 和 pojo 复制过来
然后在 orderserver 中引入 feign-api
然后把 orderserver 中有关 feign 的调用替换成我们定义的 feign-api 中的相关文件
注意要在 orderserver 启动类上的 @EnableFeignClients 注解添加我们要使用的那个 Client ,否则无法扫描到 Client 从而无法实现自动注入
这里的扫描包的方式有两种:
方式一:
指定Feign应该扫描的包:
1 @EnableFeignClients(basePackages = "cn.itcast.feign.clients") 
方式二:
指定需要加载的Client接口:
1 @EnableFeignClients(clients = {UserClient.class}) 
微服务网关 网关的作用 
对用户请求做身份验证,权限校验 
将用户的请求路由到微服务,并实现负载均衡 
对用户的请求做限流 
 
搭建GateWay网关 新建Module并引入依赖 1 2 3 4 5 6 7 8 9 10 <dependency >     <groupId > org.springframework.cloud</groupId >      <artifactId > spring-cloud-starter-gateway</artifactId >  </dependency >          <dependency > <groupId > com.alibaba.cloud</groupId > <artifactId > spring-cloud-starter-alibaba-nacos-discovery</artifactId > </dependency > 
新建启动类 1 2 3 4 5 6 @SpringBootApplication public  class  GateApplication      public  static  void  main (String[] args)           SpringApplication.run(GateApplication.class,args);     } } 
添加配置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 server:   port:  10010   spring:   application:      name:  gateway     cloud:      nacos:        server-addr:  localhost:8848       gateway:        routes:           -  id:  user-server                        uri:  lb://userserver             predicates:               -  Path=/user/**           -  id:  order-server            uri:  lb://orderserver            predicates:              -  Path=/order/**  
启动GateWay项目 分别访问
如图,我们已经实现了网关搭建
网关路由的流程图 
总结:
网关搭建步骤:
创建项目,引入nacos服务发现和gateway依赖
配置application.yml,包括服务基本信息、nacos地址、路由
 
路由配置包括:
路由id:路由的唯一标示
路由目标(uri):路由的目标地址,http代表固定地址,lb代表根据服务名负载均衡
路由断言(predicates):判断路由的规则,
路由过滤器(filters):对请求或响应做处理
 
断言工厂 我们在配置文件中写的断言规则只是字符串,这些字符串会被Predicate Factory读取并处理,转变为路由判断的条件
例如Path=/user/**是按照路径匹配,这个规则是由
org.springframework.cloud.gateway.handler.predicate.PathRoutePredicateFactory类来
处理的,像这样的断言工厂在SpringCloudGateway还有十几个:
名称 说明 示例  
 
After 
是某个时间点后的请求 
-  After=2037-01-20T17:42:47.789-07:00[America/Denver] 
 
Before 
是某个时间点之前的请求 
-  Before=2031-04-13T15:14:47.433+08:00[Asia/Shanghai] 
 
Between 
是某两个时间点之前的请求 
-  Between=2037-01-20T17:42:47.789-07:00[America/Denver],  2037-01-21T17:42:47.789-07:00[America/Denver] 
 
Cookie 
请求必须包含某些cookie 
- Cookie=chocolate, ch.p 
 
Header 
请求必须包含某些header 
- Header=X-Request-Id, \d+ 
 
Host 
请求必须是访问某个host(域名) 
-  Host=.somehost.org, .anotherhost.org 
 
Method 
请求方式必须是指定方式 
- Method=GET,POST 
 
Path 
请求路径必须符合指定规则 
- Path=/red/{segment},/blue/** 
 
Query 
请求参数必须包含指定参数 
- Query=name, Jack或者-  Query=name 
 
RemoteAddr 
请求者的ip必须是指定范围 
- RemoteAddr=192.168.1.1/24 
 
Weight 
权重处理 
 
我们只需要掌握Path这种路由工程就可以了。
路由过滤器配置 过滤器种类 Spring提供了31种不同的路由过滤器工厂。例如:
名称 说明  
 
AddRequestHeader 
给当前请求添加一个请求头 
 
RemoveRequestHeader 
移除请求中的一个请求头 
 
AddResponseHeader 
给响应结果中添加一个响应头 
 
RemoveResponseHeader 
从响应结果中移除有一个响应头 
 
RequestRateLimiter 
限制请求的流量 
 
针对某个服务添加过滤器 在 orderserver 服务中添加过滤器
1 2 3 4 5 6 7 8 9 10 spring:   cloud:      gateway:        routes:           -  id:  order-server            uri:  lb://orderserver            predicates:              -  Path=/order/**            filters:               -  AddRequestHeader=Truth,  hello  springcloud!   
然后再 orderserver 中获取这个参数
1 2 3 4 5 6 7 @GetMapping("{orderId}") public  Order queryOrderByUserId (@PathVariable("orderId")  Long orderId,                                 @RequestHeader(value = "Truth",required = false)  String Truth)      System.out.println("Truth:"  + Truth);          return  orderService.queryOrderById(orderId); } 
测试访问
添加默认过滤器 如果要对所有的路由都生效,则可以将过滤器工厂写到default下。格式如下:
1 2 3 4 5 6 7 8 9 10 spring:   cloud:      gateway:        routes:           -  id:  order-server            uri:  lb://orderserver            predicates:              -  Path=/order/**        default-filters:           -  AddRequestHeader=Truth,  hello  springcloud!  
全局过滤器 我们可以实现 GlobalFilter 接口来自定义过滤方法
添加 AuthorizeFilter 类,添加如下代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @Order(-1)  @Component public  class  AuthorizeFilter  implements  GlobalFilter      @Override      public  Mono<Void> filter (ServerWebExchange exchange, GatewayFilterChain chain)                    ServerHttpRequest request = exchange.getRequest();                  MultiValueMap<String, String> params = request.getQueryParams();         String author = params.getFirst("author" );                  if ("admin" .equals(author)){            return  chain.filter(exchange);         }                           exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);                  return  exchange.getResponse().setComplete();     } } 
这样通过网关访问微服务接口时必须携带 author=admin 的参数才能被放行
测试如下
过滤器执行顺序 请求进入网关会碰到三类过滤器:当前路由的过滤器、DefaultFilter、GlobalFilter
请求路由后,会将当前路由过滤器和DefaultFilter、GlobalFilter,合并到一个过滤器链(集合)中,排序后依次执行每个过滤器:
排序的规则是什么呢?
每一个过滤器都必须指定一个int类型的order值,order值越小,优先级越高,执行顺序越靠前 。 
GlobalFilter通过实现Ordered接口,或者添加@Order注解来指定order值,由我们自己指定 
路由过滤器和defaultFilter的order由Spring指定,默认是按照声明顺序从1递增。 
当过滤器的order值一样时,会按照 defaultFilter > 路由过滤器 > GlobalFilter的顺序执行。 
 
跨域问题处理 在 gateway 配置文件中添加如下配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 spring:   cloud:      gateway:        globalcors:           add-to-simple-url-handler-mapping:  true           corsConfigurations:            '[/**]' :              allowedOrigins:                 -  "http://localhost:5050"              allowedMethods:                 -  "GET"                -  "POST"                -  "DELETE"                -  "PUT"                -  "OPTIONS"              allowedHeaders:  "*"               allowCredentials:  true               maxAge:  360000   
请求示例:
在 5050 端口的地址上请求 10010 的接口,默认情况下会出现跨域问题,但是我们添加完配置后不会有跨域问题
Docker 什么是Docker Docker是一个快速交付应用,快速运行应用的技术:
可以将程序及其依赖、运行环境一起打包成一个镜像,可以迁移到任意的 Linux 操作系统 
运行时利用沙箱机制形成容器隔离,各个应用互不干扰 
启动、移除都可以通过一行命令完成,方便快捷 
 
Docker和虚拟机的区别 Docker可以让一个应用在任何操作系统中非常方便的运行。而以前我们接触的虚拟机,也能在一个操作系统中,运行另外一个操作系统,保护系统中的任何应用。
两者有什么差异呢?
虚拟机 (virtual machine)是在操作系统中模拟 硬件设备,然后运行另一个操作系统,比如在 Windows 系统里面运行 Ubuntu
Docker 仅仅是封装函数库,并没有模拟完整的操作系统,如图:
对比来看:
小结:
Docker和虚拟机的差异:
Docker架构 Docker中有几个重要的概念:
镜像(Image) :Docker将应用程序及其所需的依赖、函数库、环境、配置等文件打包在一起,称为镜像。
容器(Container) :镜像中的应用程序运行后形成的进程就是容器 ,只是Docker会给容器进程做隔离,对外不可见。
一切应用最终都是代码组成,都是硬盘中的一个个的字节形成的文件 。只有运行时,才会加载到内存,形成进程。
而镜像 ,就是把一个应用在硬盘上的文件、及其运行环境、部分系统函数库文件一起打包形成的文件包。这个文件包是只读的。
容器 呢,就是将这些文件中编写的程序、函数加载到内存中允许,形成进程,只不过要隔离起来。因此一个镜像可以启动多次,形成多个容器进程。
例如你下载了一个QQ,如果我们将QQ在磁盘上的运行文件 及其运行的操作系统依赖打包,形成QQ镜像。然后你可以启动多次,双开、甚至三开QQ,跟多个妹子聊天。
DockerHub 开源应用程序非常多,打包这些应用往往是重复的劳动。为了避免这些重复劳动,人们就会将自己打包的应用镜像,例如Redis、MySQL镜像放到网络上,共享使用,就像GitHub的代码共享一样。
我们一方面可以将自己的镜像共享到DockerHub,另一方面也可以从DockerHub拉取镜像:
Docker架构 我们要使用Docker来操作镜像、容器,就必须要安装Docker。
Docker是一个CS架构的程序,由两部分组成:
如图:
小结 镜像:
容器:
Docker结构:
服务端:接收命令或远程请求,操作镜像或容器
客户端:发送命令或者请求到Docker服务端
 
DockerHub:
一个镜像托管的服务器,类似的还有阿里云镜像服务,统称为DockerRegistry 
 
Linux关闭防火墙 查看防火墙状态 1 systemctl status firewalld.service 
关闭防火墙 1 systemctl stop firewalld.service 
永久关闭 1 systemctl disable  firewalld.service 
安装Docker Docker 分为 CE 和 EE 两大版本。CE 即社区版(免费,支持周期 7 个月),EE 即企业版,强调安全,付费使用,支持周期 24 个月。
Docker CE 分为 stable test 和 nightly 三个更新频道。
官方网站上有各种环境下的 安装指南 ,这里主要介绍 Docker CE 在 CentOS上的安装。
CentOS安装Docker Docker CE 支持 64 位版本 CentOS 7,并且要求内核版本不低于 3.10, CentOS 7 满足最低内核的要求,所以我们在CentOS 7安装Docker。
1.1.卸载(可选) 如果之前安装过旧版本的Docker,可以使用下面命令卸载:
1 2 3 4 5 6 7 8 9 10 11 yum remove docker \                   docker-client \                   docker-client-latest \                   docker-common \                   docker-latest \                   docker-latest-logrotate \                   docker-logrotate \                   docker-selinux \                   docker-engine-selinux \                   docker-engine \                   docker-ce 
1.2.安装docker 首先需要大家虚拟机联网,安装yum工具
1 2 3 yum install -y yum-utils \            device-mapper-persistent-data \            lvm2 --skip-broken 
然后更新本地镜像源:
1 2 3 4 5 6 7 8 #  设置docker镜像源 yum-config-manager \     --add-repo \     https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo      sed -i 's/download.docker.com/mirrors.aliyun.com\/docker-ce/g' /etc/yum.repos.d/docker-ce.repo yum makecache fast 
然后输入命令:
1 yum install -y docker-ce 
docker-ce为社区免费版本。稍等片刻,docker即可安装成功。
1.3.启动docker Docker应用需要用到各种端口,逐一去修改防火墙设置。非常麻烦,因此建议大家直接关闭防火墙!
启动docker前,一定要关闭防火墙后!!
启动docker前,一定要关闭防火墙后!!
启动docker前,一定要关闭防火墙后!!
1 2 3 4 systemctl stop firewalld systemctl disable  firewalld 
通过命令启动docker:
1 2 3 4 5 systemctl start docker   systemctl stop docker   systemctl restart docker   
然后输入命令,可以查看docker版本:
如图:
1.4.重启docker 1 systemctl restart docker 
1.5.配置镜像加速 docker官方镜像仓库网速较差,我们需要设置国内镜像服务:
参考阿里云的镜像加速文档:https://cr.console.aliyun.com/cn-hangzhou/instances/mirrors 
Docker基本操作 镜像名称 首先来看下镜像的名称组成:
镜名称一般分两部分组成:[repository]:[tag]。 
在没有指定tag时,默认是latest,代表最新版本的镜像 
 
如图:
这里的mysql就是repository,5.7就是tag,合一起就是镜像名称,代表5.7版本的MySQL镜像。
镜像命令 常见的镜像操作命令如图:
拉取镜像 我们可以访问官方网站 ,来查询要拉取的镜像
1 docker pull nginx:laster 
查看已安装镜像 
删除镜像 将镜像保存为tar文件 1 docker save -o nginx.tar nginx:latest 
使用load加载镜像 1 docker load -i nginx.tar 
容器的基本操作 基本操作图 容器操作的命令如图:
容器保护三个状态:
运行:进程正常运行 
暂停:进程暂停,CPU不再运行,并不释放内存 
停止:进程终止,回收进程占用的内存、CPU等资源 
 
其中:
docker run:创建并运行一个容器,处于运行状态
docker pause:让一个运行的容器暂停
docker unpause:让一个容器从暂停状态恢复运行
docker stop:停止一个运行的容器
docker start:让一个停止的容器再次运行
docker rm:删除一个容器
 
容器基本操作1 
我们以启动 nginx 容器为例
启动nginx容器 1 docker run --name nginxcontent -p 7070:80 -d nginx 
查看运行中的容器 
测试访问 访问:http://182.43.250.34:7070 
查看容器日志 1 docker logs nginxcontent 
持续监听查看日志 1 docker logs -f nginxcontent 
容器基本操作2 查看正在运行的容器 
查看所有容器 -a 参数可以查看所有容器,包括没有运行的容器
停止正在运行的容器 
重启已经停止的容器 
删除容器 
上面这种删除默认只能删除没有运行的容器,而不能删除正在运行的容器
强制删除正在运行的容器 
进入容器 1 docker exec  -it 容器名称 bash 
容器操作案例以Redis为例 启动一个Redis容器
进入容器内部添加一个 num=666
Docker数据卷 数据卷的作用 将容器和数据分离,解耦合,方便操作容器内数据,保证数据安全。通过数据卷和容器内的文件做相互关联,我们可以直接操作宿主机上的文件,从而达到实现修改容器内数据的目的。
数据卷的基本操作 创建一个数据卷 1 docker volume create 数据卷名称 
下图是创建一个名为 html 的数据卷
查看所有数据卷 
查看指定数据卷的详细信息 1 docker volume inspect 数据卷名称 
查询html数据卷的详细信息
删除所有未使用的数据卷 1 docker volume prune 数据卷名称 
删除指定数据卷 数据卷挂载操作 使用 -v 数据卷名称:/容器内目标路径,其中容器内目标路径可以在 DockerHub 上查到
下面是启动 nginx 容器并挂载 html 数据卷的命令
1 docker run --name mn -p 7070:80 -v html:/usr/share/nginx/html -d nginx 
然后查看 html 数据卷对应宿主机的真实文件地址
1 docker volume inspect html 
然后 cd 到查询到的位置,并且列出该文件夹下的文件
可以发现容器内的文件在我们主机上也对应的存在一份
现在访问 7070 端口来查看页面
然后尝试修改主机上的 html 文件
然后直接刷新页面
我们可以发现页面也跟着改变,说明通过数据卷来修改了容器内的文件
在运行容器时,如果数据卷不存在,docker 会自动创建数据卷,不需要刻意提前创建数据卷
 
数据卷挂载案例以MySQL为例 首先将准备好的 mysql.tar 上传到 /tmp/mysql,并且新建 config 和 data 两个文件夹
接着将准备好的 hmy.cnf 文件上传到 config 文件中
然后使用 docker 加载 tar 镜像
1 docker load -i mysql.tar 
加载完镜像后可以启动 mysql 容器了,输入下面的命令
1 2 3 4 5 6 7 docker run --name mysql8 \ 	-e MYSQL_ROOT_PASSWORD=fawu123.. \ 	-p 3306:3306 \ 	-v /tmp/mysql/conf/hmy.cnf:/etc/mysql/conf.d/hmy.cnf/hmy.cnf \ 	-v /tmp/mysql/data:/var/lib/mysql \ 	-d \ 	mysql:8.0.24 
1 2 3 4 5 6 7 docker run --name mysql \ 	-e MYSQL_ROOT_PASSWORD=abc123 \ 	-p 3307:3306 \ 	-v /tmp/mysql/config/hmy.cnf:/etc/mysql/conf.d/hmy.cnf/hmy.cnf \ 	-v /tmp/mysql/data:/var/lib/mysql \ 	-d \ 	mysql:5.7.25 
上面的命令解释
踩坑点:换行执行命令时,镜像名称要单独在一行,不能放在 -d 后面运行,否则启动会出错
 
测试链接
自定义镜像Dockerfile 
更新详细语法说明,请参考官网文档: https://docs.docker.com/engine/reference/builder 
基于Ubuntu构建Java项目 首先将事先准备好的资料复制到 docker-demo 文件夹中
其中 Dockerfile 文件内容如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 FROM  ubuntu:16.04 ENV  JAVA_DIR=/usr/localCOPY  ./jdk8.tar.gz $JAVA_DIR / COPY  ./docker-demo.jar /tmp/app.jar RUN  cd  $JAVA_DIR  \  && tar -xf ./jdk8.tar.gz \  && mv ./jdk1.8.0_144 ./java8 ENV  JAVA_HOME=$JAVA_DIR/java8ENV  PATH=$PATH:$JAVA_HOME/binEXPOSE  8090 ENTRYPOINT  java -jar /tmp/app.jar 
然后执行构建镜像的命令
1 docker build -t javaweb:1.0 . 
javaweb:1.0 表示要构建的镜像名称和tag
注意命令后面跟上空格 .,小数点的意思是表示当前目录
 
正在构建中
构建完成,查看当前的所有镜像
启动镜像
1 docker run --name web  -p 8090:8090 -d javaweb:1.0 
测试访问
基于java8构建Java项目 在上面的例子中,我们每次构建都要从新的环境去安装 java1.8 jdk环境,如果有多个服务需要构建,那么每次都要重复的执行一遍,所以我们可以基于别人弄好的一个java8项目来构建镜像
修改 Dockerfile 文件成如下
1 2 3 4 5 6 7 8 FROM  java:8 -alpineCOPY  ./docker-demo.jar /tmp/app.jar EXPOSE  8090 ENTRYPOINT  java -jar /tmp/app.jar 
然后再次执行构建命令
1 docker build -t javaweb:1.2 . 
查看构建好的镜像
启动镜像
1 docker run --name web2  -p 8091:8090 -d javaweb:1.2 
测试访问
CentOS7安装DockerCompose 2.1.下载 Linux下需要通过命令下载:
1 2 curl -L https://github.com/docker/compose/releases/download/1.23.1/docker-compose-`uname -s`-`uname -m` > /usr/local /bin/docker-compose 
如果下载速度较慢,或者下载失败,可以使用课前资料提供的docker-compose文件:
上传到/usr/local/bin/目录也可以。
修改文件权限 修改文件权限:
1 2 chmod +x /usr/local /bin/docker-compose 
2.3.Base自动补全命令: 1 2 curl -L https://raw.githubusercontent.com/docker/compose/1.29.1/contrib/completion/bash/docker-compose > /etc/bash_completion.d/docker-compose 
如果这里出现错误,需要修改自己的hosts文件:
1 echo  "199.232.68.133 raw.githubusercontent.com"  >> /etc/hosts
这里如果修改了hosts文件还是不行,重启一下 docker 再次运行即可
使用CockerCompose部署集群 服务打包 首先将各个微服务打包
每个微服务文件夹里面都只有一个 jar 包和 Dockerfile 文件
其中 Dockerfile 文件内容如下
1 2 3 FROM  java:8 -alpineCOPY  ./app.jar /tmp/app.jar ENTRYPOINT  java -jar /tmp/app.jar 
然后 docker-compose.yml 内容如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 version:  "3.2" services:   nacos:      image:  nacos/nacos-server      environment:        MODE:  standalone      ports:        -  "8848:8848"    mysql:      image:  mysql:5.7.25      environment:        MYSQL_ROOT_PASSWORD:  123      volumes:        -  "$PWD/mysql/data:/var/lib/mysql"        -  "$PWD/mysql/conf:/etc/mysql/conf.d/"    userservice:      build:  ./user-service    orderservice:      build:  ./order-service    gateway:      build:  ./gateway      ports:        -  "10010:10010"  
运行所有容器 然后执行运行命令
运行成功截图
查看正在运行的容器,可以发现所有的微服务都在运行中
查看日志 重启 docker-compose 1 docker-compose restart gateway userservice orderservice 
停止启动的所有容器 down 命令会停止所有容器,并删除相关的网络和卷。在容器停止时,docker-compose 会自动移除容器名称、网络和卷等相关资源
stop 命令会停止所有容器,但是容器名称、网络和卷等相关资源不会被删除。如果你想启动容器,可以使用 docker-compose start 命令。
Docker私有镜像仓库 搭建镜像仓库可以基于Docker官方提供的DockerRegistry来实现。
官网地址:https://hub.docker.com/_/registry 
3.1.简化版镜像仓库 Docker官方的Docker Registry是一个基础版本的Docker镜像仓库,具备仓库管理的完整功能,但是没有图形化界面。
搭建方式比较简单,命令如下:
1 2 3 4 5 6 docker run -d \     --restart=always \     --name registry	\     -p 5000:5000 \     -v registry-data:/var/lib/registry \     registry 
命令中挂载了一个数据卷registry-data到容器内的/var/lib/registry 目录,这是私有镜像库存放数据的目录。
访问http://YourIp:5000/v2/_catalog  可以查看当前私有镜像服务中包含的镜像
3.2.带有图形化界面版本 使用DockerCompose部署带有图象界面的DockerRegistry,命令如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 version:  '3.0' services:   registry:      image:  registry      volumes:        -  ./registry-data:/var/lib/registry    ui:      image:  joxit/docker-registry-ui:static      ports:        -  8080 :80      environment:        -  REGISTRY_TITLE=传智教育私有仓库        -  REGISTRY_URL=http://registry:5000      depends_on:        -  registry  
3.3.配置Docker信任地址 我们的私服采用的是http协议,默认不被Docker信任,所以需要做一个配置:
1 2 3 4 5 6 7 8 vi /etc/docker/daemon.json "insecure-registries" :["http://182.43.250.34:7071" ]systemctl daemon-reload systemctl restart docker 
启动服务 
访问 182.43.250.34:7071 来查看UI界面
上传镜像到私有仓库 首先要对已有镜像进行重新 tag,以 nginx 为例
1 docker tag nginx:latest 182.43.250.34:7071/nginx:1.0 
然后执行推送命令
1 docker push 182.43.250.34:7071/nginx:1.0 
刷新 http://182.43.250.34:7071  查看
从私有镜像拉取 1 docker pull 182.43.250.34:7071/nginx:1.0 
异步通信MQ 同步通信的优缺点 优点
缺点
耦合度高 
性能和吞吐能力下降 
有额外的资源消耗 
有级联失败问题 
 
异步通信的优缺点 优点
缺点
依赖于Broker的可靠性、安全性、吞吐能力 
架构复杂了,业务没有明显的流程线,不好追踪管理 
 
各个MQ框架的区别 
安装RabbitMq 使用 load 的方式加载课前准备好的 mq.tar 镜像包
查看加载好的镜像
运行mq,其中设置账号 szx,密码为 abc123
1 2 3 4 5 6 7 8 9 docker run \  -e RABBITMQ_DEFAULT_USER=szx \  -e RABBITMQ_DEFAULT_PASS=abc123 \  --name mq \  --hostname mq1 \  -p 15672:15672 \  -p 5672:5672 \  -d \  rabbitmq:3-management 
运行成功后访问可以查看 MQ 的 UI 界面
http://182.43.250.34:15672 
RabbitMq快速入门 Hello RabbitMQ 添加消息发送者
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 package  cn.itcast.mq.helloworld;import  com.rabbitmq.client.Channel;import  com.rabbitmq.client.Connection;import  com.rabbitmq.client.ConnectionFactory;import  org.junit.Test;import  java.io.IOException;import  java.util.concurrent.TimeoutException;public  class  PublisherTest      @Test      public  void  testSendMessage ()  throws  IOException, TimeoutException                   ConnectionFactory factory = new  ConnectionFactory();                  factory.setHost("182.43.250.34" );         factory.setPort(5672 );         factory.setVirtualHost("/" );         factory.setUsername("szx" );         factory.setPassword("abc123" );                  Connection connection = factory.newConnection();                  Channel channel = connection.createChannel();                  String queueName = "simple.queue" ;         channel.queueDeclare(queueName, false , false , false , null );                  String message = "hello, rabbitmq!" ;         channel.basicPublish("" , queueName, null , message.getBytes());         System.out.println("发送消息成功:【"  + message + "】" );                  channel.close();         connection.close();     } } 
添加消息消费者
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 package  cn.itcast.mq.helloworld;import  com.rabbitmq.client.*;import  java.io.IOException;import  java.util.concurrent.TimeoutException;public  class  ConsumerTest      public  static  void  main (String[] args)  throws  IOException, TimeoutException                   ConnectionFactory factory = new  ConnectionFactory();                  factory.setHost("182.43.250.34" );         factory.setPort(5672 );         factory.setVirtualHost("/" );         factory.setUsername("szx" );         factory.setPassword("abc123" );                  Connection connection = factory.newConnection();                  Channel channel = connection.createChannel();                  String queueName = "simple.queue" ;         channel.queueDeclare(queueName, false , false , false , null );                  channel.basicConsume(queueName, true , new  DefaultConsumer(channel){             @Override              public  void  handleDelivery (String consumerTag, Envelope envelope,                                         AMQP.BasicProperties properties, byte [] body)  throws  IOException                                  String message = new  String(body);                 System.out.println("接收到消息:【"  + message + "】" );             }         });         System.out.println("等待接收消息。。。。" );     } } 
运行两次发送者,然后再运行消息消费者,打印如下
SpringAMQP-简单队列模型 引入依赖 1 2 3 4 5 <dependency >     <groupId > org.springframework.boot</groupId >      <artifactId > spring-boot-starter-amqp</artifactId >  </dependency > 
定义消息发送者 首先添加配置
1 2 3 4 5 6 7 spring:   rabbitmq:      host:  182.43 .250 .34       port:  5672       virtual-host:  /       username:  szx       password:  abc123   
编写代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @RunWith(SpringRunner.class) @SpringBootTest public  class  SpringAmqpTest      @Autowired      RabbitTemplate rabbitTemplate;     @Test      public  void  test ()                   String queueName = "simple.queue" ;                  String message = "hello, spring amqp!" ;                  rabbitTemplate.convertAndSend(queueName, message);     } } 
此时消息队列中有一条消息待消费
定义消息消费者 同样先添加配置文件
1 2 3 4 5 6 7 spring:   rabbitmq:      host:  182.43 .250 .34       port:  5672       virtual-host:  /       username:  szx       password:  abc123   
然后需要添加一个 Bean 交由 Spring 管理,当监听到消息后会自动触发这个方法
1 2 3 4 5 6 7 @Component public  class  SpringRabbitListener      @RabbitListener(queues = "simple.queue")      public  void  listenSimpleQueueMessage (String msg)  throws  InterruptedException          System.out.println("spring 消费者接收到消息:【"  + msg + "】" );     } } 
然后需要启动 Spring 运行函数,查看打印
此时消息队列中的消息就没有了
WorkQueue Work queues,也被称为(Task queues),任务模型。简单来说就是让多个消费者绑定到一个队列,共同消费队列中的消息 。
当消息处理比较耗时的时候,可能生产消息的速度会远远大于消息的消费速度。长此以往,消息就会堆积越来越多,无法及时处理。
此时就可以使用work 模型,多个消费者共同处理消息处理,速度就能大大提高了。
消息发送 修改代码,设置一秒钟发送50个消息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @RunWith(SpringRunner.class) @SpringBootTest public  class  SpringAmqpTest2      @Autowired      RabbitTemplate rabbitTemplate;     @Test      public  void  test ()  throws  InterruptedException                   String queueName = "simple.queue" ;                  String message = "hello, spring message_____" ;         for  (int  i = 1 ; i <= 50 ; i++) {                          rabbitTemplate.convertAndSend(queueName, message + i);                          Thread.sleep(20 );         }     } } 
消息接收 可以定义多个消费者来消费,消费的是同一个队列中的消息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @RabbitListener(queues = "simple.queue") public  void  listenWorkQueueMessage1 (String msg)  throws  InterruptedException     System.out.println("spring 消费者1接收到消息:【"  + msg + "】,"  + LocalDateTime.now());     Thread.sleep(20 ); } @RabbitListener(queues = "simple.queue") public  void  listenWorkQueueMessage2 (String msg)  throws  InterruptedException     System.err.println("spring 消费者2接收到消息____:【"  + msg + "】,"  + LocalDateTime.now());     Thread.sleep(200 ); } @RabbitListener(queues = "simple.queue") public  void  listenWorkQueueMessage3 (String msg)  throws  InterruptedException     System.out.println("spring 消费者3接收到消息____:【"  + msg + "】,"  + LocalDateTime.now());     Thread.sleep(20 ); } 
修改配置文件,每次只能预取一个消息,这样放置性能不好的消费者也去获取大量消息来处理
1 2 3 4 5 spring:   rabbitmq:      listener:        simple:          prefetch:  1   
运行效果
可以看到,消费者2执行的消费比较少,其他两个执行的比较多
发布/订阅 
可以看到,在订阅模型中,多了一个exchange角色,而且过程略有变化:
Publisher:生产者,也就是要发送消息的程序,但是不再发送到队列中,而是发给X(交换机) 
Exchange:交换机,图中的X。一方面,接收生产者发送的消息。另一方面,知道如何处理消息,例如递交给某个特别队列、递交给所有队列、或是将消息丢弃。到底如何操作,取决于Exchange的类型。Exchange有以下3种类型:
Fanout:广播,将消息交给所有绑定到交换机的队列 
Direct:定向,把消息交给符合指定routing key 的队列 
Topic:通配符,把消息交给符合routing pattern(路由模式) 的队列 
 
 
Consumer:消费者,与以前一样,订阅队列,没有变化 
Queue:消息队列也与以前一样,接收消息、缓存消息。 
 
Exchange(交换机)只负责转发消息,不具备存储消息的能力 ,因此如果没有任何队列与Exchange绑定,或者没有符合路由规则的队列,那么消息会丢失!
Fanout Fanout 也可以理解为广播,消息发送给交换机后,交换机会吧消息发送给每一个与之绑定的队列,所以这两个队列都能获取到消息
完成如下示例
声明一个交换机,名称叫做:itcast.fanout 
声明两个队列,分别是:fanout.queue1, fanout.queue2 
 
添加配置类 在 consumer 中添加交换机
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 package  cn.itcast.mq.config;import  org.springframework.amqp.core.Binding;import  org.springframework.amqp.core.BindingBuilder;import  org.springframework.amqp.core.FanoutExchange;import  org.springframework.amqp.core.Queue;import  org.springframework.context.annotation.Bean;import  org.springframework.context.annotation.Configuration;@Configuration public  class  FanoutConfig           @Bean      public  FanoutExchange fanoutExchange ()          return  new  FanoutExchange("itcast.fanout" );     }          @Bean      public  Queue queue1 ()          return  new  Queue("fanout.queue1" );     }          @Bean      public  Binding bindingQueue (Queue queue1,FanoutExchange fanoutExchange)          return  BindingBuilder.bind(queue1).to(fanoutExchange);     }          @Bean      public  Queue queue2 ()          return  new  Queue("fanout.queue2" );     }          @Bean      public  Binding bindingQueue2 (Queue queue2,FanoutExchange fanoutExchange)          return  BindingBuilder.bind(queue2).to(fanoutExchange);     } } 
消息发送 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 package  cn.itcast.mq.helloworld;import  org.junit.Test;import  org.junit.runner.RunWith;import  org.springframework.amqp.rabbit.core.RabbitTemplate;import  org.springframework.beans.factory.annotation.Autowired;import  org.springframework.boot.test.context.SpringBootTest;import  org.springframework.test.context.junit4.SpringRunner;@RunWith(SpringRunner.class) @SpringBootTest public  class  SpringAmqpTest      @Autowired      RabbitTemplate rabbitTemplate;        @Test      public  void  testSendFanout ()                   String exChangeName = "itcast.fanout" ;                  String message = "hello every one" ;                  rabbitTemplate.convertAndSend(exChangeName,"" ,message);     } } 
消息接收 1 2 3 4 5 6 7 8 9 @RabbitListener(queues = "fanout.queue1") public  void  listenFanoutQueue1 (String msg)      System.out.println("消费者1接收到Fanout消息:【"  + msg + "】" ); } @RabbitListener(queues = "fanout.queue2") public  void  listenFanoutQueue2 (String msg)      System.out.println("消费者2接收到Fanout消息:【"  + msg + "】" ); } 
Direct 
队列与交换机的绑定,不能是任意绑定了,而是要指定一个RoutingKey(路由key) 
消息的发送方在 向 Exchange发送消息时,也必须指定消息的 RoutingKey。 
Exchange不再把消息交给每一个绑定的队列,而是根据消息的Routing Key进行判断,只有队列的RoutingkeyRouting key完全一致,才会接收到消息 
 
下面通过注解的方式来实现上面的图
消息接收 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @RabbitListener(bindings = @QueueBinding(         value = @Queue("directQueue1"),         exchange = @Exchange("itcast.exchange"),         key = {"blue","red"} )) public  void  directQueue1 (String msg)     System.out.println("directQueue1收到消息:"  + msg); } @RabbitListener(bindings = @QueueBinding(         value = @Queue("directQueue2"),         exchange = @Exchange("itcast.exchange"),         key = {"yellow","red"} )) public  void  directQueue2 (String msg)     System.out.println("directQueue2收到消息:"  + msg); } 
消息发送 1 2 3 4 5 6 7 8 9 @Test public  void  testDirectExChange ()          String exChangeName = "itcast.exchange" ;          String message = "hello blue" ;          rabbitTemplate.convertAndSend(exChangeName,"blue" ,message); } 
Topic Topic类型的Exchange与Direct相比,都是可以根据RoutingKey把消息路由到不同的队列。只不过Topic类型ExchangeRouting key 的时候使用通配符!
Routingkey 一般都是有一个或多个单词组成,多个单词之间以”.”分割,例如: item.insert
通配符规则:
#:匹配一个或多个词
*:匹配不多不少恰好1个词
举例:
item.#:能够匹配item.spu.insert 或者 item.spu
item.*:只能匹配item.spu
消息接收 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @RabbitListener(bindings = @QueueBinding(         value = @Queue("topicQueue1"),         exchange = @Exchange(value = "topic",type = ExchangeTypes.TOPIC),         key = "china.#" )) public  void  topicQueue1 (String msg)     System.out.println("topicQueue1收到消息:"  + msg); } @RabbitListener(bindings = @QueueBinding(         value = @Queue("topicQueue2"),         exchange = @Exchange(value = "topic",type = ExchangeTypes.TOPIC),         key = "#.weather" )) public  void  topicQueue2 (String msg)     System.out.println("topicQueue2收到消息:"  + msg); } 
消息发送 1 2 3 4 5 6 7 8 9 @Test public  void  testTopicExChange ()          String exChangeName = "topic" ;          String message = "测试新闻发送" ;          rabbitTemplate.convertAndSend(exChangeName,"china.news" ,message); } 
消息转换器 默认情况下,我们只能发送字节消息。无法发送其他类型的消息,所以需要安装消息转换器,来将消息转成json的格式来发送
安装消息转换器的依赖
添加依赖 1 2 3 4 5 <dependency >     <groupId > com.fasterxml.jackson.core</groupId >      <artifactId > jackson-databind</artifactId >  </dependency > 
配置消息bean 在启动类中添加 Bean,这个 Bean 需要在发送消息和接收消息的工程中都配置
1 2 3 4 @Bean public  MessageConverter messageConverter ()     return  new  Jackson2JsonMessageConverter(); } 
发送消息 1 2 3 4 5 6 7 @Test public  void  testMessageConverter ()     Map<String,Object> msg = new  HashMap<>();     msg.put("name" ,"里斯" );     msg.put("age" ,15 );     rabbitTemplate.convertAndSend("fanout.queue1" ,msg); } 
队列必须提前声明好
 
接收消息 1 2 3 4 @RabbitListener(queues = "fanout.queue1") public  void  objectQueue (Map<String,Object> msp)     System.out.println("接收到消息:"  + msp); } 
分布式搜索引擎 什么是elasticsearch? 
一个开源的分布式搜索引擎,可以用来实现搜索、日志统计、分析、系统监控等功能 
 
什么是elastic stack(ELK)?
是以elasticsearch为核心的技术栈,包括beats、Logstash、kibana、elasticsearch 
 
什么是Lucene?
是Apache的开源搜索引擎类库,提供了搜索引擎的核心API 
 
安装es、kibana 安装 参考课前资料:
分词器 参考课前资料:
总结 分词器的作用是什么?
创建倒排索引时对文档分词 
用户搜索时,对输入的内容分词 
 
IK分词器有几种模式?
ik_smart:智能切分,粗粒度 
ik_max_word:最细切分,细粒度 
 
IK分词器如何拓展词条?如何停用词条?
利用config目录的IkAnalyzer.cfg.xml文件添加拓展词典和停用词典 
在词典中添加拓展词条或者停用词条 
 
索引库操作 索引库就类似数据库表,mapping映射就类似表的结构。
我们要向es中存储数据,必须先创建“库”和“表”。
mapping映射属性 mapping是对索引库中文档的约束,常见的mapping属性包括:
type:字段数据类型,常见的简单类型有:
字符串:text(可分词的文本)、keyword(精确值,例如:品牌、国家、ip地址) 
数值:long、integer、short、byte、double、float、 
布尔:boolean 
日期:date 
对象:object 
 
 
index:是否创建索引,默认为true 
analyzer:使用哪种分词器 
properties:该字段的子字段 
 
例如下面的json文档:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 {   "age" : 21 ,   "weight" : 52.1 ,   "isMarried" : false ,   "info" : "黑马程序员Java讲师" ,   "email" : "zy@itcast.cn" ,   "score" : [     99.1 ,     99.5 ,     98.9    ],   "name" : {     "firstName" : "云" ,     "lastName" : "赵"    } } 
对应的每个字段映射(mapping):
age:类型为 integer;参与搜索,因此需要index为true;无需分词器 
weight:类型为float;参与搜索,因此需要index为true;无需分词器 
isMarried:类型为boolean;参与搜索,因此需要index为true;无需分词器 
info:类型为字符串,需要分词,因此是text;参与搜索,因此需要index为true;分词器可以用ik_smart 
email:类型为字符串,但是不需要分词,因此是keyword;不参与搜索,因此需要index为false;无需分词器 
score:虽然是数组,但是我们只看元素的类型,类型为float;参与搜索,因此需要index为true;无需分词器 
name:类型为object,需要定义多个子属性
name.firstName;类型为字符串,但是不需要分词,因此是keyword;参与搜索,因此需要index为true;无需分词器 
name.lastName;类型为字符串,但是不需要分词,因此是keyword;参与搜索,因此需要index为true;无需分词器 
 
 
 
创建索引库 基本语法 
请求方式:PUT 
请求路径:/索引库名,可以自定义 
请求参数:mapping映射 
 
示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 PUT/索引库名称 {   "mappings" : {     "properties" : {       "字段名" : {         "type" : "text" ,         "analyzer" : "ik_smart"        },       "字段名2" : {         "type" : "keyword" ,         "index" : "false"        },       "字段名3" : {         "properties" : {           "子字段" : {             "type" : "keyword"            }         }       }            }   } } 
下面创建一个名为 heima 的索引库
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 PUT /heima {   "mappings" : {     "properties" : {       "info" : {         "type" : "text" ,         "analyzer" : "ik_smart"        },       "email" : {         "type" : "keyword" ,         "index" : false        },       "name" : {         "properties" : {           "firstName" : {             "type" : "keyword"            },           "lastName" : {             "type" : "keyword"            }         }       }     }   } } 
运行出现如下图,表示创建索引成功
查询、删除、修改索引库 查询索引库
修改索引库,修改索引库时只能添加新的字段,不能修改已有的字段
1 2 3 4 5 6 7 8 PUT /heima/_mappings {   "properties" : {     "age" : {       "type" : "integer"      }   } } 
删除索引库
文档操作 添加、删除、查询 添加文档语法
1 POST /索引名称/_doc/要添加数据的id 
示例代码
1 2 3 4 5 6 7 8 9 10 11 # 添加文档 POST /heima/_doc/1 { "info" : "黑马程序员Java讲师" ,"age" :18 ,"email" : "zy@163.com" ,"name" : {"firstName" : "赵" ,"lastName" : "云" } } 
查询
1 2 # 查询文档 GET /heima/_doc/1 
删除
1 2 # 删除文档 DELETE /heima/_doc/1 
修改文档 全量修改,如果id不存在,则会新增一条
1 2 3 4 5 6 7 8 9 10 11 # 全量更新 PUT /heima/_doc/1 { "info" : "黑马程序员Java讲师" ,"age" :18 ,"email" : "ZhaoYun@163.com" ,"name" : {"firstName" : "赵" ,"lastName" : "云" } } 
局部修改,只修改一个字段
1 2 3 4 5 6 7 # 局部修改,只修改某个字段 POST /heima/_update/1 { "doc" : {"age" : 20 } } 
RestClient 导入 1 2 3 4 5 <properties >     <java.version > 1.8</java.version >      <elasticsearch.version > 7.12.1</elasticsearch.version >  </properties > 
1 2 3 4 5 <dependency >     <groupId > org.elasticsearch.client</groupId >      <artifactId > elasticsearch-rest-high-level-client</artifactId >  </dependency > 
默认安装的是 7.6.1 版本,要使用固定 7.12.1 版本
初始化Client 编写测试方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @SpringBootTest public  class  HotelIndexTest      private  RestHighLevelClient client;     @Test      public  void  testInitClient ()          System.out.println(client);     }     @BeforeEach      void  setClient ()          this .client = new  RestHighLevelClient(RestClient.builder(                 HttpHost.create("http://8.130.118.240:9200" )         ));     }     @AfterEach      void  removeClient ()  throws  IOException          this .client.close();     } } 
索引操作 首先我们要根据 SQL 表结构,自行设置 mapping 映射的 JSON,然后通过 RestClient 来在 Java 代码中添加索引
创建索引库 1 2 3 4 5 6 7 8 9 10 @Test void  createHotelIndex ()  throws  IOException          CreateIndexRequest request = new  CreateIndexRequest("hotel" );          request.source(MAPPING_TEMPLATE, XContentType.JSON);          client.indices().create(request, RequestOptions.DEFAULT); } 
其中 MAPPING_TEMPLATE 是我们提前准备好的 mapping 映射的 JSON 文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 package  cn.itcast.hotel.constants;public  class  HotelConstants      public  static  final  String MAPPING_TEMPLATE = "{\n"  +             "  \"mappings\": {\n"  +             "    \"properties\": {\n"  +             "      \"id\": {\n"  +             "        \"type\": \"keyword\"\n"  +             "      },\n"  +             "      \"name\":{\n"  +             "        \"type\": \"text\",\n"  +             "        \"analyzer\": \"ik_max_word\",\n"  +             "        \"copy_to\": \"all\"\n"  +             "      },\n"  +             "      \"address\":{\n"  +             "        \"type\": \"keyword\",\n"  +             "        \"index\": false\n"  +             "      },\n"  +             "      \"price\":{\n"  +             "        \"type\": \"integer\"\n"  +             "      },\n"  +             "      \"score\":{\n"  +             "        \"type\": \"integer\"\n"  +             "      },\n"  +             "      \"brand\":{\n"  +             "        \"type\": \"keyword\",\n"  +             "        \"copy_to\": \"all\"\n"  +             "      },\n"  +             "      \"city\":{\n"  +             "        \"type\": \"keyword\",\n"  +             "        \"copy_to\": \"all\"\n"  +             "      },\n"  +             "      \"starName\":{\n"  +             "        \"type\": \"keyword\"\n"  +             "      },\n"  +             "      \"business\":{\n"  +             "        \"type\": \"keyword\"\n"  +             "      },\n"  +             "      \"location\":{\n"  +             "        \"type\": \"geo_point\"\n"  +             "      },\n"  +             "      \"pic\":{\n"  +             "        \"type\": \"keyword\",\n"  +             "        \"index\": false\n"  +             "      },\n"  +             "      \"all\":{\n"  +             "        \"type\": \"text\",\n"  +             "        \"analyzer\": \"ik_max_word\"\n"  +             "      }\n"  +             "    }\n"  +             "  }\n"  +             "}" ; } 
执行完成后,我们在网页中查看该索引
判断索引是否存在 1 2 3 4 5 6 7 8 9 10 @Test void  existsHotelIndex ()  throws  IOException          GetIndexRequest request = new  GetIndexRequest("hotel" );          boolean  exists = client.indices().exists(request, RequestOptions.DEFAULT);          System.out.println(exists); } 
运行结果
删除索引 1 2 3 4 5 6 7 8 @Test void  deleteHotelIndex ()  throws  IOException          DeleteIndexRequest request = new  DeleteIndexRequest("hotel" );          client.indices().delete(request, RequestOptions.DEFAULT); } 
文档操作 添加文档 索引库对应的实体类如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Data @TableName("tb_hotel") public  class  Hotel      @TableId(type = IdType.INPUT)      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 longitude;     private  String latitude;     private  String pic; } 
但是在经纬度处理方面可映射文件的 geo_point 类型不一致,所以要转换实体类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 @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;     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();     } } 
通过id查询出一条数据,转换成文档实体类
1 2 3 4 5 6 7 8 9 10 11 12 @Test void  testAddDocument ()  throws  IOException     Hotel hotel = hotelService.getById("56227L" );     HotelDoc hotelDoc = new  HotelDoc(hotel);          IndexRequest request = new  IndexRequest("hotel" ).id(hotelDoc.getId().toString());          request.source(JSON.toJSONString(hotelDoc),XContentType.JSON);          client.index(request,RequestOptions.DEFAULT); } 
执行完成后查看是否添加成功
根据id查询文档 1 2 3 4 5 6 7 8 9 10 11 12 13 @Test void  testGetDocument ()  throws  IOException          GetRequest request = new  GetRequest("hotel" , "56227" );          GetResponse response = client.get(request, RequestOptions.DEFAULT);          String json = response.getSourceAsString();          HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);     System.out.println(hotelDoc); } 
返回结果
根据id删除文档 1 2 3 4 5 6 7 8 @Test void  testDeleteDocument ()  throws  IOException          DeleteRequest request = new  DeleteRequest("hotel" , "56227" );          client.delete(request,RequestOptions.DEFAULT); } 
修改文档 修改文档分成:
全量修改:本质是先根据id删除,再新增 
增量修改:修改文档中的指定字段值 
 
全量修改的语法和新增语法一致,下面演示增量修改的语法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Test void  testUpdateDocument ()  throws  IOException          UpdateRequest request = new  UpdateRequest("hotel" ,"56227" );          HashMap<String, Object> hashMap = new  HashMap<>();     hashMap.put("price" ,299 );     hashMap.put("starName" ,"三星级" );          request.doc(hashMap);          client.update(request,RequestOptions.DEFAULT); } 
修改前
修改后
批量新增文档 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Test void  testBulkRequest ()  throws  IOException          List<Hotel> hotelList = hotelService.list();          BulkRequest bulkRequest = new  BulkRequest();          for  (Hotel hotel : hotelList) {                  HotelDoc hotelDoc = new  HotelDoc(hotel);                  bulkRequest.add(                 new  IndexRequest("hotel" )                 .id(hotelDoc.getId().toString())                 .source(JSON.toJSONString(hotelDoc),XContentType.JSON)         );     }          client.bulk(bulkRequest,RequestOptions.DEFAULT); } 
查询操作 match_all查询 DSL语法
1 2 3 4 5 6 7 8 # 查询所有 GET /hotel/_search { "query" : {"match_all" : {} } } 
Java语法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @Test void  testQueryMatchAll ()  throws  IOException          SearchRequest searchRequest = new  SearchRequest("hotel" );          searchRequest.source().query(QueryBuilders.matchAllQuery());          SearchResponse response = client.search(searchRequest, RequestOptions.DEFAULT);          SearchHits searchHits = response.getHits();          long  total = searchHits.getTotalHits().value;     System.out.println("获取到"  + total + "条数据" );          SearchHit[] hits = searchHits.getHits();          for  (SearchHit hit : hits) {                  String json = hit.getSourceAsString();                  HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);         System.out.println(hotelDoc);     } } 
解析SearchResponse方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 void  parsingData (SearchResponse response)          SearchHits searchHits = response.getHits();          long  total = searchHits.getTotalHits().value;          SearchHit[] hits = searchHits.getHits();     for  (SearchHit hit : hits) {         String json = hit.getSourceAsString();         HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);         System.out.println(hotelDoc);     } } 
match查询 DSL查询
1 2 3 4 5 6 7 8 9 # 单字段查询 GET /hotel/_search { "query" : {"match" : {"all" : "外滩酒店" } } } 
Java代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @Test void  testMatchSearch ()  throws  IOException          SearchRequest searchRequest = new  SearchRequest("hotel" );          searchRequest.source().query(       QueryBuilders.matchQuery("all" ,"上海" )     ).from(5 ).size(6 );          SearchResponse response = client.search(searchRequest, RequestOptions.DEFAULT);          SearchHits searchHits = response.getHits();          long  total = searchHits.getTotalHits().value;          SearchHit[] hits = searchHits.getHits();     for  (SearchHit hit : hits) {         String json = hit.getSourceAsString();         HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);         System.out.println(hotelDoc);     } } 
多字段查询
DSL
1 2 3 4 5 6 7 8 9 10 # 多字段查询 GET /hotel/_search { "query" : {"multi_match" : {"query" : "上海外滩" ,"fields" : ["brand" , "name" , "business" ]} } } 
java代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Test void  testMultiMatchSearch ()  throws  IOException          SearchRequest searchRequest = new  SearchRequest("hotel" );          searchRequest.source().query(         QueryBuilders.multiMatchQuery("上海" ,"name" ,"business" )     );          SearchResponse response = client.search(searchRequest, RequestOptions.DEFAULT);          parsingData(response); } 
term精准查询 DSL语句
1 2 3 4 5 6 7 8 9 10 11 12 # 精准查询 GET /hotel/_search { "query" : {"term" : {"city" : {"value" : "上海" } } } } 
java代码
1 2 3 4 5 6 7 8 9 10 @Test void  testTermSearch ()  throws  IOException     SearchRequest request = new  SearchRequest("hotel" );     request.source().query(       QueryBuilders.termQuery("city" ,"上海" )           );     SearchResponse response = client.search(request, RequestOptions.DEFAULT);     parsingData(response); } 
range范围查询 range 查询,用于数据过滤查询,比如下面查询价格大于等于400小于等于500的数据
DSL语法
1 2 3 4 5 6 7 8 9 10 11 GET /hotel/_search {   "query" : {     "range" : {       "price" : {         "gte" : 400 ,         "lte" : 500        }     }   } } 
range 查询,用于数据过滤查询,比如下面查询价格大于400小于500的数据
1 2 3 4 5 6 7 8 9 10 11 GET /hotel/_search {   "query" : {     "range" : {       "price" : {         "gte" : 400 ,         "lte" : 500        }     }   } } 
java代码
1 2 3 4 5 6 7 8 9 @Test void  testRangeQuery ()  throws  IOException     SearchRequest request = new  SearchRequest("hotel" );     request.source()             .query(QueryBuilders.rangeQuery("price" ).gt(400 ).lt(500 ));     SearchResponse response = client.search(request, RequestOptions.DEFAULT);     parsingData(response); } 
bool布尔多条件查询 DSL
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 GET /hotel/_search {   "query" : {     "bool" : {       "must" : [         {           "match" : {             "all" : "如家"            }         },         {           "term" : {             "city" : {               "value" : "上海"              }           }         }       ],       "must_not" : [         {           "range" : {             "price" : {               "gte" : 400              }           }         }       ],       "filter" : [         {           "geo_distance" : {             "distance" : "10km" ,             "location" : {               "lat" : 31.21 ,               "lon" : 121.5              }           }         }       ]     }   } } 
java代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Test void  testBooleQuery ()  throws  IOException     SearchRequest request = new  SearchRequest("hotel" );          BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();          boolQuery.must(QueryBuilders.matchQuery("name" ,"上海如家" ));          boolQuery.must(QueryBuilders.termQuery("city" ,"上海" ));          boolQuery.filter(QueryBuilders.rangeQuery("price" ).lt(400 ));          request.source().query(boolQuery).sort("price" ,SortOrder.ASC);          SearchResponse search = client.search(request, RequestOptions.DEFAULT);     parsingData(search); } 
查询结果
分页和排序查询 搜索名字包含如家的酒店,按照评分降序排序,评分相同时按照价格升序排序
DSL
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 GET /hotel/_search {   "query" : {     "match" : {       "name" : "如家"      }   },   "sort" : [     {       "score" : "desc"      },     {       "price" : "asc"      }   ] } 
Java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Test void  testPageSearch ()  throws  IOException          int  page = 1 ,size = 10 ;          SearchRequest request = new  SearchRequest("hotel" );          request.source().query(QueryBuilders.matchQuery("all" ,"上海如家" ));          request.source().sort("price" ,SortOrder.ASC);          request.source().from((page - 1 ) * size).size(page * size);          SearchResponse response = client.search(request, RequestOptions.DEFAULT);     parsingData(response); } 
按照地理位置排序查询 查找距离自己最近的酒店
DSL语法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 GET /hotel/_search {   "query" : {     "match_all" : {}   },   "sort" : [     {       "_geo_distance" : {         "location" : {           "lat" : 30.913828 ,           "lon" : 121.50091          },         "order" : "asc" ,         "unit" : "km"        }     }   ] } 
算分函数语法 
算分函数function_scope query 查询名字中包含外滩的酒店,并且吧7天酒店的排名往前靠
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 GET hotel/_search {   "query" : {     "function_score" : {       "query" : {         "match" : {           "all" : "外滩"          }       },       "functions" : [         {           "filter" : {             "term" : {               "brand" : "7天酒店"              }           },           "weight" : 10          }       ],       "boost_mode" : "sum"      }   } } 
查询结果高亮显示 DSL 语法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 # 高亮查询 GET /hotel/_search { "query" : {"match" : {"all" : "如家" } }, "highlight" : {"fields" : {"name" : {"require_field_match" : "false" } } } } 
Java代码
1 2 3 4 5 6 7 8 9 10 @Test void  testHighlight ()  throws  IOException     SearchRequest searchRequest = new  SearchRequest("hotel" );     searchRequest.source().query(QueryBuilders.matchQuery("all" ,"如家" ));          searchRequest.source().highlighter(new  HighlightBuilder().field("name" ).requireFieldMatch(false ));     SearchResponse search = client.search(searchRequest, RequestOptions.DEFAULT);     parsingData(search); } 
解析高亮代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 void  parsingData (SearchResponse response)          SearchHits searchHits = response.getHits();          long  total = searchHits.getTotalHits().value;     System.out.println("共有:"  + total + "条数据" );          SearchHit[] hits = searchHits.getHits();     for  (SearchHit hit : hits) {         String json = hit.getSourceAsString();         HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);                  Map<String, HighlightField> highlightFields = hit.getHighlightFields();         if (!CollectionUtils.isEmpty(highlightFields)){             HighlightField highlightField = highlightFields.get("name" );             if  (highlightField != null ) {                 String name = highlightField.getFragments()[0 ].string();                 hotelDoc.setName(name);             }         }         System.out.println(hotelDoc);     } } 
多条件查询 查询条件如下
首先完善参数对象,添加对应的字段
1 2 3 4 5 6 7 8 9 10 11 12 @Data public  class  HotelParams      String key;     Integer page;     Integer size;     String sortBy;     String city;     String brand;     String starName;     Integer minPrice;     Integer maxPrice; } 
查询逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 @Autowired RestHighLevelClient client; @Override public  PageHotel hotelFilters (HotelParams params)      try  {                  SearchRequest searchRequest = new  SearchRequest("hotel" );                  buildQuery(params, searchRequest);                  SearchResponse response = client.search(searchRequest, RequestOptions.DEFAULT);                  return  parsingData(response);     } catch  (IOException e) {         throw  new  RuntimeException(e);     } } private  void  buildQuery (HotelParams params, SearchRequest searchRequest)      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 (!StringUtils.isEmpty(params.getCity())){         boolQuery.filter(QueryBuilders.termQuery("city" , params.getCity()));     }          if (!StringUtils.isEmpty(params.getBrand())){         boolQuery.filter(QueryBuilders.termQuery("brand" , params.getBrand()));     }          if (!StringUtils.isEmpty(params.getStarName())){         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()));     }          int  page = params.getPage();     int  size = params.getSize();     searchRequest.source().query(boolQuery).from((page - 1 ) * size).size(size); } 
查询距离我最近的酒店,并显示距离 前端会传递过来一个经纬度
需要在查询对象中添加对应的属性
添加查询逻辑,核心代码
1 2 3 4 5 6 7 8 if (!StringUtils.isEmpty(params.getLocation())){    searchRequest.source().sort(SortBuilders             .geoDistanceSort("location" ,new  GeoPoint(params.getLocation()))             .order(SortOrder.ASC)             .unit(DistanceUnit.KILOMETERS)     ); } 
增加距离的解析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 PageHotel parsingData (SearchResponse response)  {         SearchHits searchHits = response.getHits();          long  total = searchHits.getTotalHits().value;          SearchHit[] hits = searchHits.getHits();     ArrayList<HotelDoc> hotelDocArrayList = new  ArrayList<>();     for  (SearchHit hit : hits) {         String json = hit.getSourceAsString();         HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);                  Object[] sortValues = hit.getSortValues();         if (sortValues.length > 0 ){             hotelDoc.setDistance(sortValues[0 ]);         }         hotelDocArrayList.add(hotelDoc);     }     return  new  PageHotel(total,hotelDocArrayList); } 
效果展示
广告置顶功能 通过代码来控制算分
首先添加属性 isAD 来表示是否是广告
然后编写 DSL 找两个数据来更新文档
1 2 3 4 5 6 7 8 9 10 11 12 13 POST /hotel/_update/517915 {   "doc" : {     "isAD" : true    } } POST /hotel/_update/36934 {   "doc" : {     "isAD" : true    } } 
添加算分查询
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 private  void  buildQuery (HotelParams params, SearchRequest searchRequest)      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 (!StringUtils.isEmpty(params.getCity())){         boolQuery.filter(QueryBuilders.termQuery("city" , params.getCity()));     }          if (!StringUtils.isEmpty(params.getBrand())){         boolQuery.filter(QueryBuilders.termQuery("brand" , params.getBrand()));     }          if (!StringUtils.isEmpty(params.getStarName())){         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()));     }          FunctionScoreQueryBuilder functionScoreQueryBuilder =             QueryBuilders.functionScoreQuery(                                          boolQuery,                                          new  FunctionScoreQueryBuilder.FilterFunctionBuilder[]{                                                          new  FunctionScoreQueryBuilder.FilterFunctionBuilder(                                                                          QueryBuilders.termQuery("isAD" ,true ),                                                                          ScoreFunctionBuilders.weightFactorFunction(10 )                             )                     }             );     searchRequest.source().query(functionScoreQueryBuilder);          int  page = params.getPage();     int  size = params.getSize();     searchRequest.source().from((page - 1 ) * size).size(size);          if (!StringUtils.isEmpty(params.getLocation())){         searchRequest.source().sort(SortBuilders                 .geoDistanceSort("location" ,new  GeoPoint(params.getLocation()))                 .order(SortOrder.ASC)                 .unit(DistanceUnit.KILOMETERS)         );     } } 
效果展示
数据聚合 什么是聚合 聚合是对文档数据的统计、分析、计算
聚合的常见种类有哪些
Bucket:对文档进行分组,并统计每组数量 
Metric:对文档数据做计算,如 Max、Min 
Pipeline:基于其他聚合结果再聚合 
 
参与聚合的字段类型必须是
DSL实现聚合 按照酒店品牌做聚合
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 GET hotel/_search {   "size" : 0 ,   "aggs" : {     "brandAgge" : {       "terms" : {         "field" : "brand" ,         "size" : 20 ,         "order" : {           "_count" : "desc"          }       }     }   } } 
查询结果
默认按照 _count 降序返回,可以设置 "_count": "ase",实现升序返回
1 2 3 "order": { "_count": "desc" } 
另外聚合查询时,会吧所有的数据查出来做聚合,这对内存的消耗很大,我们可以通过 query 来限制查询范围
例如:查询价格小于等于200的酒店做聚合
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 GET hotel/_search {   "query" : {     "range" : {       "price" : {         "lte" : 200        }     }   },   "size" : 0 ,   "aggs" : {     "brandAgge" : {       "terms" : {         "field" : "brand" ,         "size" : 20 ,         "order" : {           "_count" : "asc"          }       }     }   } } 
查询结果
小结:
aggs代表聚合,与query同级,此时query的作用是什么
 
聚合必须的三要素
 
聚合可配置的属性有哪些
size 指定聚合结果的数量 
order 指定聚合结果的排序方式 
field 指定聚合字段 
 
 
 
嵌套聚合 嵌套聚合,求出每个品牌的平均得分,并按照平均分降序返回
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 GET hotel/_search {   "size" : 0 ,   "aggs" : {     "brandAgge" : {       "terms" : {         "field" : "brand" ,         "size" : 20 ,         "order" : {           "scoreAgg.avg" : "desc"          }       },       "aggs" : {         "scoreAgg" : {           "stats" : {             "field" : "score"            }         }       }     }   } } 
返回结果
RestAPI实现聚合 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 @Test void  aggregateQuery ()  throws  IOException          SearchRequest request = new  SearchRequest("hotel" );          request.source().size(0 );          TermsAggregationBuilder brandAgg = AggregationBuilders.terms("brandAgg" )         .field("brand" )         .order(BucketOrder.aggregation("scoreAvg" , false ))         .size(20 );          brandAgg.subAggregation(AggregationBuilders.avg("scoreAvg" ).field("score" ));     brandAgg.subAggregation(AggregationBuilders.max("scoreMax" ).field("score" ));     brandAgg.subAggregation(AggregationBuilders.min("scoreMin" ).field("score" ));          request.source().aggregation(brandAgg);          SearchResponse response = client.search(request, RequestOptions.DEFAULT);          Terms brandTerms = response.getAggregations().get("brandAgg" );     List<? extends Terms.Bucket> brandTermsBuckets = brandTerms.getBuckets();     System.out.println("|品牌|数量|平均分|最高分|最低分|" );     for  (Terms.Bucket bucket : brandTermsBuckets) {         String keyAsString = bucket.getKeyAsString();         long  docCount = bucket.getDocCount();                  Avg scoreAvg =  bucket.getAggregations().get("scoreAvg" );         Max scoreMax =  bucket.getAggregations().get("scoreMax" );         Min scoreMin =  bucket.getAggregations().get("scoreMin" );         System.out.print(keyAsString + "|" );         System.out.print(docCount + "|" );         System.out.print(scoreAvg.getValue() + "|" );         System.out.print(scoreMax.getValue() + "|" );         System.out.println(scoreMin.getValue() + "|" );     } } 
返回结果
实现根据查询结果聚合 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 @Override public  HashMap<String, Object> filterList (HotelParams params)       try  {         SearchRequest request = new  SearchRequest("hotel" );                  buildQuery(params, request);                  request.source().size(0 );                  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 ));                  SearchResponse response = client.search(request, RequestOptions.DEFAULT);                  Aggregations aggregations = response.getAggregations();                  List<String> brandAgg = getAggregationByName(aggregations, "brandAgg" );                  List<String> cityAgg = getAggregationByName(aggregations, "cityAgg" );                  List<String> starAgg = getAggregationByName(aggregations, "starAgg" );         HashMap<String, Object> filterMap = new  HashMap<>();         filterMap.put("brand" ,brandAgg);         filterMap.put("city" ,cityAgg);         filterMap.put("star" ,starAgg);         return  filterMap;     } catch  (IOException e) {         e.printStackTrace();     }     return  null ; } 
封装的 getAggregationByName 方法,用来根据聚合名称,获取聚合结果
1 2 3 4 5 6 7 8 9 10 11 private  List<String> getAggregationByName (Aggregations aggregations,String aggName)     Terms terms = aggregations.get(aggName);     List<? extends Terms.Bucket> buckets = terms.getBuckets();     List<String> list = new  ArrayList<>();     for  (Terms.Bucket bucket : buckets) {         String brandName = bucket.getKeyAsString();         list.add(brandName);     }     return  list; } 
实现根据拼音自动补全 安装拼音分词器 我们可以实现根据拼音自动补全功能
GitHub地址:拼音分词器) 
在线安装
es 是容器的名称,根据你的实际情况调整
下载的版本必须和你的 elasticsearch 版本一致
 
1 2 3 4 5 6 7 8 9 10 # 进入容器内部 docker exec -it es /bin/bash # 在线下载并安装 ./bin/elasticsearch-plugin  install https://github.com/medcl/elasticsearch-analysis-pinyin/releases/download/v7.12.1/elasticsearch-analysis-pinyin-7.12.1.zip #退出 exit #重启容器 docker restart es 
测试分词器是否安装成功
1 2 3 4 5 6 7 POST /_analyze {   "text" : [     "如家酒店还不错"    ],   "analyzer" : "pinyin"  } 
查看结果
自定义分词器 在上面的例子中,我们通过拼音分词器实现了拼音分词的效果,但是中文没有保留下来,并且每一个字都有一个拼音,并不是一个词语来分词,这对我们来说是没有用的,所以,我们需要自定义分词器,来实现通过拼音分词时,按照词语来分词,并且保留中文和拼音。
elasticsearch中分词器(analyzer)的组成包含三部分
character filter :在 tokennizer 之前对文本进行处理,例如:删除字符串、替换字符 
tokenizer:将文本按照一定的规则切割词条(term),例如:keyword,就是不分词,还有 ik_smart 
tokenizer filter:将 tokenizer 输出的词条做进一步处理。例如大小写转换、同义词处理、拼音处理等 
 
实现自定义分词器,运行如下函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 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"        }     }   } } 
上面这段的含义:
首先添加一个 test 索引库 
设置分词器,分词器的名称叫做 my_analyzer 
设置 tokenizer 使用 ik_max_word 分词 
filter 设置使用拼音分词器过滤,这里使用的代词 py 
然后对 py 分词器作进一步设置,设置了他的分词策略 
最后添加 mappings 映射,映射 name 字段,使用的分词器是 my_analyzer 
 
测试自定义分词器
自定义分词器只能在当前索引中使用
 
1 2 3 4 5 6 7 POST /test/_analyze {   "text" : [     "如家酒店还不错"    ],   "analyzer" : "my_analyzer"  } 
分词效果
测试在文档查询时的效果
首先往 test 索引库中添加几条文档
1 2 3 4 5 6 7 8 9 POST /test/_doc {   "name" : "狮子"  } POST /test/_doc {   "name" : "柿子"  } 
测试查询
情景一:通过拼音查询字段
1 2 3 4 5 6 7 8 GET test/_search {   "query" : {     "match" : {       "name" : "sz"      }   } } 
情景二:通过中文来查询
1 2 3 4 5 6 7 8 GET test/_search {   "query" : {     "match" : {       "name" : "动物园里有狮子"      }   } } 
自动补全 自动补全有特殊的要求:
参与补全查询的字段类型必须是:completion 类型 
字段的内容一般是用来补全的多个词条组成 
 
首先创建一个索引库
1 2 3 4 5 6 7 8 9 10 PUT test2 {   "mappings" : {     "properties" : {       "title" : {         "type" : "completion"        }     }   } } 
往索引库中添加文档
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 POST test2/_doc {   "title" : [     "Sony" ,     "WH-1000XM3"    ] } POST test2/_doc {   "title" : [     "SK-II" ,     "PITERA"    ] } POST test2/_doc {   "title" : [     "Nintendo" ,     "switch"    ] } 
测试补全查询
1 2 3 4 5 6 7 8 9 10 11 12 13 GET test2/_search {   "suggest" : {     "titleSuggest" : {       "text" : "s" ,       "completion" : {         "field" : "title" ,         "skip_duplicates" : true ,         "size" : 10        }     }   } } 
解释:
使用 suggest 查询,而不是使用 query 查询文档 
titleSuggest 自定义的字段,会作为属性返回 
"text": "s":自动补全的开头词语"skip_duplicates": true:去除重复值"size": 10:查询前10条数据 
实现酒店数据自动补全功能 修改索引库数据 先删除旧的索引库,在添加新的索引库
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 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"        }     }   } } 
修改JAVA对象 添加 suggestion 字段
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 package  cn.itcast.hotel.pojo;import  lombok.Data;import  lombok.NoArgsConstructor;import  java.util.ArrayList;import  java.util.Arrays;import  java.util.Collections;import  java.util.List;@Data @NoArgsConstructor public  class  HotelDoc      private  Long id;     private  String name;     private  String address;     private  Integer price;     private  Integer score;     private  String brand;     private  String city;     private  String starName;     private  String business;     private  String location;     private  String pic;     private  Object distance;     private  Boolean isAD;     private  List<String> suggestion;     public  HotelDoc (Hotel hotel)           this .id = hotel.getId();         this .name = hotel.getName();         this .address = hotel.getAddress();         this .price = hotel.getPrice();         this .score = hotel.getScore();         this .brand = hotel.getBrand();         this .city = hotel.getCity();         this .starName = hotel.getStarName();         this .business = hotel.getBusiness();         this .location = hotel.getLatitude() + ", "  + hotel.getLongitude();         this .pic = hotel.getPic();         if (this .business.contains("/" )){             String[] list = this .business.split("/" );             this .suggestion = new  ArrayList<>();             this .suggestion.add(this .brand);             Collections.addAll(this .suggestion,list);         }else {             this .suggestion = Arrays.asList(this .business,this .brand);         }     } } 
重新执行添加文档的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Test void  testBulkRequest ()  throws  IOException          List<Hotel> hotelList = hotelService.list();          BulkRequest bulkRequest = new  BulkRequest();          for  (Hotel hotel : hotelList) {                  HotelDoc hotelDoc = new  HotelDoc(hotel);                  bulkRequest.add(                 new  IndexRequest("hotel" )                 .id(hotelDoc.getId().toString())                 .source(JSON.toJSONString(hotelDoc),XContentType.JSON)         );     }          client.bulk(bulkRequest,RequestOptions.DEFAULT); } 
测试查询结果
1 2 3 4 5 6 7 8 9 10 11 12 13 GET hotel/_search {   "suggest" : {     "hotel_suggestion" : {       "text" : "pu" ,       "completion" : {         "field" : "suggestion" ,         "skip_duplicates" : true ,         "size" : 100        }     }   } } 
使用Java代码实现自动补全查询 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 @Test void  testSuggest ()  throws  IOException          SearchRequest request = new  SearchRequest("hotel" );          request.source().suggest(new  SuggestBuilder().addSuggestion(             "suggestion" ,             SuggestBuilders.completionSuggestion("suggestion" )                     .prefix("sh" )                     .skipDuplicates(true )                     .size(10 )     ));          SearchResponse response = client.search(request, RequestOptions.DEFAULT);          Suggest suggest = response.getSuggest();          CompletionSuggestion suggestion = suggest.getSuggestion("suggestion" );          List<CompletionSuggestion.Entry.Option> options = suggestion.getOptions();          for  (CompletionSuggestion.Entry.Option option : options) {         String text = option.getText().toString();         System.out.println(text);     } } 
调用接口实现自动补全 添加 controller
1 2 3 4 @GetMapping("suggestion") List<String> suggestionSearch (@RequestParam("key")  String key)  {    return  hotelService.suggestionSearch(key); } 
实现 suggestionSearch 方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 @Override public  List<String> suggestionSearch (String key)      try  {         SearchRequest request = new  SearchRequest("hotel" );         request.source().suggest(new  SuggestBuilder().addSuggestion(            "suggestion" ,                 SuggestBuilders.completionSuggestion("suggestion" )                         .prefix(key)                         .skipDuplicates(true )                         .size(10 )         ));         SearchResponse response = client.search(request, RequestOptions.DEFAULT);         Suggest suggest = response.getSuggest();         CompletionSuggestion suggestion = suggest.getSuggestion("suggestion" );         return  suggestion.getOptions().stream()                 .map(Suggest.Suggestion.Entry.Option::getText)                 .map(Text::toString)                 .collect(Collectors.toList());     } catch  (IOException e) {         throw  new  RuntimeException(e);     } } 
最终效果
数据同步 数据同步的三种方式 方式一:同步调用
方式二:异步通知
优点:低耦合,实现难度一般 
缺点:依赖mq的可靠性 
 
方式三:监听binlog
优点:完全解除服务间的耦合 
确定:开启binlog增加数据库的负担,实现复杂度高 
 
导入hotel-admin 在课程资料中导入 hotel-admin
运行查看
声明队列和交换机 队列和交换机的声明需要在消费者声明,所以需要在 hotel-demo 项目中完成以下操作
引入依赖
1 2 3 4 5 <dependency >     <groupId > org.springframework.boot</groupId >      <artifactId > spring-boot-starter-amqp</artifactId >  </dependency > 
添加配置
1 2 3 4 5 6 7 spring:   rabbitmq:      host:  182.43 .250 .34       port:  5672       virtual-host:  /       username:  szx       password:  abc123   
声明常量类,用来配置交换机名称、队列名称、routingKey
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 package  cn.itcast.hotel.constants;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_INERT_KEY = "hotel_insert_key" ;          public  final  static  String HOTEL_DELETE_KEY = "hotel_delete_key" ; } 
使用 Bean 注入的方式来声明交换机和队列
添加 MqConfig 配置类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 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_INERT_KEY);     }          @Bean      public  Binding deleteQueueBinding ()          return  BindingBuilder.bind(deleteQueue()).to(topicExchange()).with(MqConstants.HOTEL_DELETE_KEY);     } } 
添加消息发送 以下操作需要在 hotel-admin 项目中完成
引入依赖
1 2 3 4 5 <dependency >     <groupId > org.springframework.boot</groupId >      <artifactId > spring-boot-starter-amqp</artifactId >  </dependency > 
添加配置
1 2 3 4 5 6 7 spring:   rabbitmq:      host:  182.43 .250 .34       port:  5672       virtual-host:  /       username:  szx       password:  abc123   
添加常量配置,将上面的 MqConstants 复制过来
添加发送消息代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 @Autowired private  IHotelService hotelService;@Autowired private  RabbitTemplate rabbitTemplate;@PostMapping public  void  saveHotel (@RequestBody  Hotel hotel)     hotelService.save(hotel);          rabbitTemplate.convertAndSend(MqConstants.HOTEL_EXCHANGE,MqConstants.HOTEL_INERT_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_INERT_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); } 
Sentinel 雪崩问题 什么是雪崩问题:微服务之间相互调用,因为调用链中的一个服务故障,引起的整个链路都无法访问的情况
如何避免雪崩问题:
超时处理:设定超时时间,请求超过一定的时间没有响应就返回错误信息,不会无休止的等待。 
舱壁模式:限定每个业务能使用的线程数,避免耗尽整个tomcat的资源,因此也叫线程隔离。 
熔断降级:由断路器统计业务执行的异常比例,如果超出闻值则会熔断该业务,拦截访问该业务的一切请求。 
流量控制:限制业务访问的QPS,避免服务因流量的突增而故障。 
 
服务保护的对比 
安装Sentinel控制台 可以访问GitHUb 官网下载,也可以从课程资料中获取
运行启动命令:
1 2 3 4 java -jar sentinel-dashboard-1.8.1.jar java -Dserver.port=7001 -jar sentinel-dashboard-1.8.1.jar 
默认启动的是8080端口,所以我们访问:http://localhost:7001/ 
默认账号密码都是:Sentinel
登录成功后默认只有一个欢迎页
这是因为我们目前还没有一个微服务接入
拿出我们之前学习的 cloud-demo 项目来继续学习
同时启动nacos,查看服务列表
微服务整合Sentinel 在 order-service 服务中安装依赖
1 2 3 4 5 <dependency >     <groupId > com.alibaba.cloud</groupId >      <artifactId > spring-cloud-starter-alibaba-sentinel</artifactId >  </dependency > 
如果出现安装失败的问题,检查父组件的 Spring Cloud Alibaba 的依赖管理,是否添加作用域
1 2 3 4 5 6 7 8 <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 > 
添加配置文件
1 2 3 4 5 6 7 server:   port:  8088  spring:   cloud:      sentinel:        transport:          dashboard:  localhost:7001  
重启完成后我们访问几次order服务下的接口:
http://localhost:8080/order/101 
刷新 sentinel 控制台
流量控制 簇点链路 当请求进入微服务时,首先会访问DispatcherServlet,然后进入Controller、Service、Mapper,这样的一个调用链就叫做簇点链路 资源 。
默认情况下sentinel会监控SpringMVC的每一个端点(Endpoint,也就是controller中的方法),因此SpringMVC的每一个端点(Endpoint)就是调用链路中的一个资源。
例如,我们刚才访问的order-service中的OrderController中的端点:/order/{orderId}
流控、熔断等都是针对簇点链路中的资源来设置的,因此我们可以点击对应资源后面的按钮来设置规则:
流控:流量控制 
降级:降级熔断 
热点:热点参数限流,是限流的一种 
授权:请求的权限控制 
 
快速入门 点击接口后面的流控按钮
然后点击新增保存当前配置
打开接口压测工具,如果没有用过jmeter,可以参考课前资料提供的文档《Jmeter快速入门.md》
启动后查看返回