目录
NO.1 Eureka与Nacos注册中心、Ribbon负载均衡
2. 启动类中加入@EnableEurekaServer注解
4. 启动微服务,浏览器输入 http://127.0.0.1:10086
2. 服务注册:将user-service注册到eureka-server中去
3. 服务发现:向eureka-server拉取user-service的信息
NO.2 Nacos 配置管理、Feign 远程调用、Gateway 服务网关
1. 方式一:@Value注入的变量所在类上添加注解@RefreshScope
2. 方式二:@ConfigurationProperties注解代替@Value注解
4. nginx中修改conf/nginx.conf文件(反向代理)
6. 访问:输入 http://localhost/nacos 即可。
2. 在 order-service 的启动类添加注解开启 Feign 的功能
3. 新建 Client 包,在包下新建 UserClient 接口用来处理需要查询 User 库的请求
3. 启动服务,通过 http://localhost:10010/user/1 查询数据表。
1. 只需要利用java:8-alpine作为基础镜像(人家定好的)
4. 启动consumer会等待消息,启动publisher发送消息!
3. 为了测试,可以放到一个测试类HotelIndexTest中,利用@BeforeEach初始化
NO.1 Eureka与Nacos注册中心、Ribbon负载均衡
微服务是什么?
微服务可以理解为微服务架构。之前学的都是单体架构,将功能放到一个模块下,将业务的所有功能集中在一个项目中开发,打成一个包部署,这样一来对于大型企业开发,业务模块数量居多,导致于后期维护困难,此时就要进化为微服务架构,它可以将这些模块逐一拆分,方便后期维护。
Spring Cloud是什么?
微服务架构的实现每个企业都要有自己的方案,在Java领域,很出名的一种方案就是 Spring Cloud,他是一种微服务框架,集成了各种微服务功能组件,并基于SpringBoot实现了这些组件的自动装配。因此呢,我们在使用SpringCloud时,底层又是依赖于SpringBoot的,所以版本一定要兼容。
1. 注册RestTemplate
微服务架构中,各模块之间需要完成相互调用,当我们在 order 模块时需要通过商品 id 在 user 的数据库中查询数据时,该怎么办呢?
注册一个RestTemplate的实例到Spring容器
修改order-service服务中的OrderService类中的queryOrderById方法,根据Order对象中的userId查询User
将查询的User填充到Order对象,一起返回
代码实现:
// 将RestTemplate注册到spring容器
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
@Autowired
private RestTemplate restTemplate;
public Order queryOrderById(Long orderId) {
// 1.查询订单
Order order = orderMapper.findById(orderId);
// 2.查询用户
String url = "http://localhost:8081/user/" + order.getUserId();
User user = restTemplate.getForObject(url, User.class);
// 3.封装 user 信息
order.setUser(user);
// 4.返回
return order;
}
2. Eureka注册中心
user 作为服务提供者,如果他有多个服务提供者呢?比如8081,8082等都是userservice,此时我们服务消费者该如何去访问呢,又该访问那个呢?可以利用Eureka注册中心解决这个问题,下面是使用的步骤/原理:
当userservice被启动时,会去 eureka-server 注册中心注册服务信息,并且每 30 s都会向 eureka-server 注册中心发送心跳(存活状态)。
order-service服务向 eureka-server 注册中心拉取 userservice 服务的信息(端口号),拿到了地址信息,就会在底层会采用负载均衡算法进行远程调用
代码实现:
1. 创建一个独立的微服务:EurekaServer 服务
1. 创建 EurekaServer ,导入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
2. 启动类中加入@EnableEurekaServer注解
@EnableEurekaServer
3. 编写配置文件
server:
port: 10086 # Eureka 服务端口
spring:
application:
name: eureka-server # Eureka 服务名字
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka # Eureka 的访问路径
4. 启动微服务,浏览器输入 http://127.0.0.1:10086
2. 服务注册:将user-service注册到eureka-server中去
1. 引入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
2. 配置文件
spring:
application:
name: userservice
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
3. 启动多个 userservice 实例
在 Services 中右击一个服务,点击 Copy Config... 将 VM options 写上:-Dserver.port=8082;表示我们启动了另一个端口的 userservice 。
注意:如果没有 Services ,需要在 View 中 打开 Services,并且在配置模块里添加 spring boot。
3. 服务发现:向eureka-server拉取user-service的信息
1. 引入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
2. 配置文件
spring:
application:
name: orderservice
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
3. 服务拉取和负载均衡
给RestTemplate这个Bean添加一个@LoadBalanced注解;修改访问的url路径,用服务名代替ip、端口。通过结果我们会发现采用了负载均衡,并且是轮询的方式!为什么呢?
// 将RestTemplate注册到spring容器
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
@Autowired
private RestTemplate restTemplate;
public Order queryOrderById(Long orderId) {
// 1.查询订单
Order order = orderMapper.findById(orderId);
// 2.查询用户
String url = "http://userservice/user/" + order.getUserId();
User user = restTemplate.getForObject(url, User.class);
// 3.封装 user 信息
order.setUser(user);
// 4.返回
return order;
}
3. Ribbon负载均衡
添加了@LoadBalanced注解,即可实现负载均衡功能,这是什么原理呢?
原理是底层源码之间的互相转换,SpringCloudRibbon的底层采用了一个拦截器,拦截了RestTemplate发出的请求,对地址做了修改。 我们只要这要知道负载均衡的规则都定义在IRule接口中。
1. 自定义负载均衡策略
代码实现方式:
@Bean
public IRule randomRule(){
return new RandomRule();//随机
}
配置文件实现方式:
userservice: # 给某个微服务配置负载均衡规则,这里是userservice服务
ribbon:# 负载均衡规则
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
2. 饥饿加载
Ribbon默认是采用懒加载,即第一次访问时才会去创建LoadBalanceClient,请求时间会很长而饥饿加载则会在项目启动时创建,降低第一次访问的耗时,通过下面配置开启饥饿加载:。
ribbon:
eager-load:
enabled: true
clients: userservice
4. Nacos注册中心
SpringCloudAlibaba也推出了一个名为Nacos的注册中心。
Nacos 比 Eureka 更好用,差异:依赖不同、服务地址不同
1. Nacos注册中心的创建与使用
1. 引入依赖
在总的父工程引入依赖。
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.2.6.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
在user-service和order-service中的pom文件中引入nacos-discovery依赖:
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
2. 配置nacos地址
spring:
cloud:
nacos:
server-addr: localhost:8848
3. 登录nacos管理页面
通过 localhost:8848/nacos;默认本机账号密码=都为 nacos;
2. 集群
一个服务可以有多个实例,而我们又可以根据地域划分一下集群,即一个服务下有多个集群,一个集群下有多个实例,集群一般都是 HZ、SH、BJ等城市的名。接下来实现一下。
1. 给 user-service 添加集群
spring:
cloud:
nacos:
server-addr: localhost:8848
discovery:
cluster-name: HZ # 集群名称
2. 修改负载均衡规则
必须修改负载均衡规则,让他优先于自己所在的集群:
userservice:
ribbon:# 负载均衡规则
NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule
3. 权重
服务器设备性能有差异,部分实例所在机器性能较好,另一些较差,我们希望性能好的机器承担更多的用户请求;Nacos 提供了权重配置来控制访问频率,权重越大则访问频率越高
4. 环境隔离
在 Nacos 控制台可以创建 namespace ,用来隔离不同环境了,如果环境不同的话,会导致找不到userservice,控制台会报错。
修改 order-service 的 application.yml ,添加 namespace :
spring:
cloud:
nacos:
server-addr: localhost:8848
discovery:
cluster-name: HZ
namespace: 492a7d5d-237b-46a1-a99a-fa8e98e4b0f9 # 命名空间,填ID
5. Nacos与Eureka的区别
Nacos的服务实例分为两种类型:
临时实例:如果实例宕机超过一定时间,会从服务列表剔除,默认的类型。
非临时实例:如果实例宕机,不会从服务列表剔除,也可以叫永久实例。
Nacos与Eureka的区别
Nacos支持服务端主动检测提供者状态:临时实例采用心跳模式,非临时实例采用主动检测模式
临时实例心跳不正常会被剔除,非临时实例则不会被剔除
Nacos支持服务列表变更的消息推送模式,服务列表更新更及时
Nacos集群默认采用AP方式,当集群中存在非临时实例时,采用CP模式;Eureka采用AP方式
NO.2 Nacos 配置管理、Feign 远程调用、Gateway 服务网关
1. Nacos 配置管理
Nacos 不仅可以服务管理,管理每一个微服务,又可以统一管理管理配置文件。
1. 统一配置管理
我们可以在 Nacos 客户端进行加入配置管理
- 唯一名字:Data ID(一般为:服务名称-环境.yaml 或 服务名称.yaml)
- 指定分组:Group(一般为:默认即可)
- 配置格式:目前支持 YAML 和 Properties
对于读取配置文件,会优先读取 Nacos 配置的文件,但是一些 Nacos 的配置文件,比如端口号、服务名都写在了 Application.yaml 文件中,因此我们需要在读取Nacos 配置的文件之前就需要知晓端口号、服务名等引导信息,需要创建优先级高的 bootstrap.yml 文件。
实现步骤:
1. 引入 Nacos 的配置管理客户端依赖:
<!--nacos配置管理依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
2. 新建 bootstrap.yaml
可以看到填写的就是配置文件中的那三个参数:服务名称-环境.yaml
spring:
application:
name: userservice # 服务名称
profiles:
active: dev #开发环境,这里是dev
cloud:
nacos:
server-addr: localhost:8848 # Nacos地址
config:
file-extension: yaml # 文件后缀名
3. 读取配置信息
使用 @Value 读取文件
@Value("${pattern.dateformat}")
private String dateformat;
@GetMapping("now")
public String now(){
return LocalDateTime.now().format(DateTimeFormatter.ofPattern(dateformat));
}
2. 配置管理的热更新
1. 方式一:@Value注入的变量所在类上添加注解@RefreshScope
@RefreshScope
public class UserController {
@Value("${pattern.dateformat}")
private String dateformat;
}
2. 方式二:@ConfigurationProperties注解代替@Value注解
@Component
@Data
@ConfigurationProperties(prefix = "pattern")
public class PatternProperties {
private String dateformat;
}
3. 多环境配置共享
所谓多环境配置,就是无论什么环境都会读到这个配置文件,那么只需要在编写 Nacos 配置文件时编写:服务名称.yaml
4. 配置文件优先级
bootstrap.yaml > 服务名-环境.yaml > 服务名称.yaml > 本地配置
扩展:Nacos集群搭建
使用一个 Nacos 就是只有一个节点,当我们访问量大时,就有问题,需要多台 Nacos 来处理,此时就用到了Nacos集群搭建
步骤:
1. 新建Nacos数据库
Nacos默认数据存储在内嵌数据库Derby中,不属于生产可用的数据库。官方推荐的最佳实践是使用带有主从的高可用数据库集群。
2. 修改Nacos配置文件
- 进入nacos的conf目录,修改配置文件cluster.conf.example,重命名为cluster.conf
添加内容
127.0.0.1:8845
127.0.0.1.8846
127.0.0.1.8847- 修改application.properties文件,添加数据库配置
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=1234
3. 启动
- 将nacos文件夹复制三份,分别命名为:nacos1、nacos2、nacos3,表示三个节点
- 分别修改三个文件夹中的application.properties,端口号为8845、8846、8847
- 输入 startup.cmd 启动三个 nacos 节点
4. nginx中修改conf/nginx.conf文件(反向代理)
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;
}
}
5. 修改代码中application.yml文件配置:
spring:
cloud:
nacos:
server-addr: localhost:80 # Nacos地址
6. 访问:输入 http://localhost/nacos 即可。
2. Feign远程调用
1. Feign 替代 RestTemplate
问题:
之前我们的调用,采用 RestTemplate,因为 url 难以维护,我们需要一种方式,使得在代码中(OrderService)采用 Feign 客户端(Client)直接发送请求,使 UserService 查找。
步骤:
1. 引入依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
2. 在 order-service 的启动类添加注解开启 Feign 的功能
@EnableFeignClients()
public class OrderApplication {
}
3. 新建 Client 包,在包下新建 UserClient 接口用来处理需要查询 User 库的请求
@FeignClient("userservice")
public interface UserClient {
@GetMapping("/user/{id}")
User findById(@PathVariable("id") Long id);
}
注意:要与 此 findById() 要和 User 方法一致。
4. Feign 替代 RestTemplate
@Autowired
private UserClient userClient;
public Order queryOrderById(Long orderId) {
// 1.查询订单
Order order = orderMapper.findById(orderId);
// 2. 根据订单 id 查询用户
User user = userClient.findById(order.getUserId());
// 3. 向 order 中添加 user
order.setUser(user);
// 4.返回
return order;
}
2. Feign 的日志
1. 全局配置 - 配置文件:
feign:
client:
config:
default: #这里写default,则是针对所有微服务的配置
loggerLevel: BASIC #日志级别:仅打印路径
2. 局部配置 - 配置文件:
feign:
client:
config:
userservice: #这里写服务名称,则是针对某个微服务的配置
loggerLevel: BASIC #日志级别:仅打印路径
3. 全局配置 - java 代码方式
1. 声明bena
public class DefaultFeignConfiguration {
@Bean
public Logger.Level feignLogLevel(){
return Logger.Level.BASIC; // 日志级别为BASIC
}
}
2. 放到@EnableFeignClients注解中
@EnableFeignClients(defaultConfiguration = DefaultFeignConfiguration.class)
4. 局部配置 - java 代码方式
1. 声明bena
public class DefaultFeignConfiguration {
@Bean
public Logger.Level feignLogLevel(){
return Logger.Level.BASIC; // 日志级别为BASIC
}
}
2. 放到@FeignClient注解中
@FeignClient(value = "userservice", configuration = DefaultFeignConfiguration.class)
3. Feign 优化
Feign 底层的客户端实现:
• URLConnection :默认实现,不支持连接池
• Apache HttpClient :支持连接池
• OKHttp :支持连接池
优化手段:
① 使用连接池代替默认的 URLConnection
② 日志级别,最好用 basic 或 none
1. 连接池配置
导入依赖
<!--httpClient的依赖(Feign优化,使用连接池代替默认的URLConnection)-->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>
配置文件
feign: # feign相关的配置
httpclient: # 使用的连接池的这个配置
enabled: true # 开启feign对HttpClient的支持
max-connections: 200 #最大连接数
max-connections-per-route: 50 #每个路径的最大连接数
2. 抽取 FeignClient
这是FeignClient 对 UserService 发起的请求,我们将 feign 封装到一个模块下,这样,那个需要对 UserService 发起请求,直接加入 feign 的依赖就可了。
步骤:
1. 将 feign 的配置类、user类、客户端封装到 feign-api 中。
2. 删除 order-service 中feign 的配置类、user类、客户端。
3. 修改 order-service 中user的导包,需要导入 feign-api 中的。
4. 此时 SpringBootApplication 无法扫描到 FeignClient 以下两种方式:
1. @EnableFeignClients(basePackages ="cn.itcast.feign.clients")
2. @EnableFeignClients(clients = {UserClient.class})
3. Gateway 服务网关
网关的作用:
• 对用户请求做身份认证、权限校验
• 将用户请求路由到微服务,并实现负载均衡
• 对用户请求做限流
1. Gateway 快速用法
完成通过访问网关ip地址可以通过userId查询到user,并利用路由断言按照指定规则进行路径匹配(只要以/user/开头就符合要求),并且指定服务的负载均衡
1. 新建模块 gateway 导入依赖
<dependencies> <!--网关--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency> <!--nacos服务发现依赖--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> </dependencies>
2. 新建配置文件 application.yml
server: port: 10010 # 网关端口 spring: application: name: gateway # 服务名称 cloud: nacos: server-addr: localhost:8848 # nacos地址 gateway: routes: # 网关路由配置 - id: user-service # 路由id,自定义,只要唯一即可 # uri: http://127.0.0.1:8081 # 路由的目标地址 http就是固定地址 uri: lb://userservice # 路由的目标地址 lb就是负载均衡,后面跟服务名称 predicates: # 路由断言,也就是判断请求是否符合路由规则的条件 - Path=/user/** # 这个是按照路径匹配,只要以/user/开头就符合要求
3. 启动服务,通过 http://localhost:10010/user/1 查询数据表。
2. 断言工厂
我们在配置文件中写的断言规则只是字符串,这些字符串会被Predicate Factory读取并处理,转变为路由判断的条件。像这样的断言工厂在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 | 权重处理 |
3. 过滤器工厂
GatewayFilter是网关中提供的一种过滤器,可以对进入网关的请求和微服务返回的响应做处理
Spring提供了31种不同的路由过滤器工厂。例如:
名称 | 说明 |
---|---|
AddRequestHeader | 给当前请求添加一个请求头 |
RemoveRequestHeader | 移除请求中的一个请求头 |
AddResponseHeader | 给响应结果中添加一个响应头 |
RemoveResponseHeader | 从响应结果中移除有一个响应头 |
RequestRateLimiter | 限制请求的流量 |
案例:给所有进入userservice的请求添加一个请求头:Truth=itcast is freaking awesome!
修改gateway服务的application.yml文件,添加路由过滤
spring:
cloud:
gateway:
routes:
- id: user-service
uri: lb://userservice
predicates:
- Path=/user/**
filters: # 过滤器
- AddRequestHeader=Truth, my name is xz! # 添加请求头
此时,可以通过springboot提供的:@RequestHeader获取请求头
@GetMapping("/{id}")
public User queryById(@PathVariable("id") Long id,
//表示请求头名Truth,可以不传递此参数
@RequestHeader(value = "Truth", required = false) String name) {
System.out.println(name);
return userService.queryById(id);
}
默认过滤器:对所有的路由都生效
spring:
cloud:
gateway:
routes:
- id: user-service
uri: lb://userservice
predicates:
- Path=/user/**
default-filters: # 默认过滤项
- AddRequestHeader=Truth, my name is xz!
4. 全局过滤器
全局过滤器的作用也是处理一切进入网关的请求和微服务响应,与GatewayFilter的作用一样。区别在于GatewayFilter通过配置定义,处理逻辑是固定的;而GlobalFilter的逻辑需要自己写代码实现。定义方式是实现 GlobalFilter 接口。
案例演示:
定义全局过滤器,拦截请求,判断请求的参数是否满足下面条件:
参数中是否有authorization
authorization参数值是否为admin
@Component
@Order(-1) //越小优先级越高
public class AuthorizeFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 1.获取请求参数
MultiValueMap<String, String> queryParams = exchange.getRequest().getQueryParams();
// 2.获取authorization参数
String auth = queryParams.getFirst("authorization");
// 3.校验
if ("admin".equals(auth)) {
// 放行
return chain.filter(exchange);
}
// 4.拦截
// 4.1.禁止访问,设置状态码 404
exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
// 4.2.结束处理
return exchange.getResponse().setComplete();
}
}
指定了这么多过滤器,他们有时候应该先执行那个呢?
GlobalFilter通过实现Ordered接口,或者添加@Order注解来指定order值,由我们自己指定
路由过滤器和defaultFilter的order由Spring指定,默认是按照声明顺序从1递增。
当过滤器的order值一样时,会按照 defaultFilter > 路由过滤器 > GlobalFilter的顺序执行
5. 网关跨域问题
浏览器禁止请求的发起者与服务端发生跨域ajax请求,请求被浏览器拦截的问题。
我们userservice 可以访问到 orderservice 是因为没有 ajax 请求。
当浏览器有一个 ajax 请求到网关 loalhost:10010 会发生跨域问题。
解决方案:
添加配置(CORS):
spring:
cloud:
gateway:
globalcors: # 全局的跨域处理
add-to-simple-url-handler-mapping: true # 解决options请求被拦截问题
corsConfigurations:
'[/**]': #拦截所有路径
allowedOrigins: # 允许哪些网站的跨域请求
- "http://localhost:8090"
allowedMethods: # 允许的跨域ajax的请求方式
- "GET"
- "POST"
- "DELETE"
- "PUT"
- "OPTIONS"
allowedHeaders: "*" # 允许在请求中携带的头信息
allowCredentials: true # 是否允许携带cookie
maxAge: 360000 # 这次跨域检测的有效期
NO.3 Docker
大型项目组件较多,运行环境也较为复杂,部署时会碰到一些问题:
依赖关系复杂,容易出现兼容性问题
开发、测试、生产环境有差异
1. Docker概念
1. Docker解决依赖兼容问题
将应用的Libs(函数库)、Deps(依赖)、配置与应用一起打包
将每个应用放到一个隔离容器去运行,避免互相干扰
2. Docker解决操作系统环境差异
操作的结构分为:
- 计算机硬件:CPU、内存、磁盘。
- 内核:内核与计算机硬件交互,对外提供内核指令用于操作计算机硬件。
- 系统应用:操作系统有各种应用,对应着会有各自的函数库,函数库封装内核指令。
存在问题:
在Ubuntu版本的Linux操作系统中的Mysql安装到CentOS版本,Mysql会调用Ubuntu版本的函数库,导致无法找到。
Docker解决方案:
既然 系统应用 产生的各种函数库都不一致,那么在将Mysql部署的时候,直接也将Mysql的Ubuntu版本的函数库打包,让他自己处理后封装内核指令到Linux,就可以了。
3. Docker与虚拟机的区别
docker是一个系统进程;虚拟机是在操作系统中的操作系统
docker体积小、启动速度快、性能好;虚拟机体积大、启动速度慢、性能一般
4. Docker镜像和容器
镜像(Image):Docker将应用程序及其所需的依赖、函数库、环境、配置等文件打包在一起,称为镜像,这个文件包是只读的。
容器(Container):镜像中的应用程序运行后形成的进程就是容器,只是Docker会给容器进程做隔离,对外不可见。可以启动多次,形成多个容器进程。
5. DockerHub
开源应用程序非常多,打包这些应用往往是重复的劳动。为了避免这些重复劳动,人们就会将自己打包的应用镜像。
DockerHub:DockerHub是一个官方的Docker镜像的托管平台。这样的平台称为Docker Registry。
6. Docker结构:
Docker结构:
服务端:接收命令或远程请求,操作镜像或容器
客户端:发送命令或者请求到Docker服务端
7. Docker安装
1. 卸载旧版本的Docker
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
2. 安装docker
yum install -y yum-utils \
device-mapper-persistent-data \
lvm2 --skip-broken
3. 更新本地镜像源
# 设置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.repoyum makecache fast
4. 输入命令
yum install -y docker-ce
5. 关闭防火墙
Docker应用需要用到各种端口,逐一去修改防火墙设置。非常麻烦,练习需要关上。
# 关闭
systemctl stop firewalld
# 禁止开机启动防火墙
systemctl disable firewalld
6. 启动docker
systemctl start docker # 启动docker服务
systemctl stop docker # 停止docker服务
systemctl restart docker # 重启docker服务
7. 查看docker版本
docker -v
8. 配置国内镜像加速
2. Docker的操作
docker pull nginx | 指定最新版本nginx进行拉取nginx的镜像 |
docker images | 查看拉取到的镜像 |
docker save --help | 查看save命令用法 |
docker save -o nginx.tar nginx:latest | 将nginx镜像导出磁盘,起名为nginx.tar,可以用 load 加载 |
docker rmi nainx:latest | 删除nainx:latest镜像 |
docker load -i nginx.tar | 运行命令,加载本地文件 |
docker run --name mn -p 80:80 -d nginx | 创建并运行一个起的名字为 mn 的容器,指定宿主机端口80(左侧)与容器端口80(右侧)映射,并后台运行nginx此镜像 |
docker exec -it mn bash | 进入容器内部,执行 -it 命令,表示给mn这个容器创建一个标准输入、输出终端,允许我们与容器交互,bash表示进入容器后执行的命令 |
docker logs -f | 查看容器日志,添加 -f 参数可以持续查看日志 |
docker ps -a | 查看容器状态,加 -a 包括已经停止的 |
docker rm -f <containerid> | 删除指定id的容器 |
基本步骤:
1)docker pull nginx 拉取想用的镜像
2)docker run --name mn -p 80:80 -d nginx 运行一个容器,后台运行nginx镜像
3)docker exec -it mn bash 进入容器,可以执行命令。
在容器中想要修改一些镜像的东西,总是很麻烦,比如,我们需要进入nginx中,修改默认的主页面index.html,我们可以进入index.html的目录后使用命令:
sed -i -e 's#Welcome to nginx#xz,nginx中遨游吧!#g' -e 's#<head>#<head><meta charset="utf-8">#g' index.html
有什么办法吗?有!数据卷(容器数据管理)
3. 数据卷(容器数据管理)
数据卷(volume)是一个虚拟目录,指向宿主机文件系统中的某个目录。一旦完成数据卷挂载,对容器的一切操作都会作用在数据卷对应的宿主机目录了。比如将nginx与宿主机目录完成数据卷挂载,我们操作宿主机的/var/lib/docker/volumes/html目录,就等于操作容器内的/usr/share/nginx/html目录了
1. 数据集操作命令
docker volume [COMMAND] 表示对数据卷操作[COMMAND]如下命令
create | 创建一个volume |
inspect | 显示一个或多个volume的信息 |
ls | 列出所有的volume |
prune | 删除未使用的volume |
rm | 删除一个或多个指定的volume |
案例:创建一个数据卷,并查看数据卷在宿主机的目录位置
步骤:
- docker volume create html 创建一个数据卷
- docker volume ls 查看所有数据
- docker volume inspect html 查看数据卷的详细信息
- 就可以看到数据卷关联的宿主机目录
2. 实现挂载
1. 数据卷实现挂载
我们在创建容器时,可以通过 -v 参数来挂载一个数据卷到某个容器内目录,命令格式如下:
docker run \
--name mn \
-v html:/root/html \
-p 8080:80 \
nginx \
-v html:/root/html
:把html数据卷挂载到容器内的/root/html这个目录中
案例:创建一个nginx容器,修改容器内的html目录内的index.html内容
步骤:
- 创建容器并挂载数据卷到容器内的HTML目录
docker run --name mn -v html:/usr/share/nginx/html -p 80:80 -d nginx- 查看html数据卷所在位置
docker volume inspect html- 进入该目录
cd /var/lib/docker/volumes/html/_data- 修改文件
vi index.html
2. 宿主机直接挂载模式
容器不仅仅可以挂载数据卷,也可以直接挂载到宿主机目录上。
带数据卷模式:宿主机目录 --> 数据卷 ---> 容器内目录
直接挂载模式:宿主机目录 ---> 容器内目录
语法:
目录挂载与数据卷挂载的语法是类似的:
-v [宿主机目录]:[容器内目录]
-v [宿主机文件]:[容器内文件]
数据卷挂载耦合度低,由docker来管理目录,但是目录较深,不好找
目录挂载耦合度高,需要我们自己管理目录,不过目录容易寻找查看
4. Dockerfile自定义镜像
镜像是将应用程序及其需要的系统函数库、环境、配置、依赖打包而成。简单来说,镜像就是在系统函数库、运行环境基础上,添加应用程序文件、配置文件、依赖文件等组合,然后编写好启动脚本打包在一起形成的文件。
Dockerfile就是一个文本文件,其中包含一个个的指令(Instruction),用指令来说明要执行什么操作来构建镜像。每一个指令都会形成一层Layer。
1. 基于Ubuntu构建Java项目
案例:基于Ubuntu镜像构建一个新镜像,运行一个java项目
1. 准备Java项目、jdk8
2. 将Dockerfile编写以下内容:
# 指定基础镜像
FROM ubuntu:16.04
# 配置环境变量,JDK的安装目录
ENV JAVA_DIR=/usr/local# 拷贝jdk和java项目的包
COPY ./jdk8.tar.gz $JAVA_DIR/
COPY ./docker-demo.jar /tmp/app.jar# 安装JDK
RUN cd $JAVA_DIR \
&& tar -xf ./jdk8.tar.gz \
&& mv ./jdk1.8.0_144 ./java8# 配置环境变量
ENV JAVA_HOME=$JAVA_DIR/java8
ENV PATH=$PATH:$JAVA_HOME/bin# 暴露端口
EXPOSE 8090
# 入口,java项目的启动命令
ENTRYPOINT java -jar /tmp/app.jar
3. 将这三个文件上传至虚拟机,在它的目录里输入:
docker build -t javaweb:1.0 . 表示在当前文件夹下构建镜像
4. 启动容器,运行项目在浏览器ip地址
2. 基于java8构建Java项目
虽然我们可以基于Ubuntu基础镜像,添加任意自己需要的安装包,构建镜像,但是却比较麻烦。所以大多数情况下,我们都可以在一些安装了部分软件的基础镜像上做改造。
例如,构建java项目的镜像,可以在已经准备了JDK的基础镜像基础上构建。
案例:基于java8构建Java项目
1. 只需要利用java:8-alpine作为基础镜像(人家定好的)
2. 编写 Dockerfile 文件:
FROM java:8-alpine
COPY ./app.jar /tmp/app.jar # 左边的是项目打包名,右边是jdk8中名字
EXPOSE 8090
ENTRYPOINT java -jar /tmp/app.jar
3. 使用docker build命令构建镜像
4. 使用docker run创建容器并运行
5. Docker-Compose
我们发现,我们在运行项目时,每次都需要创建和运行容器,有没有什么可以解决?
Docker Compose可以基于Compose文件帮我们快速的部署分布式应用,而无需手动一个个创建和运行容器!
1. Docker-Compose的下载
Linux下需要通过命令下载:
1. 下载
curl -L https://github.com/docker/compose/releases/download/1.23.1/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose
2. 修改权限
chmod +x /usr/local/bin/docker-compose
3. Base自动补全命令:
curl -L https://raw.githubusercontent.com/docker/compose/1.29.1/contrib/completion/bash/docker-compose > /etc/bash_completion.d/docker-compose
如果出错:
echo "199.232.68.133 raw.githubusercontent.com" >> /etc/hosts
2. Docker-Compose的使用
我们在项目部署时,需要将mysql、nacos完成部署,Docker-Compose可以一步到位将他们一起放到容器中并运行
案例:将之前学习的cloud-demo微服务集群利用DockerCompose部署
1. Docker-Compose的配置文件
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"
nacos
:作为注册中心和配置中心
image: nacos/nacos-server
: 基于nacos/nacos-server镜像构建
environment
:环境变量
MODE: standalone
:单点模式启动
ports
:端口映射,这里暴露了8848端口
mysql
:数据库
image: mysql:5.7.25
:镜像版本是mysql:5.7.25
environment
:环境变量
MYSQL_ROOT_PASSWORD: 123
:设置数据库root账户的密码为123
volumes
:数据卷挂载,这里挂载了mysql的data、conf目录,其中有我提前准备好的数据
userservice
、orderservice
、gateway
:都是基于Dockerfile临时构建的
2. 将数据库、nacos地址都命名为docker-compose中的服务名
就是项目中访问路径 localhost 要改为 微服务名,比如访问的 localhost:3306 要改为 mysql:3306;localhost:8848 要改为 nacos:8848。
3. maven打包工具,将项目中的每个微服务都打包为app.jar
要有此 maven 配置:
<build>
<!-- 服务打包的最终名称 -->
<finalName>app</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
4. 将打包好的app.jar拷贝到cloud-demo中的每一个对应的子目录中
cloud-demo 项目每个模块都放至文件夹里,对应的模块有两个文件,一个为 app.jar 是对应的 jar 包,一个是 DockerFile 文件,
5. 将mysql打包
要提供 mysql 的 date 数据和配置 cnf
6. 将cloud-demo上传至虚拟机,利用 docker-compose up -d 来部署
进入cloud-demo目录,然后运行下面的命令:
docker-compose up -d
注意:有可能有bug,需要 docker restart 一下。
6. Docker镜像仓库
1. 搭建私有镜像仓库
搭建镜像仓库可以基于Docker官方提供的DockerRegistry来实现。
1. 简化版镜像仓库
Docker官方的Docker Registry是一个基础版本的Docker镜像仓库,具备仓库管理的完整功能,但是没有图形化界面。
搭建方式比较简单,命令如下:
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 可以查看当前私有镜像服务中包含的镜像
2. 带有图形化界面版本
使用DockerCompose部署带有图象界面的DockerRegistry,命令如下:
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. 配置Docker信任地址
我们的私服采用的是http协议,默认不被Docker信任,所以需要做一个配置:
# 打开要修改的文件
vi /etc/docker/daemon.json
# 添加内容:
"insecure-registries":["http://192.168.150.101:8080"]
# 重加载
systemctl daemon-reload
# 重启docker
systemctl restart docker
2. 推送、拉取镜像
推送镜像到私有镜像服务必须先tag,步骤如下:
① 重新tag本地镜像,名称前缀为私有仓库的地址:192.168.150.101:8080/
docker tag nginx:latest 192.168.150.101:8080/nginx:1.0
② 推送镜像
docker push 192.168.150.101:8080/nginx:1.0
③ 拉取镜像
docker pull 192.168.150.101:8080/nginx:1.0
NO.4 MQ
1. MQ概述
1. 同步和异步通讯
同步通讯:就像打电话,需要实时响应。
异步通讯:就像发邮件,不需要马上回复。
异步通讯:
我们以购买商品为例,用户支付后需要调用订单服务完成订单状态修改,调用物流服务,从仓库分配响应的库存并准备发货。
在事件模式中,支付服务是事件发布者(publisher),在支付完成后只需要发布一个支付成功的事件(event),事件中带上订单id。
订单服务和物流服务是事件订阅者(Consumer),订阅支付成功的事件,监听到事件后完成自己业务即可。
为了解除事件发布者与订阅者之间的耦合,两者并不是直接通信,而是有一个中间人(Broker)。发布者发布事件到Broker,不关心谁来订阅事件。订阅者从Broker订阅事件,不关心谁发来的消息。
2. 常见的MQ
几种常见MQ的对比:
RabbitMQ | ActiveMQ | RocketMQ | Kafka | |
---|---|---|---|---|
公司/社区 | Rabbit | Apache | 阿里 | Apache |
开发语言 | Erlang | Java | Java | Scala&Java |
协议支持 | AMQP,XMPP,SMTP,STOMP | OpenWire,STOMP,REST,XMPP,AMQP | 自定义协议 | 自定义协议 |
可用性 | 高 | 一般 | 高 | 高 |
单机吞吐量 | 一般 | 差 | 高 | 非常高 |
消息延迟 | 微秒级 | 毫秒级 | 毫秒级 | 毫秒以内 |
消息可靠性 | 高 | 一般 | 高 | 一般 |
追求可用性:Kafka、 RocketMQ 、RabbitMQ
追求可靠性:RabbitMQ、RocketMQ
追求吞吐能力:RocketMQ、Kafka
追求消息低延迟:RabbitMQ、Kafka
2. 使用RabbitMQ
1. 在虚拟机中安装 RabbitMQ
- docker pull rabbitmq:3-management # 在线拉取镜像
- docker load -i mq.tar # 虚拟机中加载镜像
- 执行命令运行MQ容器
docker run \
-e RABBITMQ_DEFAULT_USER=itcast \
-e RABBITMQ_DEFAULT_PASS=123321 \
--name mq \
--hostname mq1 \
-p 15672:15672 \
-p 5672:5672 \
-d \
rabbitmq:3-management
2. RabbitMQ的基本使用
案例:通过publisher和consumer实现消息的发送和订阅消息。
1. 启动MQ容器
2. 编写publisher
public class PublisherTest {
@Test
public void testSendMess() throws IOException, TimeoutException {
//1. 建立连接
ConnectionFactory connectionFactory = new ConnectionFactory();
//2. 设置:主机名、端口号、vhost、用户名、密码
connectionFactory.setHost("192.168.204.100");
connectionFactory.setPort(5672);
connectionFactory.setVirtualHost("/");
connectionFactory.setUsername("itcast");
connectionFactory.setPassword("123321");
//3. 建立连接
Connection connection = connectionFactory.newConnection();
//4. 创建通道Channel
Channel channel = connection.createChannel();
//5. 创建简单队列
String queueName = "simple.queue";
channel.queueDeclare(queueName, false, false, false, null);
//6. 发送消息
String message = "hello, xz";
channel.basicPublish("", queueName, null, message.getBytes());
System.out.println("发送成功");
//7. 关闭通道、连接
channel.close();
connection.close();
}
}
3. 编写consumer
public class ConsumerTest {
public static void main(String[] args){
//1. 建立连接
ConnectionFactory connectionFactory = new ConnectionFactory();
//2. 设置:主机名、端口号、vhost、用户名、密码
connectionFactory.setHost("192.168.204.100");
connectionFactory.setPort(5672);
connectionFactory.setVirtualHost("/");
connectionFactory.setUsername("itcast");
connectionFactory.setPassword("123321");
//3. 建立连接
Connection connection = null;
try {
connection = connectionFactory.newConnection();
//4. 创建通道Channel
Channel channel = connection.createChannel();
//5. 创建简单队列
String queueName = "simple.queue";
channel.queueDeclare(queueName, false, false, false, null);
//6. 订阅消息
channel.basicConsume(queueName, true, new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope,
AMQP.BasicProperties properties, byte[] body) {
//7. 处理消息
String message = new String(body);
System.out.println("接受到消息:【" + message + "】");
}
});
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("等待接收消息中");
}
}
4. 启动consumer会等待消息,启动publisher发送消息!
3. SpringAMQP
SpringAMQP是基于RabbitMQ封装的一套模板,并且还利用SpringBoot对其实现了自动装配,使用起来非常方便。SpringAMQP提供了三个功能:
自动声明队列、交换机及其绑定关系
基于注解的监听器模式,异步接收消息
封装了RabbitTemplate工具,用于发送消息
<!--AMQP依赖,包含RabbitMQ-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
1. BasicQueue 简单队列模型
官方的HelloWorld是基于最基础的消息队列模型来实现的,只包括三个角色:
- publisher:消息发布者,将消息发送到队列queue
- queue:消息队列,负责接受并缓存消息
- consumer:订阅队列,处理队列中的消息
1. 配置
配置MQ地址,在publisher服务和consumer服务的application.yml中添加配置
spring:
rabbitmq:
host: 192.168.150.101 # 主机名
port: 5672 # 端口
virtual-host: / # 虚拟主机
username: itcast # 用户名
password: 123321 # 密码
2. 编写 publisher 服务
@SpringBootTest
@RunWith(SpringRunner.class)
public class PublisherTest2 {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void testBasicQueue() {
//队列什么名
String queueName = "basic.queue";//注意:不会给创建队列
//什么消息
String message = "hello, xz!";
//发送
rabbitTemplate.convertAndSend(queueName, message);
}
}
此时可以测试,在浏览器输入:虚拟机名:5672 输入账号密码,此队列会有一条消息
3. 编写 consumer 服务
@Component
public class SpringRabbitLister {
@RabbitListener(queues = "basic.queue")
public void listenBasicQueueMessage(String msg){
System.out.println("消息为:【" + msg+ "】");
}
}
启动 consumer 服务,可以将队列消息打印出,并毁灭队列中存在的消息
4. 注意
注意:
- 在启动容器时,需要配上用户名密码与端口映射。
- rabbitTemplate.convertAndSend() 不支持创建队列,需要先有个队列。
2. WorkQueue 任务模型
刚刚模型会使消息约堆越多,无法及时处理。
解决方案:让多个消费者绑定到一个队列,共同消费队列中的消息。
1. 配置
跟上一步一样
2. 编写 publisher 服务
模拟生产者速度快,不停向队列发消息,迅速将队列堆满。
@Test
public void testWorkQueue() throws InterruptedException {
//队列什么名
String queueName = "basic.queue";//注意:不会给创建队列
//什么消息
String message = "hello, xz!";
//发送
for (int i = 0; i < 50; i++) {
rabbitTemplate.convertAndSend(queueName, message);
Thread.sleep(2);
}
}
3. 编写 consumer 服务
模拟有多个消费者,同时向队列订阅消息,各自打印各自的消息。
@RabbitListener(queues = "basic.queue")
public void listenWorkQueueMessage1(String msg) throws Exception {
System.out.println("Work1消息为:【" + msg+ "】" + LocalTime.now());
Thread.sleep(20);
}
@RabbitListener(queues = "basic.queue")
public void listenWorkQueueMessage2(String msg) throws Exception {
System.out.println("Work2消息为:【" + msg+ "】" + LocalTime.now());
Thread.sleep(200);
}
4. 结论:
可以看到Work1很快完成了自己的25条消息。Work2却在缓慢的处理自己的25条消息。
也就是说消息是平均分配给每个消费者,并没有考虑到消费者的处理能力。这样显然是有问题的。
解决方案:
spring:
rabbitmq:
listener:
simple:
prefetch: 1 # 每次只能获取一条消息,处理完成才能获取下一个消息
3. 发布/订阅,以下都属于此类别
比起上两个,在订阅模型中,多了一个exchange角色,而且过程略有变化:
Publisher:生产者,也就是要发送消息的程序,但是不再发送到队列中,而是发给(交换机)
Exchange:交换机。一方面,接收生产者发送的消息。另一方面,知道如何处理消息,例如递交给某个特别队列、递交给所有队列、或是将消息丢弃。到底如何操作,取决于Exchange的类型。Exchange有以下3种类型:
Fanout:广播,将消息交给所有绑定到交换机的队列
Direct:定向,把消息交给符合指定routing key 的队列
Topic:通配符,把消息交给符合routing pattern(路由模式) 的队列
Consumer:消费者,与以前一样,订阅队列,没有变化
Queue:消息队列也与以前一样,接收消息、缓存消息。
Exchange(交换机)只负责转发消息,不具备存储消息的能力,因此如果没有任何队列与Exchange绑定,或者没有符合路由规则的队列,那么消息会丢失!
4. Fanout 广播模式
在广播模式下,消息发送流程是这样的:
1) 可以有多个队列
2) 每个队列都要绑定到Exchange(交换机)
3) 生产者发送的消息,只能发送到交换机,交换机来决定要发给哪个队列,生产者无法决定
4) 交换机把消息发送给绑定过的所有队列
5) 订阅队列的消费者都能拿到消息
1. 声明交换机,两个队列,并进行绑定
@Configuration //不要忘记
public class FanoutConfig {
@Bean //交换机
public FanoutExchange fanoutExchange(){
return new FanoutExchange("itcast.fanout");
}
@Bean //队列1
public Queue fanoutQueue1(){
return new Queue("fanout.queue1");
}
@Bean //绑定已经返回的fanoutQueue1
public Binding bindingQueue1(Queue fanoutQueue1, FanoutExchange fanoutExchange){
return BindingBuilder.bind(fanoutQueue1).to(fanoutExchange);
}
@Bean //队列2
public Queue fanoutQueue2(){
return new Queue("fanout.queue2");
}
@Bean //绑定已经返回的fanoutQueue2
public Binding bindingQueue2(Queue fanoutQueue2, FanoutExchange fanoutExchange){
return BindingBuilder.bind(fanoutQueue2).to(fanoutExchange);
}
}
2. 编写publisher,向交换机发送消息
@Test
public void testFanoutQueue(){
// 交换机名称
String exchangeName = "itcast.fanout";
// 消息
String message = "hello, everyone!";
rabbitTemplate.convertAndSend(exchangeName, "", message);
}
3. 编写consumer,接受各个队列消息
@RabbitListener(queues = "fanout.queue1")
public void listenFanoutQueueMessage1(String msg) throws Exception {
System.out.println("Fanout1消息为:【" + msg+ "】");
}
@RabbitListener(queues = "fanout.queue2")
public void listenFanoutQueueMessage2(String msg) throws Exception {
System.out.println("Fanout2消息为:【" + msg+ "】");
}
4. 总结
交换机的作用是什么?
接收publisher发送的消息
将消息按照规则路由到与之绑定的队列
不能缓存消息,路由失败,消息丢失
FanoutExchange的会将消息路由到每个绑定的队列
声明队列、交换机、绑定关系的Bean是什么?
Queue
FanoutExchange
Binding
5. Direct
在某些场景下,我们希望不同的消息被不同的队列消费。这时就要用到Direct类型的Exchange。
队列与交换机的绑定,不能是任意绑定了,而是要指定一个
RoutingKey
(路由key)消息的发送方在 向 Exchange发送消息时,也必须指定消息的
RoutingKey
。Exchange不再把消息交给每一个绑定的队列,而是根据消息的
Routing Key
进行判断,只有队列的Routingkey
与消息的Routing key
完全一致,才会接收到消息
1. 基于注解生明队列和交换机
基于@Bean的方式声明队列和交换机比较麻烦,Spring还提供了基于注解方式来声明。
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "direct.queue1"),
exchange = @Exchange(name = "itcast.direct", type = ExchangeTypes.DIRECT),
key = {"red", "blue"}
))
public void listenDirectQueue1(String msg){
System.out.println("消费者接收到direct.queue1的消息:【" + msg + "】");
}
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "direct.queue2"),
exchange = @Exchange(name = "itcast.direct", type = ExchangeTypes.DIRECT),
key = {"red", "yellow"}
))
public void listenDirectQueue2(String msg){
System.out.println("消费者接收到direct.queue2的消息:【" + msg + "】");
}
2. 消息发送
@Test
public void testSendDirectExchange() {
// 交换机名称
String exchangeName = "itcast.direct";
// 消息
String message = "山东菏泽曹县!";
// 发送消息
rabbitTemplate.convertAndSend(exchangeName, "red", message);
}
3. 总结
描述下Direct交换机与Fanout交换机的差异?
Fanout交换机将消息路由给每一个与之绑定的队列
Direct交换机根据RoutingKey判断路由给哪个队列
如果多个队列具有相同的RoutingKey,则与Fanout功能类似
基于@RabbitListener注解声明队列和交换机有哪些常见注解?
@Queue
@Exchange
6. Topic
Topic
类型的Exchange
与Direct
相比,都是可以根据RoutingKey
把消息路由到不同的队列。只不过Topic
类型Exchange
可以让队列在绑定Routing key
的时候使用通配符!
Routingkey
一般都是有一个或多个单词组成,多个单词之间以”.”分割,例如:item.insert
通配符规则:
#
:匹配一个或多个词
*
:匹配恰好1个词举例:
item.#
:能够匹配item.spu.insert
或者item.spu
item.*
:只能匹配item.spu
1. 消息发送
@Test
public void testSendTopicExchange() {
// 交换机名称
String exchangeName = "itcast.topic";
// 消息
String message = "枉我大曹县!";
// 发送消息
rabbitTemplate.convertAndSend(exchangeName, "china.news", message);
}
2. 消息接收
注意交换机类型
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "topic.queue1"),
exchange = @Exchange(name = "itcast.topic", type = ExchangeTypes.TOPIC),
key = "china.#"
))
public void listenTopicQueue1(String msg){
System.out.println("消费者接收到topic.queue1的消息:【" + msg + "】");
}
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "topic.queue2"),
exchange = @Exchange(name = "itcast.topic", type = ExchangeTypes.TOPIC),
key = "#.news"
))
public void listenTopicQueue2(String msg){
System.out.println("消费者接收到topic.queue2的消息:【" + msg + "】");
}
3. 总结
描述下Direct交换机与Topic交换机的差异?
Topic交换机接收的消息RoutingKey必须是多个单词,以
**.**
分割Topic交换机与队列绑定时的bindingKey可以指定通配符
#
:代表0个或多个词
*
:代表1个词
7. 消息转换
传输过程中,不一定是文字,如果是Java对象,spring会消息序列化为字节发送给MQ,接受对象时,再回把字节反序列化成Java对象,所以有下问题:数据体积过大、可读性差等。
配置JSON转换器
显然,JDK序列化方式并不合适。我们希望消息体的体积更小、可读性更高,因此可以使用JSON方式来做序列化和反序列化。
在publisher和consumer两个服务中都引入依赖:
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
<version>2.9.10</version>
</dependency>
配置消息转换器。
在启动类中添加一个Bean即可:
@Bean
public MessageConverter jsonMessageConverter(){
return new Jackson2JsonMessageConverter();
}
NO. 5 分布式搜索引擎elasticsearch
1. elasticsearch基础
1. elasticsearch的概述
1. elasticsearch的作用
elasticsearch是一款非常强大的开源搜索引擎,具备非常多强大功能,可以帮助我们从海量数据中快速找到需要的内容。
2. ELK技术栈
elasticsearch结合kibana、Logstash、Beats,也就是elastic stack(ELK)。被广泛应用在日志数据分析、实时监控等领域,而elasticsearch是elastic stack的核心,负责存储、搜索、分析数据。
3. elasticsearch和lucene
elasticsearch底层是基于lucene来实现的。
Lucene是一个Java语言的搜索引擎类库,是Apache公司的顶级项目,由DougCutting于1999年研发。官网地址:Apache Lucene - Welcome to Apache Lucene 。
2. 倒排索引
倒排索引的概念是基于MySQL这样的正向索引而言的。
正向索引:
如果是根据id查询,那么直接走索引,查询速度非常快。但是逐行扫描,也就是全表扫描,随着数据量增加,其查询效率也会越来越低。当数据量达到数百万时,就是一场灾难。
1. 倒排索引两个非常重要的概念
文档(
Document
):用来搜索的数据,其中的每一条数据就是一个文档。例如一个网页、一个商品信息词条(
Term
):对文档数据或用户搜索数据,利用某种算法分词,得到的具备含义的词语就是词条。例如:我是中国人,就可以分为:我、是、中国人、中国、国人这样的几个词条
2. 创建倒排索引概念
创建倒排索引是对正向索引的一种特殊处理,流程如下:
将每一个文档的数据利用算法分词,得到一个个词条
创建表,每行数据包括词条、词条所在文档id、位置等信息
因为词条唯一性,可以给词条创建索引,例如hash表结构索引
3. 倒排索引执行流程
1)用户输入条件
"华为手机"
进行搜索。2)对用户输入内容分词,得到词条:
华为
、手机
。3)拿着词条在倒排索引中查找,可以得到包含词条的文档id:1、2、3。
4)拿着文档id到正向索引中查找具体文档。
4. 总结
正向索引是最传统的,根据id索引的方式。但根据词条查询时,必须先逐条获取每个文档,然后判断文档中是否包含所需要的词条,是根据文档找词条的过程。
而倒排索引则相反,是先找到用户要搜索的词条,根据词条得到保护词条的文档的id,然后根据id获取文档。是根据词条找文档的过程。
3. Mysql与ES
我们统一的把mysql与elasticsearch的概念做一下对比:
MySQL | Elasticsearch | 说明 |
---|---|---|
Table | Index | 索引(index),就是文档的集合,类似数据库的表(table) |
Row | Document | 文档(Document),就是一条条的数据,类似数据库中的行(Row),文档都是JSON格式 |
Column | Field | 字段(Field),就是JSON文档中的字段,类似数据库中的列(Column) |
Schema | Mapping | Mapping(映射)是索引中文档的约束,例如字段类型约束。类似数据库的表结构(Schema) |
SQL | DSL | DSL是elasticsearch提供的JSON风格的请求语句,用来操作elasticsearch,实现CRUD |
Mysql:擅长事务类型操作,可以确保数据的安全和一致性
Elasticsearch:擅长海量数据的搜索、分析、计算
4. 安装es、kibana
部署单点es
1. 创建网络
因为我们还需要部署kibana容器,因此需要让es和kibana容器互联。这里先创建一个网络:
docker network create es-net
2. 加载镜像
下载es镜像的tar包传到虚拟机中,然后运行命令加载即可:
docker load -i es.tar
3. 同理还有kibana
的tar包也需要这样做。
4. 运行
运行docker命令,部署单点es:
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 "http.host=0.0.0.0"
:监听的地址,可以外网访问
-e "ES_JAVA_OPTS=-Xms512m -Xmx512m"
:内存大小
-e "discovery.type=single-node"
:非集群模式
-v es-data:/usr/share/elasticsearch/data
:挂载逻辑卷,绑定es的数据目录
-v es-logs:/usr/share/elasticsearch/logs
:挂载逻辑卷,绑定es的日志目录
-v es-plugins:/usr/share/elasticsearch/plugins
:挂载逻辑卷,绑定es的插件目录
--privileged
:授予逻辑卷访问权
--network es-net
:加入一个名为es-net的网络中
-p 9200:9200
:端口映射配置
在浏览器中输入:http://192.168.204.100:9200/
5. kibana的部署
kibana可以给我们提供一个elasticsearch的可视化界面,便于我们学习。
运行docker命令,部署kibana
docker run -d \ --name kibana \ -e ELASTICSEARCH_HOSTS=http://es:9200 \ --network=es-net \ -p 5601:5601 \ kibana:7.12.1
解释:
--network es-net
:加入一个名为es-net的网络中,与elasticsearch在同一个网络中
-e ELASTICSEARCH_HOSTS=http://es:9200"
:设置elasticsearch的地址,因为kibana已经与elasticsearch在一个网络,因此可以用容器名直接访问elasticsearch
-p 5601:5601
:端口映射配置
kibana启动一般比较慢,需要多等待一会,可以通过输入http://192.168.204.100:5601/
5. 分词器
我们知道elasticsearch有时对一条数据可以设置的 index 属性,他就是表示这个字段会被查询要进行分词了,他会将词语分逐一分解,一个词语对应一个文档,用来快速查找。但针对于中文,他们的分词器并不能合理的拆分我们的中文词语,因此需要用到 IK 插件
1. 安装分词器(离线方式)
查看数据卷目录
安装插件需要知道elasticsearch的plugins目录位置,而我们用了数据卷挂载,因此需要查看elasticsearch的数据卷目录,通过下面命令查看:
docker volume inspect es-plugins解压缩分词器安装包并上传
将 ik 下载后,解压并重名名为:ik;并将此文件夹放置第一步的数据卷目录里重启容器
docker restart es
2. 测试使用
IK分词器包含两种模式:ik_smart:最少切分、ik_max_word:最细切分
通过启动的 kibana 客户端,在DevTools页面下可以输入DL语句进行测试
GET /_analyze
{
"analyzer": "ik_max_word",
"text": "山东菏泽曹县他来了"
}
可以发现,他将text拆分了很多
3. 扩展词词典
随着互联网的发展,“造词运动”也越发的频繁。出现了很多新的词语,在原有的词汇列表中并不存在。所以我们的词汇也需要不断的更新,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> </properties>
新建一个 ext.dic,可以参考config目录下复制一个配置文件进行修改
奥利给
哈拉少重启elasticsearch
docker restart es
3. 停用词词典
在互联网项目中,在网络间传输的速度很快,所以很多语言是不允许在网络上传递的,那么我们在搜索时也应该忽略一些词汇。IK分词器也提供了强大的停用词功能,让我们在索引时就直接忽略当前的停用词汇表中的内容。
步骤:
- 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>
- 在 stopword.dic 添加停用词
沙比- 重启服务
docker restart es
2. elasticsearch的索引库操作
索引库就类似数据库表,mapping映射就类似表的结构。我们要向es中存储数据,必须先创建“库”和“表”。这就像是在设计表结构。
1. mapping映射属性
mapping是对索引库中文档的约束,常见的mapping属性包括:
type:字段数据类型,常见的简单类型有:
字符串:text(可分词的文本)、keyword(精确值,例如:品牌、国家、ip地址)
数值:long、integer、short、byte、double、float
布尔:boolean
日期:date
对象:object
index:是否创建索引,默认为true
analyzer:使用哪种分词器
properties:该字段的子字段
2. 索引库的增删改查
1. 添加索引库
#! 放到test1,在mappings里制定约束,
#! properties为它的子字段增加数据
PUT /test1
{
"mappings": {
"properties": {
"name":{
"type": "text",
"analyzer": "ik_smart"
},
"age":{
"type": "integer"
}
}
}
}
2. 查询索引库
GET /test1
3. 修改索引库(只能添加)
PUT /test1/_mapping
{
"properties":{
"many":{
"type": "keyword"
}
}
}
4. 删除索引库
DELETE /test1
3. elasticsearch的文档操作
文档是真实的数据,代表的是每一条信息
1. 新增
POST /test1/_doc/1
{
"name": "天霸动霸tua",
"age": 21,
"many": {
"hoppy": "喜欢篮球",
"desc": "一个公正的人"
}
}
2. 查询
GET /test1/_doc/1
3. 修改
1. 全量修改
全量修改是覆盖原来的文档,其本质是:
根据指定的id删除文档
新增一个相同id的文档
PUT /test1/_doc/1
{
"name": "天霸tua",
"age": 21,
"many": {
"hoppy": "喜欢乒乓球",
"desc": "不公正"
}
}}
2. 增量修改
增量修改是只修改指定id匹配的文档中的部分字段。
POST /test1/_update/1
{
"doc": {
"name": "动霸tua"
}
}
4. 删除
DELETE /test1/_doc/1
4. RestAPI - 操作索引库
ES官方提供了各种不同语言的客户端,用来操作ES。这些客户端的本质就是组装DSL语句,通过http请求发送给ES。
我们学习的是Java HighLevel Rest Client客户端API
1. 初始化操作
1. 引入依赖
1)引入es的RestHighLevelClient依赖: <dependency> <groupId>org.elasticsearch.client</groupId> <artifactId>elasticsearch-rest-high-level-client</artifactId> </dependency> 2)会出错,因为SpringBoot默认的ES版本是7.6.2,所以我们需要覆盖默认的ES版本: <properties> <java.version>1.8</java.version> <elasticsearch.version>7.12.1</elasticsearch.version> </properties>
2. 初始化RestHighLevelClient
RestHighLevelClient client = new RestHighLevelClient(
RestClient.builder(HttpHost.create("http://192.168.150.101:9200") ));
3. 为了测试,可以放到一个测试类HotelIndexTest中,利用@BeforeEach初始化
public class HotelIndexTest {
private RestHighLevelClient client;
@BeforeEach
void setUp() {
this.client = new RestHighLevelClient(RestClient.builder(
HttpHost.create("http://192.168.204.100:9200")
));
}
@AfterEach
void tearDown() throws IOException {
this.client.close();
}
}
注意:使用@BeforeEach需要引入:
<dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-engine</artifactId> </dependency>
2. 创建索引库
注意 CreateIndexRequest 的导包:
import org.elasticsearch.client.indices.CreateIndexRequest;
MAPPING_TEMPLATE是一个自己定义的常量,DL语句,一般都放到constants包下表示常量。
@Test
void createHotelIndex() throws IOException {
// 1.创建Request对象,索引库名字hotel
CreateIndexRequest request = new CreateIndexRequest("hotel");
// 2.准备请求的参数:一个是DSL语句,一个是JSON类型
request.source(MAPPING_TEMPLATE, XContentType.JSON);
// 3.发送请求,indices返回对象中包含索引库操作的所有方法
client.indices().create(request, RequestOptions.DEFAULT);
}
3. 删除索引库
@Test
void testDeleteHotelIndex() throws IOException {
// 1.创建Request对象
DeleteIndexRequest request = new DeleteIndexRequest("hotel");
// 2.发送请求
client.indices().delete(request, RequestOptions.DEFAULT);
}
4. 判断索引库是否存在
断索引库是否存在,本质就是查询,对应的DSL是: GET /hotel
@Test
void testExistsHotelIndex() throws IOException {
// 1.创建Request对象
GetIndexRequest request = new GetIndexRequest("hotel");
// 2.发送请求
boolean exists = client.indices().exists(request, RequestOptions.DEFAULT);
// 3.输出
System.err.println(exists ? "索引库已经存在!" : "索引库不存在!");
}
5. RestAPI - 操作文档
为了与索引库操作分离,我们再次参加一个测试类,做两件事情:
初始化RestHighLevelClient
我们的酒店数据在数据库,需要利用IHotelService去查询,所以注入这个接口
1. 初始化操作
- 要编写pojo实体类,与索引库所对应,如果遇到两两数据合并的,要写一个Doc,比如索引库一个字段 many 有两个值 hoppy 和 desc,doc中要写构造方法合并后与之对应
- 依赖什么的已在操作索引库演示完毕
2. 新增文档
新增文档的DSL语句如下:
POST /{索引库名}/_doc/1
{
"name": "Jack",
"age": 21
}我们导入酒店数据,基本流程一致,但是需要考虑几点变化:
酒店数据来自于数据库,我们需要先查询出来,得到hotel对象
hotel对象需要转为HotelDoc对象
HotelDoc需要序列化为json格式
@Test
void testAddDocument() throws IOException {
// 1.根据id查询酒店数据
Hotel hotel = hotelService.getById(61083L);
// 2.转换为文档类型
HotelDoc hotelDoc = new HotelDoc(hotel);
// 3.将HotelDoc转json
String json = JSON.toJSONString(hotelDoc);
// 1.准备Request对象
IndexRequest request = new IndexRequest("hotel").id(hotelDoc.getId().toString());
// 2.准备Json文档
request.source(json, XContentType.JSON);
// 3.发送请求
client.index(request, RequestOptions.DEFAULT);
}
因此,代码整体步骤如下:
1)根据id查询酒店数据Hotel
2)将Hotel封装为HotelDoc
3)将HotelDoc序列化为JSON
4)创建IndexRequest,指定索引库名和id
5)准备请求参数,也就是JSON文档
6)发送请求
3. 查询文档
查询的DSL语句如下: GET /hotel/_doc/{id}
准备Request对象
发送请求
@Test
void testGetDocumentById() throws IOException {
// 1.准备Request
GetRequest request = new GetRequest("hotel", "61082");
// 2.发送请求,得到响应
GetResponse response = client.get(request, RequestOptions.DEFAULT);
// 3.解析响应结果
String json = response.getSourceAsString();
HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
System.out.println(hotelDoc);
}
与之前类似,也是三步走:
1)准备Request对象。这次是查询,所以是GetRequest
2)发送请求,得到结果。因为是查询,这里调用client.get()方法
3)解析结果,就是对JSON做反序列化
4. 删除文档
删除的DSL为是这样的: DELETE /hotel/_doc/{id}
与查询相比,仅仅是请求方式从DELETE变成GET,可以想象Java代码应该依然是三步走:
1)准备Request对象,因为是删除,这次是DeleteRequest对象。要指定索引库名和id
2)准备参数,无参
3)发送请求。因为是删除,所以是client.delete()方法
@Test
void testDeleteDocument() throws IOException {
// 1.准备Request
DeleteRequest request = new DeleteRequest("hotel", "61083");
// 2.发送请求
client.delete(request, RequestOptions.DEFAULT);
}
5. 修改文档
如果新增时,ID已经存在,则修改;如果新增时,ID不存在,则新增
与之前类似,也是三步走:
1)准备Request对象。这次是修改,所以是UpdateRequest
2)准备参数。也就是JSON文档,里面包含要修改的字段
3)更新文档。这里调用client.update()方法
@Test
void testUpdateDocument() throws IOException {
// 1.准备Request
UpdateRequest request = new UpdateRequest("hotel", "61083");
// 2.准备请求参数
request.doc(
"price", "952",
"starName", "四钻"
);
// 3.发送请求
client.update(request, RequestOptions.DEFAULT);
}
6. RestAPI - 批量导入数据
利用mybatis-plus查询酒店数据
将查询到的酒店数据(Hotel)转换为文档类型数据(HotelDoc)
利用JavaRestClient中的BulkRequest批处理,实现批量新增文档
@Test
void testBulkRequest() throws IOException {
// 批量查询酒店数据
List<Hotel> hotels = hotelService.list();
// 1.创建Request
BulkRequest request = new BulkRequest();
// 2.准备参数,添加多个新增的Request
for (Hotel hotel : hotels) {
// 2.1.转换为文档类型HotelDoc
HotelDoc hotelDoc = new HotelDoc(hotel);
// 2.2.创建新增文档的Request对象
request.add(new IndexRequest("hotel")
.id(hotelDoc.getId().toString())
.source(JSON.toJSONString(hotelDoc), XContentType.JSON));
}
// 3.发送请求
client.bulk(request, RequestOptions.DEFAULT);
}
1)创建Request对象。这里是BulkRequest
2)准备参数。批处理的参数,就是其它Request对象,这里就是多个IndexRequest
3)发起请求。这里是批处理,调用的方法为client.bulk()方法