微服务
微服务就是一种经过良好架构设计的分布式架构方案,微服务架构的特征:
单一职责:微服务拆分力度更大,每一个服务都对应唯一的业务能力,做到单一职责,避免重复的业务开发
面向服务:微服务只对外暴露业务接口
自治:团队独立、技术独立、数据独立、部署独立
隔离性强:服务调用做好隔离、容错、降级,避免出现级联问题
第一天
一、认识微服务
1. 几种技术的比较
2. SpringCloud
SpringCloud是目前国内使用最广泛的微服务框架
官网地址:https://spring.io/projects/spring-cloud
版本对应信息查询地址:https://start.spring.io/actuator/info
SpringCloud集成了各种微服务功能组件,并给予SpringBoot实现了这些组件的自动装配,从而提供了良好的开箱即用体验
SpringCloud与SpringBoot的版本兼容情况如下:

3. 服务拆分及远程调用
3.1 服务拆分注意事项
微服务要根据业务模块拆分,做到单一职责,不同的微服务中不要重复开发相同的业务
微服务可以将业务暴露为接口,供其他微服务使用
微服务数据独立,不要访问其他微服务的数据库
3.2 微服务的远程调用
基于RestTemplate发起的http请求实现远程调用
4. 提供者与消费者
服务提供者:在一次业务中,被其他微服务调用的服务
服务消费者:在一次业务中,调用其他微服务的服务
二、Eureka注册中心
Eureka的作用:将所有微服务集中注册,微服务请求时向注册中心发送消息即可获取服务列表
消费者如何获取服务提供者的具体信息?
服务提供者启动时会向EurekaServer注册自己的信息
消费者根据服务名称向EurekaServer拉取提供者信息
如果有多个服务提供者,消费者该如何选择?
服务消费者利用负载均衡算法,从服务列表中挑选一个
消费者如何感知服务提供者的健康状态?
服务提供者会每隔30秒向EurekaServer注册中心发送心跳请求,报告健康状态
EurekaServer会根据健康状态更新服务列表,消费者就可以获取到最新的服务信息
1. 搭建EurekaServer
搭建EurekaServer服务步骤如下:
创建项目,引入
spring-cloud-starter-netflix-eureka-server依赖<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId> </dependency>编写启动类,添加
@EnableEurekaServer注解添加
application.yml文件,书写配置内容如下:server: port: 10086 spring: application: name: eurekaserver eureka: client: service-url: defaultZone: http://127.0.0.1:10086/eureka
2. 注册微服务
步骤如下:
在
user-service项目中引入spring-cloud-starter-netflix-eureka-client的依赖<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency>在
application.yml文件添加如下配置:spring: application: name: userservice eureka: client: service-url: defaultZone: http://127.0.0.1:10086/eureka/
3. 服务发现
在order-service中完成服务发现
修改
OrderService的代码,修改访问的url路径,用服务名代替IP和端口号String url = "http://uservice/user/" + order.getUserId(); User user = restTemplate.getForObject(url,User.class);在
order-service项目的启动类OrderApplication中的RestTemplate添加负载均衡注解@LoadBalanced:@Bean @LoadBalanced public RestTemplate restTemplate(){ return new RestTemplate(); }
三、Ribbon负载均衡
1. 负载均衡的流程
order-service发起请求http://userservice/user/1LoadBalancerInterceptor负载均衡拦截器拦截请求,交给RibbonLoadBanlancerClientRibbonLoadBanlancerClient获得服务名称交给DynamicServerListLoadBalancerDynamicServerListLoadBalancer从IRule中选取负载均衡规则去决定选择使用的服务并将服务地址返还给RibbonLoadBalancerClientRibbonLoadBalancerClient将带有服务名称的url修改为指定服务真实地址的url并发送请求
2. IRule负载均衡的策略

3. 修改负载均衡规则的方式
3.1 代码方式
在order-service中的Order Application类中,定义一个新的IRule:
@Bean public IRule randomRule(){ return new RandomRule(); }
3.2 配置文件方式
在order-service的application.yml文件中,添加新的配置也可以修改规则:
userservice: ribbon: NFLoadBanlancerRuleClassName: com.netflix.loadbalancer.RandomRule
4. 饥饿加载
Ribbon默认采用懒加载,即第一次访问时才会创建LoadBalanceClient,请求时间会很长,而饥饿加载会在项目启动时创建,降低第一次访问时的耗时,通过下面的配置开启饥饿加载:
ribbon eager-load: enabled: true clients: - userservice
四、Nacos注册中心
GitHub: https://github.com/alibaba/nacos
版本对应说明:地址
前往Github下载安装包,解压到指定位置即可,使用以下命令启动:
startup.cmd -m standalone

1. Nacos搭建
步骤如下:
创建项目,引入spring-cloud-alibaba的依赖
<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>
2. 注册微服务
对应微服务引入
spring-cloud-starter-alibaba-nacos-discovery
com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discovery ```
修改该服务的application.yml文件,配置如下:
spring: cloud: nacos: server-addr: localhost:8848
3. Nacos服务多级存储模型
一级是服务,二级是集群,三级是实例
3.1 服务集群属性
修改提供者
application.yml,添加配置如下:spring: cloud: nacos: server-addr: localhost:8848 #配置nacos服务端地址 discovery: cluster-name: HZ #配置集群名称修改消费者
application.yml,添加配置如下:servicename: ribbon: NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRuleNacosRule负载均衡策略
优先选择同集群服务实例列表
本地集群找不到提供者,才去其他集群寻找,并且会警告
确定了可用实例列表后,再采用随机负载均衡挑选实例
注:从
spring-cloud-alibaba2020.*版本开始,已经逐渐弃用netflix相关组件,例如ribbon,要想继续使用,应该回退版本到spring-cloud-alibaba2.*,还要注意对应的spring-boot和spring-cloud版本的兼容性版本说明:Github版本说明
实例的权重控制
Nacos控制台可以设置实例的权重值,0-1之间
统计群内的多个实例,权重越高被访问的频率越高
权重设置为0则完全不会被访问
3.2 环境隔离 namespace
Nacos中服务存储和数据存储的最外层都是一个名为namespace的东西,用来做最外层隔离,未设置namespace的情况下默认为public

实例
在Nacos控制台创建命名空间dev,并复制namespace的UUID
在注册服务的application.yml中添加如下配置文件,即可将该服务分配到此命名空间下,此时若其他服务与它不属于同一namespace下,将无法访问此服务
namespace: 8f6f25b2-4758-40f7-8445-6c36ae9fb962spring: cloud: nacos: server-addr: localhost:8848 discovery: cluster-name: 西安 namespace: 8f6f25b2-4758-40f7-8445-6c36ae9fb962 #dev环境
Nacos环境隔离
namespace用来做环境隔离
每个namespace都有唯一id,由Nacos控制台生成UUID
不同namespace下服务不可见
可用来分别控制开发环境,测试环境,生产环境下的服务之间不可随意调用
4. Nacos和Eureka的区别

4.1 临时实例和非临时实例
服务注册时,在启动配置文件中添加以下配置来设置当前实例的类别
spring:
cloud:
nacos:
discovery:
ephemeral: false #设置为非临时实例
ephemeral:英 [ɪˈfemərəl]美 [ɪˈfemərəl]
adj. 短暂的;(主指植物)短生的,短命的
n. 只生存一天的事物;短生植物
4.2 区别与联系
共同点
都支持服务注册和服务拉取
都支持服务提供者心跳方式做健康检测
区别
Nacos支持服务端主动检测提供者状态
临时实例采用心跳模式
非临时实例采用主动监测模式
在Nacos中,临时实例心跳不正常会被剔除,非临时实例则不会被剔除
Nacos支持服务列表变更的消息推送模式,服务列表更新及时
Nacos集群默认采用AP方式,当集群中存在非临时实例时,采用CP模式;Eureka采用AP方式
第二天
五、 Nacos配置管理

1. 统一配置管理

因为bootstrap.yml的优先级高于application.yml,所以我们可以通过将Nacos服务信息配置到bootstrap.yml中来读取Nacos管理的配置文件,然后与本地配置文件进行合并,然后创建Spring容器,加载Bean启动程序
1.1 在指定服务的pom中引入Nacos配置管理客户端依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
1.2 在指定服务的resources目录下添加一个bootstrap.yml文件,这是一个引导文件,优先级高于application.yml
spring:
application:
name: user-service #服务名称
profiles:
active: dev #开发环境
cloud:
nacos:
server-addr: localhost:8848 #Nacos地址
config:
file-extension: yaml #文件后缀
2. 配置热更新
2.1 方式一
通过
@Value注解注入,结合@RefreshScope来刷新
2.2 方式二
通过
@ConfigurationProperties注解创建对应配置类注入,自动刷新
2.3 注意事项
不是所有的配置都适合放到配置中心,维护起来比较麻烦
建议将一些关键参数,需要运行时调整的参数放到nacos配置中心,一般都是自定义配置
3. 多环境配置共享
微服务启动时会从Nacos中读取多个配置文件:
[spring.application.name]-[spring.profiles.active].yaml
[spring.application.name].yaml
我们发现无论是开发环境,测试环境,生产环境,[spring.application.name].yaml一定会被加载,因此我们可以将多环境共享的配置内容写入这个文件
3.1 注意事项
当一个环境共享配置文件的值在本地配置文件中也存在时,以环境共享配置文件的值为主
多种配置的优先级:
[spring.application.name]-[spring.profiles.active].yaml>[spring.application.name].yaml>application.yaml
4. 搭建Nacos集群

4.1 创建集群数据库
create database if not exists nacos_config default character set utf8mb4 collate utf8mb4_general_ci;
4.2 创建集群数据表
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,
`encrypted_data_key` text NOT NULL COMMENT '秘钥',
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 '租户字段',
`encrypted_data_key` text NOT NULL 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 '租户字段',
`encrypted_data_key` text NOT NULL 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
);
4.3 向表中插入数据
INSERT INTO users (username, password, enabled) VALUES ('nacos', '$2a$10$EuWPZHzz32dJN7jexM34MOeYirDdFAZm2kuWj7VEOJhhZkDrxfvUu', TRUE);
INSERT INTO roles (username, role) VALUES ('nacos', 'ROLE_ADMIN');
4.4 配置Nacos
1. 打开 \Nacos安装目录\conf\
找到
cluster.conf.example文件,将后缀.example去掉,编辑cluster.conf文件,配置多个Nacos服务的地址及端口127.0.0.1:8845 127.0.0.1:8846 127.0.0.1:8847找到
application.properties文件,编辑文件将第33行取消注释,表示我们所使用的数据库为mysql
#*************** Config Module Related Configurations ***************# ### If use MySQL as datasource: spring.datasource.platform=mysql将36,39,40,41行取消注释,并修改为我们刚才建立的集群数据库信息
### Count of DB: db.num=1 ### Connect URL of DB: db.url.0=jdbc:mysql://127.0.0.1:3306/nacos_config?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC db.user.0=root db.password.0=root
2. 复制Nacos文件夹并分别修改为Nacos1,Nacos2,Nacos3
编辑
application.properties文件,修改对应端口nacos1
server.port = 8840nacos2
server.port = 8845nacos3
server.port = 8847
分别启动三个nacos节点
startup.cmd
4.5 Nginx反向代理
upstream nacos-cluster {
server 192.168.1.7:8840;
server 192.168.1.7:8845;
server 192.168.1.7:8847;
}
server {
listen 80;
server_name localhost;
location /nacos {
proxy_pass http://nacos-cluster;
}
}
如果访问
localhost/nacos提示404,那么就是80端口号被占用,请更改端口号重启nginx

六、 基于Feign的远程调用
1. 添加依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</ artifactId>
</dependency>
2. 代码书写
在服务启动类上添加
@EnableFeignClients注解@SpringBootApplication @EnableFeignClients public class OrderServiceApplication { public static void main(String[] args) { SpringApplication.run(OrderServiceApplication.class, args); } }创建远程调用服务接口,格式如下
@FeignClient("user-service") public interface UserClients { @GetMapping("/user/info/{id}") Response getUserInfoById(@PathVariable("id") Long id); }Feign调用远程服务接口实例
public Response getOrderInfoById(Long id){ Response response = new Response(); OrderInfo orderInfo = orderMapper.selectById(id); if(orderInfo!=null){ OrderVO orderVO = new OrderVO(); BeanUtils.copyProperties(orderInfo,orderVO); orderVO.setPrice(Double.valueOf((orderInfo.getPrice()/100))); Response userInfoById = userClients.getUserInfoById(orderInfo.getUserId()); orderVO.setUserInfo(userInfoById.getData()); response.code(ResponseCode.SUCCESS).message("获取成功").count(1).data(orderVO); } return response; }返回结果
{
"code": 200,
"message": "获取成功",
"count": 1,
"data": {
"id": "1543151273902505986",
"price": 3902.0,
"name": "OculusQuest2",
"num": 2,
"userId": "1543062156246155265",
"userInfo": {
"id": "1543062156246155265",
"username": "zhiyuan121",
"email": "5168154488@gmail.com",
"introduction": "个人简介",
"phoneNumber": "97938192868",
"nickname": "絷缘",
"status": "正常",
"registerTime": "2022-07-02"
},
"createTime": "2022-07-02"
}
}
3. Feign的自定义配置
3.1 日志配置方式
配置文件方式
全局生效
feign: client: config: default: #default表示全局配置 loggerLevel: FULL #日志级别局部生效
feign: client: config: user-service: #对应服务名表示局部配置 loggerLevel: FULL #日志级别
Java代码方式
声明一个Bean
public class FeignClientConfiguration{ @Bean public Logger.Level feignLogLevel(){ return Logger.Level.BASIC; } }全局生效:使用加在启动类上的
@EnableFeignClients上@EnableFeignClients(defaultConfiguration = FeignClientConfiguration.class)局部生效:使用加在指定服务上的
@FeignClient上@FeignClient(value = "user-service",configuration = FeignClientsConfiguration.class)
3. Feign性能优化
3.1 Feign底层客户端的实现
URLConnection:默认实现,不支持连接池
Apache HttpClient:支持连接池
OKHttp:支持连接池
因此优化Feign的性能主要包括:
使用连接池代替默认的URLConnection
日志级别,最好用basic或none
3.2 为Feign添加HttpClient支持
引入依赖
<dependency> <groupId>io.github.openfeign</groupId> <artifactId>feign-httpclient</artifactId> </dependency>配置连接池
feign: client: config: default: loggerLevel: BASIC httpclient: enabled: true #开启httpclient支持 max-connections: 200 #最大连接数 max-connections-per-route: 50 #每个路径最大连接数
4. Feign的最佳实践
4.1 方式一(继承)
给消费者的FeignClient和提供者的Controller定义统一的父接口作为标准
4.2 方式二(抽取)
将FeignClient抽取为独立模块,并且把接口有关的Entity、默认的Feign配置都放到这个模块中,提供给所有消费者使用

步骤:
首先创建一个module,命名为feign-api,然后引入feign依赖
将order-service中编写的UserClient、User、DefaultFeignConfiguration都复制到feign-api项目中
在order-service中引入feign-api依赖
修改order-service中所有与上述三个组件有关的import部分,改成导入feign-api中的包
当定义的FeignClient不在SpringBootApplication的扫描包范围时,这些FeignClient无法使用,解决方式有两种:
指定FeignClient所在包
@EnableFeignClients(basePackages = "com.zhiyuan.feign.clients")指定FeignClient字节码
@EnableFeignClients(clients = {UserClient.class})
抽取为feign-api后会导致日志配置失效,通过添加
logging.level.com.zhiyuan.clients:DEBUG可以使日志配置生效
七、统一网关Gateway
1. 网关的功能
1.1 身份认证和权限校验
1.2 服务路由、负载均衡
1.3 请求限流
2. SpringCloud中网关的实现
2.1 Gateway
2.2 zuul
Zuul是基于Servlet实现的,属于阻塞式编程,而SpringCloudGateway则是基于Spring5中提供的WebFlux,属于响应式编程的实现,具备更好的性能
3. 搭建网关
3.1 创建新的module,引入SpringCloudGateway的依赖和nacos的服务发现依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-nacos-discovery</artifactId>
</dependency>
3.2 编写路由配置及nacos地址
server:
port: 10010
spring:
application:
name: gateway
cloud:
nacos:
server-addr: localhost:8848
gateway:
routes: #网关路由配置
- id: user-service #路由ID,自定义唯一即可
uri: lb://user-service # 路由目标地址
predicates: #路由断言,即用来判断请求是否符合路由规则的配置
- Path=/user/**
3.3 路由断言工厂Route Predicate Factory
我们在配置文件中写的断言规则只是字符串,这些字符串会被Predicate Factory读取并处理,转变为路由判断的条件
org.springframework.cloud.gateway.handler.predicate.PathRoutePredicateFactory
3.4 路由过滤器GatewayFilter
局部过滤器
全局过滤器

使用示例
局部过滤器
#局部过滤器,只对定义该过滤器的服务有效 server: port: 10010 spring: application: name: gateway cloud: nacos: server-addr: localhost:8848 #nacos服务端地址 discovery: cluster-name: 西安 gateway: routes: - id: user-service uri: lb://user-service predicates: - Path=/user/** filters: - AddRequestHeader=Author,ZhiYuanXie - id: order-service uri: lb://order-service predicates: - Path=/order/**#默认过滤器,对配置的所有的服务有效 server: port: 10010 spring: application: name: gateway cloud: nacos: server-addr: localhost:8848 #nacos服务端地址 discovery: cluster-name: 西安 gateway: routes: - id: user-service uri: lb://user-service predicates: - Path=/user/** - id: order-service uri: lb://order-service predicates: - Path=/order/** default-filters: - AddRequestHeader=Author,ZhiYuanXie全局过滤器
@Order(-1) @Component public class AuthorizeFilter implements GlobalFilter,Ordered { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { //获取请求参数 MultiValueMap<String, String> queryParams = exchange.getRequest().getQueryParams(); //获取响应对象 ServerHttpResponse response = exchange.getResponse(); //获取authorization参数 String auth = queryParams.getFirst("authorization"); if ("admin".equals(auth)){ //放行 return chain.filter(exchange); } //拦截请求,响应对象设置HTTP状态码 response.setStatusCode(HttpStatus.UNAUTHORIZED); return response.setComplete(); } //设置过滤器优先级 public int getOrder(){ return -1; } }@Order注解路由过滤器、默认过滤器、全局过滤器的执行顺序

每一个过滤器都必须指定一个int类型的order值,order值越小,优先级越高
全局过滤器GlobalFilter通过实现Ordered接口,或者添加
@Order注解来指定order值,由我们自己实现路由过滤器Filters和默认过滤器defaultFilter由Spring指定,默认是按声明顺序从1开始递增
当过滤器的order值都一样时,会按照 defaultFilter > 路由过滤器 > GlobalFilter 的顺序执行
参考以下方法查看优先级:

3.5 网关的跨域请求配置
跨域:域名不一致就是跨域,主要包括:
域名不同
域名相同,端口不同
跨域问题:浏览器禁止请求发起者与服务端发生跨域ajax请求,请求被浏览器拦截的问题
解决方案:CORS
Gateway的跨域只需要简单配置即可实现
server:
port: 10010
spring:
application:
name: gateway
cloud:
nacos:
server-addr: localhost:8848 #nacos服务端地址
discovery:
cluster-name: 西安
gateway:
globalcors: #全局跨域处理
add-to-simple-url-handler-mapping: true #解决options请求被拦截的问题
cors-configurations:
'[/**]':
allowedOrigins: #跨域允许的网址
- "http://localhost:8080/"
allowedMethods: #跨域允许的请求方式
- "GET"
- "POST"
- "DELETE"
- "PUT"
- "OPTIONS"
allowedHeaders: "*" #跨域是否允许携带Header信息
allowedCredentials: true #跨域是否允许携带Cookie信息
maxAge: 360000 #跨域检测有效期
routes:
- id: user-service
uri: lb://user-service
predicates:
- Path=/user/**
- id: order-service
uri: lb://order-service
predicates:
- Path=/order/**
default-filters:
- AddRequestHeader=Author,ZhiYuanXie
第三天
八、容器化部署Docker
Docker如何解决大型项目依赖关系复杂,不同组件依赖的兼容性问题?
Docker允许开发汇总将应用、依赖、函数库、配置一起打包,形成可以直接安装的镜像
Docker应用运行在容器中,使用沙箱机制,相互隔离
Docker如何解决开发、测试、生产环境有差异的问题?
Docker镜像中包含完整的运行环境,包括系统函数库,仅依赖系统的Linux内核,因此可以在任意Linux操作系统上运行
Docker与虚拟机的区别:
虚拟机是使用Hypervisor技术在操作系统中模拟硬件设备,然后运行另一个操作系统,比如在Windows系统里面运行Ubuntu系统。
1. 概念
1.1 镜像(Image)
Docker将应用程序及其所需的依赖、函数库、环境、配置等文件打包在一起,称为镜像
1.2 容器(Container)
镜像中的应用程序运行后形成的进程就是容器,只是Docker会给容器做隔离,对外不可见
1.3 DockerHub
DockerHub是一个Docker镜像的托管平台,这样的平台称为Docker Registry
国内也有类似于DockerHub的公开服务,比如网易云镜像服务、阿里云镜像库等
1.4 Docker
Docker是一个CS架构的程序,由两部分组成
服务端:Docker守护进程,负责处理Docker指令,管理镜像、容器等
客户端:通过命令或RestAPI向Docker服务端发送指令,可以在本地或远程向服务端发送指令

2. 安装Docker
2.1 CentOS7安装Docker
CentOS7系统下载地址:
http://mirrors.163.com/centos/7.9.2009/isos/x86_64/CentOS-7-x86_64-DVD-2009.iso
迅雷下载镜像文件有奇效,IDM平时下载很快,下载镜像开8线程只能跑到1M/s,迅雷直接10M/s
若之前安装过其他版本Docker,通过以下方式完成卸载:
yum remove docker \ docker-client \ docker-client-latest \ docker-common \ docker-lastest-logrotate \ docker-logrotate \ docker-selinux \ docker-engine-selinux \ docker-engine \ docker-ce安装Docker
yum install -y yum-utils device-mapper-persistent-data lvm2更新yum本地镜像源
# 删除原有配置源目录 cd /etc rm -rf yum.repos.d # 新建配置源目录 mkdir yum.repos.d # 下载阿里镜像源 cd yum.repos.d wget https://mirrors.aliyun.com/repo/Centos-7.repo # 更新yum缓存 yum makecache更新软件源信息
参考:https://developer.aliyun.com/mirror/docker-ce?spm=a2c6h.13651102.0.0.40491b11PUGxwo
# step 1: 安装必要的一些系统工具 yum install -y yum-utils device-mapper-persistent-data lvm2 # Step 2: 添加软件源信息 yum-config-manager --add-repo https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo # Step 3 # Step 4: 更新并安装Docker-CE yum makecache fast yum -y install docker-ce # Step 4: 开启Docker服务 service docker start关闭防火墙(为了学习Docker,开发中应该开启指定端口)
# 关闭防火墙应用 systemctl stop firewalld # 禁止开机启动防火墙 systemctl disable firewalld # 查看防火墙状态 systemctl status firewalld配置Docker镜像源
mkdir -p /etc/docker tee /etc/docker/daemon.json <<-'EOF' { "registry-mirrors": ["https://xm9ypajm.mirror.aliyuncs.com"] } EOF systemctl daemon-reload systemctl restart docker
2.2 Docker基本操作
镜像的命名规范:
镜像名称一般由两部分组成:
[respository]:[tag]
respository:镜像名称
tag:版本
如果没有指定tag时,默认是latest,代表最新版本的镜像

镜像相关指令
docker images:查看当前所有镜像docker pull [respository]:[tag]:从镜像仓库中拉取指定镜像docker save -o [Path/FileName.tar] [respository]:[tag]:将指定镜像打包docker load -i [Path/FileName.tar]:将打包好的镜像加载到Docker中docker rmi [respository]:[tag]:移除指定镜像
示例:
从DockerHub中拉取一个nginx镜像并查看
docker pull nginx
docker images将nginx镜像打包到本地
docker save -o ~/nginx.tar nginx:latest将镜像文件从本地tar包加载
docker load -i ~/nginx.tar
docker images
容器相关命令
docker run:创建并运行一个容器docker pause:暂停运行docker unpause:继续运行docker stop:停止运行docker start:运行docker ps:查看所有运行容器及状态docker logs:查看容器运行日志docker exec:进入容器执行命令docker rm:删除指定容器
示例:
运行一个nginx容器
docker run --name mn -p 80:80 -d nginx
--name:指定容器名称
-p:指定端口映射
-d:后台运行
docker ps查看指定容器运行日志
docker logs mn跟踪查看运行日志:
docker logs -f mn进入容器执行命令
docker exec -it mv bash
exit:退出容器停止运行容器
docker stop mn查看所有容器包括未运行的
docker ps -a运行容器
docker start mn删除容器
docker rm:只能删除未运行的容器
docker rm -f mn:强制删除容器,无论是否运行
示例:运行一个持久化存储的redis容器,并通过redis-cli设置num=666
运行容器
docker run --name my-redis -p 6379:6379 -d redis redis-server --appendonly yes进入容器
docker exec -it my-redis bash启动redis-cli
redis-cli设置num=666
set num 666退出redis-cli,退出容器
exit
docker exec -it my-redis redis-cli:直接进入容器中启动redis-cli
2.3 数据卷操作
数据卷的作用:将容器与数据分离,解耦合,方便操作容器内数据,保证数据安全

基本语法:
docker volume [COMMAND]COMMAND
create:创建一个volume
inspect:显示一个或多个volume的信息
ls:列出所有的volume
prune:删除未使用的volume
rm:删除一个或多个指定的volume
挂载数据卷
创建并运行容器时指定数据卷的挂载目录,若数据卷不存在,则自动创建数据卷
docker run \ --name mn \ -p 80:80 \ -v html:/usr/share/nginx/html \ -d nginx
挂载目录
docker run \ --name some-mysql \ -e MYSQL_ROOT_PASSWORD=root \ -p 3306:3306 \ -v /tmp/mysql/data:/var/lib/mysql \ -v /tmp/mysql/conf/hmy.cnf:/etc/mysql/conf.d/hmy.cnf \ -d mysql:latest
数据卷挂载与目录挂载
数据卷挂载耦合度低,有docker来管理目录,但是目录较深,不好找
目录挂载耦合度高,需要我们自己管理目录,不过目录容易寻找查看
3. 镜像结构
镜像就是将应用程序及其所需要的系统函数库、环境、配置、依赖打包而成的
基础镜像(BaseImage):应用依赖的系统函数库、环境变量、配置、文件系统等
入口(Entrypoint):镜像运行入口,一般是程序启动的脚本和参数
层(Layer):在BaseImage基础上添加安装包、依赖、配置等,每次操作形成新的一层
镜像是分层结构,每一层称一个Layer
3.1 自定义镜像
Dockerfile:一个文本文件,指令的合集,用指令来说明要执行什么操作来构建镜像,每一个指令都会形成一层Layer
# 指定基础镜像
FROM ubuntu:16.04
# 配置环境变量,JDK的安装目录
ENV JAVA_DIR=/usr/local
# 拷贝jdk和java项目的包
COPY ./jdk8.tar.gz $JAVA_DIR/
COPY ./docker-demo.jar /tmp/app.jar
# 安装JDK
RUN cd $JAVA_DIR \
&& tar -xf ./jdk8.tar.gz \
&& mv ./jdk1.8.0_144 ./java8
# 配置环境变量
ENV JAVA_HOME=$JAVA_DIR/java8
ENV PATH=$PATH:$JAVA_HOME/bin
# 暴露端口
EXPOSE 8090
# 入口,java项目的启动命令
ENTRYPOINT java -jar /tmp/app.jar
mkdir -p /tmp/docker-demo将
docker-demo.jarjdk8.tar.gzDockerfile上传至/tmp/docker-demodocker build -t javaweb:1.0 .docker imagesdocker run --name web -p 8090:8090 -d javaweb:1.0访问
ip:8090/hello/count
我们发现在Dockerfile中构建jdk环境的操作是可复用的,我们应该把构建jdk环境的部分构建一个镜像,这样以后就可以直接使用了,而java:8-alpine帮我们做了这件事
3.2 DockerCompose
DockerCompose可以基于Compose文件帮我们快速部署分布式应用,而无需手动一个个创建和运行容器
Compose文件是一个文本文件,通过指令定义集群中的每个容器如何运行
version: "3.9" services: mysql: #指定服务名称 image: mysql:8.0.21 # 指定镜像文件 environment: #设置环境变量 MYSQL_ROOT_PASSWORD: 123456 volumes: #数据卷挂载 - /tmp/mysql/data:/var/lib/mysql - /tmp/mysql/conf/hym.cnf:/etc/mysql/conf.d/hym.cnf web: #指定服务名称 build: . #从当前目录中构建镜像 ports: #设置端口号 - "8090:8090"书写格式参考规范:
https://docs.docker.com/compose/compose-file/compose-file-v3/
https://docs.docker.com/compose/compose-file/compose-file-v2/
3.2.1 安装DockerCompose
参考:https://docs.docker.com/compose/install/compose-plugin/#installing-compose-on-linux-systems
curl -SL https://github.com/docker/compose/releases/download/v2.6.1/docker-compose-linux-x86_64 -o /usr/local/bin/docker-compose
docker-compose所在目录/usr/local/bin/docker-compose给
docker-compose添加可执行权限chmod +x docker-composeBase自动补全命令
curl \ -L https://raw.githubusercontent.com/docker/compose/v2.6.1/contrib/completion/bash/docker-compose \ -o /etc/bash_completion.d/docker-compose如果无法访问该地址,则修改本机hosts文件
echo "185.199.108.133 raw.githubusercontent.com" >> /etc/hosts
3.2.2 部署微服务集群
docker run \
--name my-mysql8 \
-e MYSQL_ROOT_PASSWORD=root \
-p 3306:3306 \
-d mysql:latest \
--character-set-server=utf8mb4 \
--collation-server=utf8mb4_unicode_ci
FROM java:8-alpine COPY ./app.jar /tmp/app.jar ENTRYPOINT java -jar /tmp/app.jar# docker-comspose配置文件语法版本 version: 3.8 services: nacos: images: nacos/nacos-server environment: MODE: standalone ports: - "8848:8848" mysql: images: mysql:8.0.31 environment: MYSQL_ROOT_PASSWORD: 996748 volumes: - "$PWD/mysql/data:/var/lib/mysql" - "$PWD/mysql/conf:/etc/mysql/conf.d" user-service: build: ./user-service order-service: build: ./order-service gateway: build: ./gateway ports: - "10010:10010"
4. Docker镜像仓库
4.1 配置Docker信任地址
我们的私服采用的是http协议,默认不被Docker信任
# 编辑Docker服务守护进程配置文件
vi /etc/docker/daemon.json
# 添加内容
"insecure-registries":["http://192.168.96.130:8080"]
# 重新加载Docker服务守护进程
systemctl daemon-reload
# 重启Docker
systemctl restart docker
4.2 使用Docker部署带有图形界面的DockerRegistry
version: '3.0'
services:
registry:
image: registry
volumes:
- ./registry-data:/var/lib/registry
ui:
image: joxit/docker-registry-ui:1.5-static
ports:
- 8080:80
environment:
- REGISTRY_TITLE=絷缘私有仓库
- REGISTRY_URL=http://registry:5000
depends_on:
- registry
mkdir /tmp/docker-registry-ui
cd /tmp/docker-registry-ui
touch docker-compose.yml
vim docker-compose.yml
docker-compose up -d
4.3 在私有镜像仓库推送/拉取镜像
# 将现有镜像打包成为私有镜像
docker tag nginx:latest 192.168.96.130:8080/nginx:latest
# 将私有镜像推送到私有仓库
docker push 192.168.96.130:8080/nginx:latest
# 将私有镜像拉取到当前环境
docker pull 192.168.96.130:8080/nginx:latest

第四天
九、消息队列RabbitMQ
同步调用的问题
微服务之间基于Feign的调用就属于同步方式,存在一些问题
耦合度高,每次加入新的需求,就需要修改原来的代码
阻塞调用,调用者需要等待提供者响应,调用链过长时等待时间相当于业务执行时间总和
资源浪费,调用者在等待过程中,不会释放请求占用的资源
级联失败:当调用链中有一个服务出现问题,name就会导致依赖于此服务的所有微服务发生故障
异步调用的方案
实现方式是事件驱动模式
优势
服务解耦
性能提升,吞吐量提高
故障隔离,服务之间没有强依赖,不担心级联失败
流量削峰
缺点
依赖于Broker的可靠性、安全性、吞吐能力
架构复杂了,业务之间没有明显的流程线,不好追踪管理
1. MQ(MessageQueue)
消息队列,字面来看就是存放消息的队列,也就是事件驱动架构中的Broker
2. RabbitMQ快速入门
RabbitMQ是基于Erlang语言开发的开源消息通信中间件
官网地址:https://rabbitmq.com/
2.1 安装RabbitMQ
2.1.1 下载镜像
docker pull rabbitmq:3-management
2.1.2 安装MQ
docker run \
-e RABBITMQ_DEFAULT_USER=zhiyuan \
-e RABBITMQ_DEFAULT_PASS=123456 \
--name my-mq \
--hostname mql \
-p 15672:15672 \
-p 5672:5672 \
-d \
rabbitmq:3-management
若浏览器无法访问
ip:15672,则按照下方操作即可:docker exec -it my-mq bash rabbitmq_plugins enable rabbitmq_management
2.2 概念解析

channel:操作MQ的工具
exchange:路由消息到队列中
queue:缓存消息
virtualhost:虚拟主机,是对queue、exchange等资源的逻辑分组
2.3 常见消息模型
2.3.1 基本消息队列(BasicQueue)

2.3.2 工作消息队列(Work Queue)
可以提高消息处理速度,避免队列消息堆积

2.3.3 发布订阅(Publish Subscribe)
发布订阅模式允许将同一消息发送给多个消费者,实现方式就是加入交换机exchange

Fanout Exchange:广播
Direct Exchange:路由
Topic Exchange:主题
3. SpringAMQP

3.1 入门案例:消息的发送
引入AMQP依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
在publisher服务中编写application.yml,添加mq连接信息:
spring:
rabbitmq:
host: 192.168.174.130
port: 5672
virtual-host: /
username: zhiyuan
password: 123456
在publisher服务中新建一个测试类,编写测试方法:
@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringAMQPTest{
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void testSimpleQueue(){
String queueName = "simple.queue";
String message = "Hello,SpringAMQP";
rabbitTemplate.covertAndSend(queueName,message);
}
}
3.2 入门案例:消息的接收
在consumer服务中编写application.yml,添加mq连接信息:
spring:
rabbitmq:
host: 192.168.174.130
port: 5672
virtual-host: /
username: zhiyuan
password: 123456
在consumer服务中新建一个类,编写消费逻辑:
@Component
public class SpringRabbitListener{
@RabbitListener(queues = {"simple.queue"})
public void listenSimpleQueueMessage(String msg) throws InterruptedException{
System.out.println("SpringConsumer接受到消息:【" + msg + "】");
}
}
注意事项:消息一旦被消费就会从队列中移除,RabbitMQ没有消息回溯功能
3.3 工作队列案例
基本思路
在publisher服务中定义测试方法,每秒产生50条消息发送到simple.queue
在consumer服务中定义两个消息监听者,都监听simple.queue
消费者1每秒钟处理50条消息,消费者2每秒钟处理10条消息
@Test public void testSendMessageToWorkQueue() throws InterruptedException { String queueName = "simple.queue"; String message = "Hello,SpringAMQP-MESSAGE_"; for (int i = 0; i < 50; i++) { rabbitTemplate.convertAndSend(queueName,message + i); Thread.sleep(20); } }@RabbitListener(queues = {"simple.queue"}) public void listenWorkQueueMessage1(String msg) throws InterruptedException { System.out.println("SpringConsumer1接收到消息“【" + msg + "】" + LocalTime.now()); Thread.sleep(20); }@RabbitListener(queues = {"simple.queue"}) public void listenWorkQueueMessage2(String msg) throws InterruptedException { System.err.println("SpringConsumer2接收到消息“【" + msg + "】" + LocalTime.now()); Thread.sleep(200); }我们发现一个现象,消费者1很快处理完消息后就停止处理了,而把所有的消息都交由速度较慢的消费者2,这是由于预取消息导致的,我们可以通过修改application.yml文件来限制预取消息的上限
spring: rabbitmq: host: 192.168.174.130 port: 5672 virtual-host: / username: zhiyuan password: 123456 listener: simple: prefetch: 1 # 每次只能获取一条消息,处理完成才能发获取下一条消息
3.4 发布订阅案例
3.4.1 Fanout Exchange:将接受到的消息路由到每一个与其绑定的queue

实现思路
在consumer服务中利用代码声明队列,交换机,并将二者绑定
在consumer服务中编写两个消费者方法,分别监听fanout.queue1和fanoput.queue2
在publisher服务中编写测试方法,向zhiyuan.fanout发送消息
@Configuration public class FanoutConfig { @Bean public FanoutExchange fanoutExchange(){ return new FanoutExchange("zhiyuan.fanout"); } @Bean public Queue fanoutQueue1(){ return new Queue("fanout.queue1"); } @Bean public Binding bindingQueue1(Queue fanoutQueue1,FanoutExchange fanoutExchange){ return BindingBuilder.bind(fanoutQueue1).to(fanoutExchange); } @Bean public Queue fanoutQueue2(){ return new Queue("fanout.queue2"); } @Bean public Binding bindingQueue2(Queue fanoutQueue2,FanoutExchange fanoutExchange){ return BindingBuilder.bind(fanoutQueue2).to(fanoutExchange); } }@RabbitListener(queues = {"fanout.queue1"}) public void listenFanoutQueue1(String msg) throws InterruptedException { System.err.println("SpringConsumer1接收到FanoutQueue1消息“【" + msg + "】" + LocalTime.now()); } @RabbitListener(queues = {"fanout.queue2"}) public void listenFanoutQueue2(String msg) throws InterruptedException { System.err.println("SpringConsumer2接收到FanoutQueue2消息“【" + msg + "】" + LocalTime.now()); }@Test public void testSendMessageToFanoutQueue() throws InterruptedException { String exchangeName = "zhiyuan.fanout"; String message = "Hello,Fanout EveryOne!"; rabbitTemplate.convertAndSend(exchangeName,"",message); }
3.4.2 Direct Exchange:将接受到的消息根据规则路由到指定的queue

实现思路
每一个queue都与Exchange设置一个BindingKey
发布者发送消息时,指定消息的RoutingKey
Exchange将消息路由到BindingKey与消息RoutingKey一致的队列
利用@RabbitListener声明Exchange、Queue、RoutingKey
在consumer服务中编写两个消费者方法,分别监听direct.queue1和direct.queue2
在publisher中编写测试方法,向zhiyuan.direct发送消息
@RabbitListener(bindings = @QueueBinding( value = @Queue(name = "direct.queue1"), exchange = @Exchange(name = "zhiyuan.direct",type = ExchangeTypes.DIRECT), key = {"red","blue"} )) public void listenDirectQueue1(String msg){ System.err.println("SpringConsumer接收到DirectQueue1消息“【" + msg + "】" + LocalTime.now()); } @RabbitListener(bindings = @QueueBinding( value = @Queue(name = "direct.queue2"), exchange = @Exchange(name = "zhiyuan.direct",type = ExchangeTypes.DIRECT), key = {"red","yellow"} )) public void listenDirectQueue2(String msg){ System.err.println("SpringConsumer接收到DirectQueue2消息“【" + msg + "】" + LocalTime.now()); }@Test public void testSendMessageToDirectQueue1() throws InterruptedException { String exchangeName = "zhiyuan.direct"; String message = "Hello,Direct key is blue"; rabbitTemplate.convertAndSend(exchangeName,"blue",message); } @Test public void testSendMessageToDirectQueue2() throws InterruptedException { String exchangeName = "zhiyuan.direct"; String message = "Hello,Direct key is yellow"; rabbitTemplate.convertAndSend(exchangeName,"yellow",message); } @Test public void testSendMessageToDirectQueue() throws InterruptedException { String exchangeName = "zhiyuan.direct"; String message = "Hello,Direct key is red"; rabbitTemplate.convertAndSend(exchangeName,"red",message); }
3.4.3 TopicExchange:与DirectExchange类似,区别在于routingKey必须是多个单词列表,并且以.分割
Queue和Exchange指定BindingKey时可以使用通配符
#:代指0个或多个单词*:代指一个单词

实现思路
利用@RabbitListener声明Exchange、Queue、RoutingKey
在consumer服务中编写两个消费者方法,分别监听topic.queue1和topic.queue2
在publisher中编写测试方法,向zhiyuan.topic发送消息
@RabbitListener(bindings = @QueueBinding( value = @Queue(name = "topic.queue1"), exchange = @Exchange(name = "zhiyuan.topic",type = ExchangeTypes.TOPIC), key = "china.#" )) public void listenTopicQueue1(String msg){ System.err.println("SpringConsumer接收到TopicQueue1消息“【" + msg + "】" + LocalTime.now()); } @RabbitListener(bindings = @QueueBinding( value = @Queue(name = "topic.queue2"), exchange = @Exchange(name = "zhiyuan.topic",type = ExchangeTypes.TOPIC), key = "#.news" )) public void listenTopicQueue2(String msg){ System.err.println("SpringConsumer接收到TopicQueue2消息“【" + msg + "】" + LocalTime.now()); }@Test public void testSendMessageToTopicQueue() throws InterruptedException { String exchangeName = "zhiyuan.topic"; String message = "Hello,Topic key is china.news"; rabbitTemplate.convertAndSend(exchangeName,"china.news",message); }
4.SpringAMQP消息转换器
在SpringAMQP的发送方法中,接收消息的类型时Object,也就是说我们可以发送任意对象类型的消息,SpringAMQP最终会帮我们序列化为字节后发送,
content-type为application/x-java-serialized-object,默认直接以对象方式传输是很不安全的而且消息很长
SpringAMQP对消息对象的处理是由
org.springframework.amqp.support.converter.MessageConverter完成的默认实现:
SimpleMessageConverter基于JDK的
ObjectOutputStream完成序列化如果要修改只需要定义一个MessageConverter类型的Bean即可
创建 object.queue 队列
@Bean public Queue objectQueue(){ return new Queue("object.queue"); }引入依赖
<dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> </dependency> <dependency> <groupId>com.fasterxml.jackson.dataformat</groupId> <artifactId>jackson-dataformat-xml</artifactId> <version>2.9.10</version> </dependency>在publisher、consumer服务声明MessageConverter
@Bean public MessageConverter jsonMessageConverter(){ return new Jackson2JsonMessageConverter(); }测试发送HashMap类型消息
@Test public void testSendObjectMessageToSimpleQueue(){ String queueName = "object.queue"; HashMap<String, String> map = new HashMap<>(); map.put("name","zhiyuan"); map.put("gender","male"); rabbitTemplate.convertAndSend(queueName,map); }测试接收HashMap类型消息
@RabbitListener(queues = "object.queue") public void listenObjectQueue(Map<String,String> msg){ System.out.println("收到消息:【" + msg + "】"); }
十、分布式搜索Elasticsearch
1. 初识elasticsearch
介绍:elasticsearch是一款非常强大的开源搜索引擎,可以帮助我们从海量数据中快速找到需要的内容
elasticsearch结合kibana、Logstash、Beats,也就是说elastic stack(ELK)被广泛应用于日志数据分析、实时监控等领域
elasticsearch是elastic stack的核心,负责存储、搜索、分析数据
elasticsearch的发展
Lucene是一个Java语言的搜索引擎类库,是Apache公司的顶级项目,由DougCutting于1999年研发
官网地址:https://lucene.apache.org
优势:
易扩展
高性能(基于倒排索引)
缺点:
只限于java语言开发
学习曲线陡峭
不支持水平扩展
相比于lucene,elasticsearch具有以下优势
支持分布式,可水平扩展
提供Restful接口,可被任何语言调用
搜索引擎技术排名
Elasticsearch:开源的分布式搜索引擎
Splunk:商业项目收费
Solr: Apache的开源搜索引擎
1.1. 正向索引和倒排索引
传统数据库采用正向索引
elaticsearch采用倒排索引
文档(document):每条数据就是一个文档
词条(term):文档按照语义分成的词语


1.2 文档
elasticsearch是面向文档存储的,可以是数据库中的一条商品数据,一个订单信息
文档数据会被序列化为json格式存储在elasticsearch中
1.3 索引
索引(index):相同类型文档的集合
映射(mapping):索引中文档的字段约束信息,类似表的结构约束

1.4 概念对比

MySQL擅长事务类型操作,可以确保数据的安全和一致性
Elasticsearch擅长海量数据的搜索、分析、计算

2. 安装Elasticsearch
2.1 创建网络
因为我们还需要部署kibana容器,因此需要让es和kibana容器互联,这里先创建一个网络
docker network create es-net
2.2 加载镜像
docker pull elasticsearch:7.12.1
2.3 运行
docker run -d \
--name es \
-e "ES_JAVA_OPTS=-Xms512m -Xmx512m" \
-e "discovery.type=single-node" \
-v es-data:/usr/share/elasticsearch/data \
-v es-plugins:/usr/share/elasticsearch/plugins \
--privileged \
--network es-net \
-p 9200:9200 \
-p 9300:9300 \
elasticsearch:7.12.1
命令解释:
-e "cluster.name=es-docker-cluster":设置集群名称
-e "ES_JAVA_OPTS=-Xms512m -Xmx512m":设置JAVA程序运行内存
-e "discovery.type=single-node":设置es为单例模式
--privileged:授予数据卷访问权
--network es-net:加入名为es-net的网络
2.4 部署kibana
docker pull kibana:7.12.1
docker run -d \
--name kibana \
-e ELASTICSEARCH_HOSTS=http://es:9200 \
--network=es-net \
-p 5601:5601 \
kibana:7.12.1

3. 分词器
es在创建倒排索引时需要对文档分词,在搜索时,需要对用户输入内容分词,但默认的分词规则对中文处理并不友好
POST /_analyze { "analyzer":"standard", "text":"神探狄仁杰" }
语法说明
POST:请求方式
/_analyze:请求路径,省略了
http://IP:9200,kibana会帮我们补充请求参数:JSON风格
analyzer:分词器类型,这里默认时standard分词器
text:要分词的内容
elasticsearch 的分词器对中文支持不好,一般我们会采用IK分词器
测试默认分词器效果
POST /_analyze { "text":"神探狄仁杰", "analyzer":"standard" }{ "tokens" : [ { "token" : "神", "start_offset" : 0, "end_offset" : 1, "type" : "<IDEOGRAPHIC>", "position" : 0 }, { "token" : "探", "start_offset" : 1, "end_offset" : 2, "type" : "<IDEOGRAPHIC>", "position" : 1 }, { "token" : "狄", "start_offset" : 2, "end_offset" : 3, "type" : "<IDEOGRAPHIC>", "position" : 2 }, { "token" : "仁", "start_offset" : 3, "end_offset" : 4, "type" : "<IDEOGRAPHIC>", "position" : 3 }, { "token" : "杰", "start_offset" : 4, "end_offset" : 5, "type" : "<IDEOGRAPHIC>", "position" : 4 } ] }
IK分词器:https://github.com/medcl/elasticsearch-analysis-ik
3.1 安装分词器
# 进入容器内部
docker exec -it elasticsearch /bin/bash
# 在线下载插件并安装
./bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.12.1/elasticsearch-analysis-ik-7.12.1.zip
# 退出
exit
# 重启容器
docker restart elasticsearch
在线安装可能会失败,解决办法离线安装
docker volume inspect es-plugins cd /var/lib/docker/volume/es-plugins/_data # 上传插件压缩包解压后目录 # 重启容器 docker restart es # 查看es日志 docker logs -f es
3.2 IK分词器
IK分词器包含两种模式
ik_smart:最少切分ik_max_word:最细切分
测试请求:
POST /_analyze
{
"text":"神探狄仁杰",
"analyzer":"ik_smart"
}
返回结果
{
"tokens" : [
{
"token" : "神",
"start_offset" : 0,
"end_offset" : 1,
"type" : "CN_CHAR",
"position" : 0
},
{
"token" : "探",
"start_offset" : 1,
"end_offset" : 2,
"type" : "CN_CHAR",
"position" : 1
},
{
"token" : "狄仁杰",
"start_offset" : 2,
"end_offset" : 5,
"type" : "CN_WORD",
"position" : 2
}
]
}
3.3 IK分词器的拓展和停用字典
要拓展IK分词器的词库,只需要修改一个IK分词器目录中config目录下的IKAnalyzer.cfg.xml文件
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
<comment>IK Analyzer扩展配置</comment>
<!-- 用户可以在这里配置自己的扩展字典 -->
<entry key="ext_dict">ext.dic</entry>
<!-- 用户可以在这里配置自己的扩展停止词典 -->
<entry key="ext_stopwords">stopword.dic</entry>
</properties>
在config目录下对应的字典文件中添加扩展词语,或创建自己的扩展字典加入扩展词语,词语以换行符分隔
4. 索引库操作
4.1 mapping属性
mapping是对索引库中文档的约束
type:字段数据类型,常见的简单类型有:
字符串:text(可分词的文本)关键字:keyword(精确值)
数值:long、integer、short、byte、double、float
布尔:boolean
日期:date
对象:object
index:是否创建索引,默认为true
analyzer:分词器
properties:某字段的子字段
4.2 创建索引库
ES中通过Restful请求操作索引库、文档。请求内容用DSL语句来表示
PUT /索引库名称
{
"mappings":{
"properties":{
"字段名1":{
"type":"text",
"analzyer":"ik_smart"
},
"字段名2":{
"type":"keyword",
"index":false
},
"字段名3":{
"properties":{
"子字段": {
"type":"keyword"
}
}
}
}
}
}
4.3 查看、删除索引库
查看索引库
GET /索引库名
删除索引库
DELETE /索引库名
4.4 修改索引库
索引库和mapping一旦创建无法修改,但是可以加入新的字段
PUT /索引库名/_mapping
{
"properties":{
"新字段名":{
"type":"integer"
}
}
}
5. 文档操作
5.1 新增文档
POST /索引库名/_doc/文档ID
{
"字段1":"值1",
"字段2":"值2",
"字段3":{
"子属性1":"值3",
"子属性2":"值4"
}
}
5.2 查询文档
GET /索引库名/_doc/文档ID
5.3 删除文档
DELETE /索引库名/_doc/文档ID
5.4 修改文档
5.4.1 全量修改
会删除旧文档,添加新文档
PUT /索引库名/_doc/文档ID
{
"字段1":"值1",
"字段2":"值2"
}
5.4.2 增量修改
只修改指定字段值
POST /索引库名/_update/文档ID
{
"doc":{
"字段名":"新值"
}
}
6. RestClient
ES中支持两种地理坐标类型:
geo_point:由纬度(latitude)和经度(longitude)确定的一个点
geo_shape:由多个geo_point组成的复杂几何图形
ES中字段拷贝可以使用copy_to属性将当前字段拷贝到指定字段
"all":{ "type":"text", "analyzer":"ik_max_word" }, "brand":{ "type":"keyword", "copy_to":"all" }PUT /hotel { "mappings":{ "properties": { "id":{ "type":"keyword" }, "name":{ "type":"text", "analyzer":"ik_max_word", "copy_to": "all" }, "address":{ "type":"keyword", "index":false }, "price":{ "type":"integer" }, "score":{ "type":"integer" }, "brand":{ "type":"keyword" }, "city":{ "type": "keyword" }, "starName":{ "type":"keyword" }, "business":{ "type":"keyword", "copy_to": "all" }, "location":{ "type":"geo_point" }, "pic":{ "type":"keyword", "index":false }, "all":{ "type":"text", "analyzer": "ik_max-word" } } } }
参不参与分词
"type":"keyword":不参与分词"type":"text":参与分词"analyzer":指定分词器
参不参与搜索:
"index":true:参与搜索"index":false:不参与搜索
多个字段均参与搜索:
添加一个新的字段,并将要参与搜索的字段copy_to新的字段
"新字段":{ "type":"text", "analyzer":"ik_max_word" }, "city":{ "type":"keyword", "copy_to":"新字段" }
6.1 初始化 JavaRestClient
引入依赖
<properties> <elasticsearch.version>7.12.1</elasticsearch.version> </properties><dependency> <groupId>org.elasticsearch.client</groupId> <artifactId>elasticsearch-rest-high-level-client</artifactId> <version>${elasticsearch.version}</version> </dependency>书写测试类
public class HotelIndexTest { private RestHighLevelClient client; @BeforeEach void setUp(){ this.client = new RestHighLevelClient(RestClient.builder( HttpHost.create("http://192.168.174.130:9200") )); } @AfterEach void tearDown() throws IOException{ this.client.close(); } @Test public void testInit(){ System.out.println(client); } }
6.2 RestClient实现索引库的CRUD
6.2.1 创建索引库
将DSL语句创建为常量使用,内容如下,需要去掉请求方式和请求地址,变成正常的JSON格式
{
"mappings":{
"properties": {
"id":{
"type":"keyword"
},
"name":{
"type":"text",
"analyzer":"ik_max_word",
"copy_to": "all"
},
"address":{
"type":"keyword",
"index":false
},
"price":{
"type":"integer"
},
"score":{
"type":"integer"
},
"brand":{
"type":"keyword"
},
"city":{
"type": "keyword"
},
"starName":{
"type":"keyword"
},
"business":{
"type":"keyword",
"copy_to": "all"
},
"location":{
"type":"geo_point"
},
"pic":{
"type":"keyword",
"index":false
},
"all":{
"type":"text",
"analyzer": "ik_max_word"
}
}
}
}
@Test
public void createHotelIndex() throws IOException {
//创建提交创建索引库请求的对象
CreateIndexRequest hotelRequest = new CreateIndexRequest("hotel");
//为请求对象设置请求DSL语句及请求格式
hotelRequest.source(HotelConstants.HOTEL_MAPPING_TEMPLATE, XContentType.JSON);
//创建索引库
client.indices().create(hotelRequest, RequestOptions.DEFAULT);
}
indices():此方法返回的对象中包含所有有关索引库的操作方法
6.2.2 删除索引库
@Test
public void deleteHotelIndex() throws IOException {
//创建提交删除索引库请求的对象
DeleteIndexRequest hotelRequest = new DeleteIndexRequest("hotel");
client.indices().delete(hotelRequest,RequestOptions.DEFAULT);
}
6.2.3 判断索引库是否存在
@Test
public void existsHotelIndex() throws IOException {
//创建提交查询索引库请求的对象
GetIndexRequest hotelRequest = new GetIndexRequest("hotel");
boolean exists = client.indices().exists(hotelRequest, RequestOptions.DEFAULT);
System.out.println(exists?"索引库已存在":"索引库不存在");
}
6.2.4 查询索引库
@Test
public void getHotelIndex() throws IOException{
//创建提交查询索引库请求的对象
GetIndexRequest hotelRequest = new GetIndexRequest("hotel");
GetIndexResponse hotelResponse = client.indices().get(hotelRequest, RequestOptions.DEFAULT);
Map<String, MappingMetadata> mappings = hotelResponse.getMappings();
ObjectMapper objectMapper = new ObjectMapper();
String mappingsJSON = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(mappings);
System.out.println(mappingsJSON);
}
6.3 RestClient实现文档的CRUD
6.3.1 创建文档
@Test
public void createDoc() throws IOException{
Hotel hotel = hotelService.getById(39106L);
IndexRequest indexRequest = new IndexRequest("hotel").id(hotel.getId().toString());
HotelDoc hotelDoc = new HotelDoc(hotel);
indexRequest.source(JSON.toJSONString(hotelDoc), XContentType.JSON);
client.index(indexRequest, RequestOptions.DEFAULT);
}
6.3.2 查询文档
@Test
public void getDoc() throws IOException{
GetRequest getRequest = new GetRequest("hotel").id("39106");
GetResponse response = client.get(getRequest, RequestOptions.DEFAULT);
System.out.println(JSON.toJSONString(response.getSource(),SerializerFeature.PrettyFormat));
}
6.3.3 删除文档
@Test
public void deleteDoc() throws IOException{
DeleteRequest deleteRequest = new DeleteRequest("hotel").id("39106");
client.delete(deleteRequest,RequestOptions.DEFAULT);
}
6.3.4 修改文档
全量更新与创建文档操作并无差别
增量更新
@Test
public void modifyDoc() throws IOException{
UpdateRequest updateRequest = new UpdateRequest("hotel","39106");
updateRequest.doc(
"price","350",
"starName","五钻"
).upsert(
"area","120"
);
client.update(updateRequest,RequestOptions.DEFAULT);
}
6.3.5 批量新增文档
@Test
public void testBulk() throws IOException{
BulkRequest bulkRequest = new BulkRequest();
List<Hotel> hotelList = hotelService.list();
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);
}
GET /hotel/_search
7. DSL语法
7.1 查询所有
match_all
示例
GET /indexName/_search
{
"query":{
"[queryType]":{
"[queryCondition|field]":"[conditionValue|value]"
}
}
}
实例
//查询所有
GET /hotel/_search
{
"query":{
"match_all":{}
}
}
7.2 全文检索
match_query
multi_match_query
示例
GET /indexName/_search
{
"query":{
"match":{
"[fieldName]":"[textValue]"
}
}
}
GET /indexName/_search
{
"query": {
"multi_match": {
"query": "[textValue]",
"fields": ["field1","field2","field3"...]
}
}
}
实例
GET /hotel/_search
{
"query": {
"match": {
"all": "外滩"
}
}
}
GET /hotel/_search
{
"query": {
"multi_match": {
"query": "如家",
"fields": ["brand","name","business"]
}
}
}
7.3 精确查询
ids
range:根据值的范围查询 gte:大于等于 lte:小于等于 gt:大于 lt:小于
term:根据词条精确值查询
示例
GET /indexName/_search
{
"query":{
"term":{
"[fieldName]":{
"value":"[value]"
}
}
}
}
GET /indexName/_search
{
"query":{
"range":{
"[fieldName]":{
"gte":[numValue],
"lte":[numValue]
}
}
}
}
实例
GET /hotel/_search
{
"query":{
"term": {
"city": {
"value": "上海"
}
}
}
}
GET /hotel/_search
{
"query": {
"range": {
"price": {
"gte": 100,
"lte": 300
}
}
}
}
7.4 地理查询
geo_distance
geo_bounding_box
示例
GET /indexName/_search
{
"query":{
"geo_distance":{
"distance":"15km",
"[fieldName]":"31.21,121.5"
}
}
}
实例
GET /hotel/_search
{
"query": {
"geo_distance":{
"distance":"15km",
"location":"31.21,121.5"
}
}
}
7.5 复合查询
bool
function_score:算分函数查询,可以控制文档相关性算分,控制文档排名
7.5.1 相关性算分查询

TF-IDF:在elasticsearch5.0之前,会随着词频增加而越来越大BM25:在elasticsearch5.0之后,会随着词频增加而增大,蛋增长曲线趋于水平示例
GET /indexName/_search
{
"query":{
"function_score":{
"query":{
"match":{
"all":"外滩"
}
},
"functions":[
{
"filter":{
"term":{
"id":"1"
}
},
"weight":10
}
],
"boost_mode":"multiply"
}
}
}

实例
//让如家品牌酒店排名更靠前一些
GET /hotel/_search
{
"query":{
"function_score":{
"query":{
"match":{
"all":"外滩"
}
},
"functions":[
{
"filter":{
"term":{
"brand":"如家"
}
},
"weight":2
}
],
"boost_mode":"sum"
}
}
}
7.5.2 布尔查询
布尔查询是一个或多个查询字句的组合,子查询的组合方式有如下:
must:必须匹配每个子查询,参与算分,类似
与should:选择性匹配子查询,参与算分,类似
或,must_not:必须不匹配,不参与算分,类似
非filter:必须匹配,不参与算分
实例
GET /hotel/_search
{
"query":{
"bool":{
"must":[
{"term":{"city":"上海"}}
],
"should":[
{"term":{"brand":"皇冠假日"}},
{"term":{"brand":"华美达"}}
],
"must_not":[
{"range":{"price":{"lte":500}}}
],
"filter":[
{"range":{"score":{"gte":45}}}
]
}
}
}
GET /hotel/_search
{
"query":{
"bool":{
"must":[
{"match":{"name":"如家"}}
],
"must_not":[
{"range":{"price":{"gt":400}}}
],
"filter":[
{"geo_distance":{"distance":"10km","location":{"lat":"31.21","lon":"121.5"}}
]
}
}
}
8. 排序
elasticsearch支持对搜索结果排序,默认是根据相关度算分(_score)来排序,可排序的字段类型有:
keyword
数值
地理坐标
日期类型
GET /indexName/_search
{
"query":{
"match_all":{}
},
"sort":[
{
"[fieldName]":"[desc|asc]"
}
]
}
GET /indexName/_search
{
"query":{
"match_all":{}
},
"sort":[
{
"_geo_distance":{
"location":"lat,lau",
"order":"[desc|asc]",
"unit":"km"
}
}
]
}
9. 分页
elasticsearch默认情况下只返回top10的数据,而如果要查询更多的数据就需要修改分页参数
from:分页开始的位置,默认为0
size:每页文档条数
GET /indexName/_search
{
"query":{
"match_all":{}
},
"from":100,
"size":20,
"sort":[
{"price":"asc"}
]
}
深度分页问题

ES是分布式的,所以会面临深度分页问题
首先在每个数据分片上都排序并查询前1000条文档
然后将所有节点的结果聚合,在内存中重新排序选出前1000条文档
如果搜索页数过深,或者结果集(from+size)越大,对内存和CPU的消耗也越高,所以ES设定结果集查询上限是10000
解决方案
search_after:分页时需要排序,原理是从上一次的排序值开始,查询下一页数据【官方推荐】
scroll:原理是将排序数据形成快照,保存在内存【官方不推荐】
10. 高亮
高亮就是在搜索结果中吧搜索关键字突出显示
高亮显示的原理
后端将搜索结果中的关键字用指定标签标记出来
前端对指定标签添加高亮样式
默认情况下,ES搜索字段必须与高亮字段一致,才会正常返回高亮字段内容,如果要改变默认情况,应该将
require_field_match改为false
GET /hotel/_search
{
"query":{
"match":{
"[fieldName]":"[textValue]"
}
},
"highlight":{
"fields":{
"[fieldName]":{
"pre_tags":"<em>",
"post_tags":"</em>",
"require_field_match":false
}
}
}
}
11. RestClient查询文档
11.1 查询所有
代码
@Test void testMatchAll() throws IOException { SearchRequest hotelRequest = new SearchRequest("hotel"); hotelRequest.source().query(QueryBuilders.matchAllQuery()); SearchResponse searchResponse = client.search(hotelRequest, RequestOptions.DEFAULT); SearchHits searchHits = searchResponse.getHits(); long total = searchHits.getTotalHits().value; System.out.println("共检索到" + total + "条数据"); SearchHit[] hits = searchHits.getHits(); for (SearchHit hit : hits) { System.out.println(JSON.toJSONString(hit.getSourceAsMap(), SerializerFeature.PrettyFormat)); } }DSL
GET /hotel/_search { "query":{ "match_all":{} } }RestAPI中构建DSL都是通过HighLevelRestClient中的source()方法实现的
11.2 全文检索
代码
@Test void testMatch() throws IOException { SearchRequest hotelRequest = new SearchRequest("hotel"); hotelRequest.source().query(QueryBuilders.matchQuery("all","如家")); SearchResponse searchResponse = client.search(hotelRequest, RequestOptions.DEFAULT); SearchHits searchHits = searchResponse.getHits(); long total = searchHits.getTotalHits().value; System.out.println("共检索到" + total + "条数据"); SearchHit[] hits = searchHits.getHits(); for (SearchHit hit : hits) { System.out.println(JSON.toJSONString(hit.getSourceAsMap(), SerializerFeature.PrettyFormat)); } } @Test void testMultiMatch() throws IOException{ SearchRequest hotelRequest = new SearchRequest("hotel"); hotelRequest.source().query(QueryBuilders.multiMatchQuery("如家","business","name")); SearchResponse searchResponse = client.search(hotelRequest, RequestOptions.DEFAULT); SearchHits searchHits = searchResponse.getHits(); long total = searchHits.getTotalHits().value; System.out.println("共检索到" + total + "条数据"); SearchHit[] hits = searchHits.getHits(); for (SearchHit hit : hits) { System.out.println(JSON.toJSONString(hit.getSourceAsMap(), SerializerFeature.PrettyFormat)); } }DSL
GET /hotel/_search { "query":{ "match":{ "all":"如家" } } } GET /hotel/_search { "query":{ "match":{ "query":"如家", "fields":["brand","name"] } } }
11.3 精确查询
代码
@Test void testTerm() throws IOException{ SearchRequest hotelRequest = new SearchRequest("hotel"); hotelRequest.source().query(QueryBuilders.termQuery("city","上海")); SearchResponse searchResponse = client.search(hotelRequest, RequestOptions.DEFAULT); SearchHits searchHits = searchResponse.getHits(); long total = searchHits.getTotalHits().value; System.out.println("共检索到" + total + "条数据"); SearchHit[] hits = searchHits.getHits(); for (SearchHit hit : hits) { System.out.println(JSON.toJSONString(hit.getSourceAsMap(), SerializerFeature.PrettyFormat)); } } @Test void testRange() throws IOException{ SearchRequest hotelRequest = new SearchRequest("hotel"); hotelRequest.source().query(QueryBuilders.rangeQuery("price").gte(150).lte(200)); SearchResponse searchResponse = client.search(hotelRequest, RequestOptions.DEFAULT); SearchHits searchHits = searchResponse.getHits(); long total = searchHits.getTotalHits().value; System.out.println("共检索到" + total + "条数据"); SearchHit[] hits = searchHits.getHits(); for (SearchHit hit : hits) { System.out.println(JSON.toJSONString(hit.getSourceAsMap(), SerializerFeature.PrettyFormat)); } }DSL
GET /hotel._search { "query":{ "term":{ "city":"上海" } } } GET /hotel/_search { "query":{ "range":{ "price":{ "gte":100, "lte":150 } } } }
11.4 地理查询
代码
@Test void testGeoDistance() throws IOException{ SearchRequest hotelRequest = new SearchRequest("hotel"); hotelRequest.source().query(QueryBuilders.geoDistanceQuery("location").distance("15km").point(31.21,121.5)); SearchResponse searchResponse = client.search(hotelRequest, RequestOptions.DEFAULT); SearchHits searchHits = searchResponse.getHits(); long total = searchHits.getTotalHits().value; System.out.println("共检索到" + total + "条数据"); SearchHit[] hits = searchHits.getHits(); for (SearchHit hit : hits) { System.out.println(JSON.toJSONString(hit.getSourceAsMap(), SerializerFeature.PrettyFormat)); } }DSL
GET /hotel/_search { "query": { "geo_distance":{ "distance":"15km", "location":"31.21,121.5" } } }
11.5 复合查询
代码
@Test void testBool() throws IOException{ SearchRequest searchRequest = new SearchRequest("hotel"); BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder(); boolQueryBuilder .must(QueryBuilders.termQuery("city","上海")) .filter(QueryBuilders.rangeQuery("price").gte(150).lte(200)); searchRequest.source().query(boolQueryBuilder); SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT); SearchHits searchHits = searchResponse.getHits(); long total = searchHits.getTotalHits().value; System.out.println("共检索到" + total + "条数据"); SearchHit[] hits = searchHits.getHits(); for (SearchHit hit : hits) { System.out.println(JSON.toJSONString(hit.getSourceAsMap(), SerializerFeature.PrettyFormat)); } }DSL
GET /hotel/_search { "query":{ "bool":{ "must":[ {"term":{"city":"上海"}} ], "should":[], "must_not":[], "filter":[ { "range":{ "price":{ "gte":150, "lte":200 } } } ] } } }
11.6 排序分页
代码
@Test void testPageAndSort() throws IOException{ SearchRequest searchRequest = new SearchRequest("hotel"); searchRequest.source() .query(QueryBuilders.matchAllQuery()) .from(0) .size(20) .sort("price", SortOrder.DESC); SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);SearchHits searchHits = searchResponse.getHits(); long total = searchHits.getTotalHits().value; System.out.println("共检索到" + total + "条数据"); SearchHit[] hits = searchHits.getHits(); for (SearchHit hit : hits) { System.out.println(JSON.toJSONString(hit.getSourceAsMap(), SerializerFeature.PrettyFormat)); } }DSL
GET /hotel/_search { "query":{ "match_all":{} }, "from":0, "size":20, "sort":[ { "price":"desc" } ] }
11.7 高亮文档
代码
@Test void testHighlight() throws IOException{ SearchRequest searchRequest = new SearchRequest("hotel"); HighlightBuilder highlightBuilder = new HighlightBuilder(); highlightBuilder.field("name") .requireFieldMatch(false) .preTags("<keyword-highlight>") .postTags("</keyword-highlight>"); searchRequest.source() .query(QueryBuilders.matchQuery("all","如家")) .highlighter(highlightBuilder); SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT); SearchHits searchHits = searchResponse.getHits(); long total = searchHits.getTotalHits().value; System.out.println("共检索到" + total + "条数据"); SearchHit[] hits = searchHits.getHits(); for (SearchHit hit : hits) { Map<String, HighlightField> highlightFields = hit.getHighlightFields(); if(highlightFields!=null && highlightFields.size()>0){ HighlightField highlightField = highlightFields.get("name"); System.out.println(highlightField.getFragments()[0].toString()); } System.out.println(JSON.toJSONString(hit.getSourceAsMap(), SerializerFeature.PrettyFormat)); } }DSL
GET /hotel/_search { "query":{ "match":{ "all":"如家" } }, "highlight":{ "fields":{ "name":{ "pre_tags":"<em>", "post_tags":"</em>", "require_field_match":false } } } }
12. 数据聚合
12.1 聚合的分类
12.2 DSL实现聚合
12.3 RestAPI实现聚合
13. 自动补全
原文作者:絷缘
作者邮箱:zhiyuanworkemail@163.com
原文地址:https://blog.zyblog.xyz/archives/wei-fu-wu-xue-xi
版权声明:本文为博主原创文章,转载请注明原文链接作者信息



