微服务是一种经过良好架构设计的分布式架构方案,微服务架构特征:
微服务这种方案需要技术框架来落地,全球的互联网公司都在积极尝试自己的微服务落地技术。在国内最知名的就是SpringCloud和阿里巴巴的Dubbo。
Dubbo | SpringCloud | SpringCloudAlibaba | |
---|---|---|---|
注册中心 | zookeeper、Redis | Eureka、Consul | Nacos、Eureka |
服务远程调佣 | Dubbo协议 | Feign(HTTP协议) | Dubbo、Feign |
配置中心 | 无 | SpringCloudConfig | SpringCloudConfig、Nacos |
服务网关 | 无 | SpringCloudGateway、Zuul | SpringCloudGateway、Zuul |
服务监控和保护 | dubbo-admin,功能弱 | Hystix | Sentinel |
服务拆分注意事项:
创建工程
创建数据库:
CREATE DATABASE cloud-order;
USE `cloud-order`;
DROP TABLE IF EXISTS `tb_order`;
CREATE TABLE `tb_order` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '订单id',
`user_id` bigint(20) NOT NULL COMMENT '用户id',
`name` varchar(100) DEFAULT NULL COMMENT '商品名称',
`price` bigint(20) NOT NULL COMMENT '商品价格',
`num` int(10) DEFAULT '0' COMMENT '商品数量',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `username` (`name`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=109 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT;
insert into `tb_order`(`id`,`user_id`,`name`,`price`,`num`) values (101,1,'Apple 苹果 iPhone 12 ',699900,1),(102,2,'雅迪 yadea 新国标电动车',209900,1),(103,3,'骆驼(CAMEL)休闲运动鞋女',43900,1),(104,4,'小米10 双模5G 骁龙865',359900,1),(105,5,'OPPO Reno3 Pro 双模5G 视频双防抖',299900,1),(106,6,'美的(Midea) 新能效 冷静星II ',544900,1),(107,2,'西昊/SIHOO 人体工学电脑椅子',79900,1),(108,3,'梵班(FAMDBANN)休闲男鞋',31900,1);
CREATE DATABASE `cloud-user` ;
USE `cloud-user`;
DROP TABLE IF EXISTS `tb_user`;
CREATE TABLE `tb_user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`username` varchar(100) DEFAULT NULL COMMENT '收件人',
`address` varchar(255) DEFAULT NULL COMMENT '地址',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `username` (`username`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=109 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT;
insert into `tb_user`(`id`,`username`,`address`) values (1,'柳岩','湖南省衡阳市'),(2,'文二狗','陕西省西安市'),(3,'华沉鱼','湖北省十堰市'),(4,'张必沉','天津市'),(5,'郑爽爽','辽宁省沈阳市大东区'),(6,'范兵兵','山东省青岛市');
导入项目
注册RestTemplate:在order-service的OrderApplication中注册RestTemplate (配置类
)
/**
* 创建RestTemplate并注入Spring容器
* @return
*/
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
注入RestTemplate
@Autowired
private RestTemplate restTemplate;
发送请求
public Order queryOrderById(Long orderId) {
// 1.查询订单
Order order = orderMapper.findById(orderId);
// 2.利用RestTemplate发起HTTP请求,查询用户
// 2.1.url路径
String url = "http://localhost:8081/user/"+order.getUserId();
// 2.2.发送HTTP请求,实现远程调用
User user = restTemplate.getForObject(url, User.class);
// 3. 封装user到Order
order.setUser(user);
// 4.返回
return order;
}
提供者与消费者
搭建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: #Eureka的地址信息
defaultZone: http://127.0.0.1:10086/eureka
将user-service服务注册到EurekaServer
在user-server项目引入 spring-cloud-starter-netflix-eureka-client
的依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
在application.yml文件,编写下面配置
server:
port: 10086 #服务端口
spring:
application:
name: userservice #服务名称
eureka:
client:
service-url: #Eureka的地址信息
defaultZone: http://127.0.0.1:10086/eureka
服务拉取是基于服务名称获取服务列表,然后在对服务列表做负载均衡
修改OrderService的代码,修改访问的路径,用服务名代替ip、端口:
在order-service项目的启动类OrderApplication中的RestTemplate添加 负载均衡 注解: @LoadBalanced
Ribbon的负载均衡规则是一个叫做IRule的接口来定义的,每一个子接口都是一种规则:
负载均衡策略:
内置负载均衡规则类 | 规则描述 |
---|---|
RoundRobinRule | 简单轮询服务列表来选择服务器。它是Ribbon默认的负载均衡规则 |
AvailabilityFilteringRule | 对以下两种服务器进行忽略: 1、在默认情况下,这台服务器如果3次连接失败,这台服务器就会被设置为“短路”状态。短路状态将持续30秒,如果再次连接失败,短路的持续时间就会几何级地增加 2、并发数过高的服务器。如果一个服务器的并发连接数过高,配置了AVailabilityFilteringRule规则的客户端也会将其忽略。并发连接数的上限,可以由客户的 |
WeightedResponseTimeRule | 为每一个服务器赋予一个权重值。服务器响应时间越长,这个服务器的权重就越小。这个规则会随机选择一个服务器,这个权重值会影响服务器的选择。 |
ZoneAvoidanceRule |
以区域可用的服务器为基础进行服务器的选择。使用Zone对服务器进行分类,这个Zone可以理解为一个机房、一个机架等。然后再对Zone内的多个服务做轮询 |
BestAvailableRule | 忽略哪些短路的服务器,并选择并发数较低的服务器 |
RandomRule | 随机选择一个可用的服务器 |
RetryRule | 重试机制的选择逻辑 |
通过定义IRule实现可以修改负载均衡规则,有两种方式:
代码方式(全局):在order-service中的OrderApplication类(配置类
)中,定义一个新的IRule:
@Bean
public IRule randomRule(){
return new RandomRule();
}
配置文件方式:在order-service的application.yml
userservice:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule #负载均衡规则
Ribbon默认是采用懒加载,即第一次访问时才会去创建LoadBalanceClient,请求时间会很长
而饥饿加载则会在项目启动时创建,降低第一次访问的耗时,通过下面配置开启饥饿加载:
ribbon:
eager-load:
enabled: true #开启饥饿加载
clients: userservice #指定对userservice这个服务饥饿加载,这里可以添加多个服务(采用数组的方式)
Nacos是阿里巴巴产品,现在是SpringCloud中的一个组件。相比Eureka功能更加丰富,在国内受欢迎程度较高
官网下载地址
Windows启动命令:
startup.cmd -m standalone
在cloud-demo父工程中添加 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>
注释掉order-service和user-service中原有的Eureka依赖
添加nacos的客户端依赖
<!--nacos客户端-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
修改user-service&order-service中的application.yml,注释Eureka地址,添加nacos地址
spring:
cloud:
nacos:
discovery:
server-addr: localhost:8848 #nacos 服务端地址
服务集群属性
修改application.yml,添加如下内容
spring:
cloud:
nacos:
discovery:
cluster-name: GZ #配置集群名称,也就是机房位置,例如:GZ,贵州
在order-service中设置负载均衡的IRule为NacosRule,这个规则优先会寻找与自己同集群的服务
userservice:
ribbon:
NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule
根据权重负载均衡
Nacos中服务存储和数据存储的最外层都是一个名为namespace的东西,用来做最外层隔离
修改order-service的application.yml,添加namespace
spring:
cloud:
nacos:
discovery:
namespace: ae6c2eb8-c143-4854-ad08-7e2c2c744290 #命名空间,填Id
不同namespace下的服务不可见
配置非临时实例
spring:
cloud:
nacos:
discovery:
ephemeral: false #是否是临时实例
Feign是一个声明式的HTTP客户端,官网地址
其作用就是帮助我们优雅的实现HTTP请求的发送,解决上面提到的问题
RestTemplate方式调用存在的问题
String url = "http://userservice/user/" + order.getUserId():
User user = restTemplate.getForObject(url,User.class);
定义和使用Feign客户端
引入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
在order-service的启动类加 @EnableFeignClients
注解开启Feign的功能:
编写Feign客户端:
@FeignClient("userservice") //指定服务名称
public interface UserClient {
@GetMapping("/user/{id}")
User findByID( @PathVariable("id") Long id );
}
主要是基于SpringMVC的注解来声明远程调用的信息,比如:
使用Feign客户端
@Autowired
private UserClient userClient; //注入userClient
public Order queryOrderById(Long orderId) {
// 1.查询订单
Order order = orderMapper.findById(orderId);
// 2.用Feign远程调用
User user = userClient.findByID(order.getUserId());
// 3. 封装user到Order
order.setUser(user);
// 4.返回
return order;
}
Feign运行自定义配置来覆盖默认配置,可以修改的配置如下:
类型 | 作用 | 说明 |
---|---|---|
feign.Logger.Level | 修改日志级别 | 包含四种不同的级别:NONE、Basic、HEADERS、FULL |
feign.codec.Decoder | 响应结果的解析器 | HTTP远程调用的结果做解析,例如解析json字符串为java对象 |
feign.codec.Encoder | 请求参数编码 | 将请求参数编码,便于通过HTTP请求发送 |
feign.Contract | 支持的注解格式 | 默认是SpringMVC的注解 |
feign.Retryer | 失败重试机制 | 请求失败的重试机制,默认是没有,不过会使用Ribbon的重试 |
配置Feign日志有两种方式:
方式一:配置文件方式
全局生效
feign:
client:
config:
default:
loggerLevel: FULL
局部生效
feign:
client:
config:
userservice: #服务名称
loggerLevel: FULL
方式二:java代码方式,需要先声明一个Bean:
public class DefaultFeignConfiguration {
@Bean
public Logger.Level logLevel(){
return Logger.Level.BASIC;
}
}
全局配置,则把他放到@EnableFeignClients这个注解中:
@EnableFeignClients(defaultConfiguration = DefaultFeignConfiguration.class)
局部配置,则把它放到@FeignClient这个注解中:
@FeignClient(value = "userservice",configuration = DefaultFeignConfiguration.class)
Feign底层的客户端实现:
因此提高Feign的性能主要手段就是使用连接池代替默认的URLConnection
Feign添加HTTPClient的支持:
引入依赖
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>
配置连接池
feign:
httpclient:
enabled: true # 开启Feign对HttpClient的支持
max-connections: 200 # 最大连接数
max-connections-per-route: 50 # 每个路径的最大连接数
方式一(继承):给消费者的FeignClient和提供者的Controller定义统一的父接口作为标准
方式二(抽取):将FeignClient抽取为独立模块,并且把接口有关的POJO、默认的Feign配置都放到这个模块中,提供给所有消费者使用
抽取FeignClient
首先创建一个module,命名为feign-api,然后引入feign的starter依赖
将order-service中编写的UserClient、User、DefaultFeignConfiguration都复制到feign-api项目中
在order-service中引入feign-api的依赖
<!--引入feign的统一api-->
<dependency>
<groupId>com.xiaowu.demo</groupId>
<artifactId>feign-api</artifactId>
<version>1.0</version>
</dependency>
修改order-service中所有上述三个组件有关的import部分,改成导入feign-api中的包
重启测试
当定义的FeignClient不在SpringBootApplication的扫描包范围时,这些FeignClient无法使用。用两种方式解决:
方式一:指定FeignClient所在包
@EnableFeignClients(basePackages = "com.xiaowu.feign.clients")
方式二:指定FeignClient字节码
@EnableFeignClients(clients = {UserClient.class})
网关的功能:
网关的技术实现
在SpringCloud中网关的实现包括两种:
Zuul是基于Servlet的实现,属于阻塞式编程。而SpringCloudGateway则是基于Spring5中提供的WebFlux,属于响应式编程的实现,具备更好的性能
创建一个新的module,引入SpringCloudGateway的依赖和nacos的服务发现依赖
<!--nacos服务注册发现依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--网关gateway依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
编写主方法
@SpringBootApplication
public class GatewayApplication {
public static void main( String[] args ) {
SpringApplication.run(GatewayApplication.class,args);
}
}
编写路由配置及nacos地址
server:
port: 10010
spring:
application:
name: gateway
cloud:
nacos:
discovery:
server-addr: localhost:8848 #nacos地址
gateway:
routes:
- id: user-service # 路由标识,必须唯一
uri: lb://userservice #路由的目标地址
predicates: # 路由断言,判断请求是否符合规则
- Path=/user/** # 路径断言,判断路径是否以/user开头,如果是则符合
- id: order-service
uri: lb://orderservice
predicates:
- Path=/order/**
网关路由可以配置的内容包括:
predicates:路由断言,判断请求是否符合要求,符合则转发到路由目的地
Spring提供了11种基本的Predicate工厂:
名称 | 说明 | 示例 |
---|---|---|
After | 在某个时间点后的请求 | - After=2023-01-20T17:12:47.789-07:00[America/Denver] |
Before | 是某个时间点之前的请求 | - Before=2023-01-20T17:12:47.789-07:00[Asia/Shanghai] |
Between | 是某两个时间点之前的请求 | - Between=2023-01-20T17:12:47.789-07:00[Asia/Shanghai],2025-01-20T17:12:47.789-07:00[Asia/Shanghai] |
Cookie | 请求必须包含某些Cookie | - Cooke=chocolate,ch.p |
Header | 请求必须包含某些Header | - Header=X-Request,\d+ |
Host | 请求必须是访问某个host(域名) | - Host = **.somehost,**.anotherhost.ort |
Method | 请求方式必须是指定方式 | - Method=GET,POST |
Path | 请求路径必须符合指定规则 | - Path=/red/{segment},/bule/** |
Query | 请求参数必须包含指定参数 | - Query=name,jack或者Query=name |
RemoteAddr | 请求者的ip必须是指定范围 | - RemoteAdd=192.168.1.1/24 |
Weight | 权重处理 |
GatewayFilter是网关中提供的一种过滤器,可以对进入网关的请求和微服务返回的响应做处理
Spring提供了31种不同的路由过滤器工厂。例如:
名称 | 说明 |
---|---|
AddRequestHeader | 给当前请求添加一个请求头 |
RemoveRequestHeader | 移除请求中的一个请求头 |
AddResponseHeader | 给响应结果中添加一个响应头 |
RemoveResponseHeader | 从响应结果中移除一个响应头 |
RequestRateLimiter | 限制请求流量 |
…… |
给所有进入userservice的请求添加一个请求头
给所有进入userservice的请求添加一个请求头:Truth=xiaowu
实现方式:在gateway中修改application.yml文件,给userservice的路由添加过滤器:
server:
port: 10010
spring:
application:
name: gateway
cloud:
nacos:
discovery:
server-addr: localhost:8848 #nacos地址
gateway:
routes:
- id: user-service # 路由标识,必须唯一
uri: lb://userservice #路由的目标地址
predicates: # 路由断言,判断请求是否符合规则
- Path=/user/** # 路径断言,判断路径是否以/user开头,如果是则符合
filters: #过滤器
- AddRequestHeader= Truth, xiaowu
- id: order-service
uri: lb://orderservice
predicates:
- Path=/order/**
默认过滤器: 如果要对所有的路由都生效,则可以将过期工厂写到default下,如
server:
port: 10010
spring:
application:
name: gateway
cloud:
nacos:
discovery:
server-addr: localhost:8848 #nacos地址
gateway:
routes:
- id: user-service # 路由标识,必须唯一
uri: lb://userservice #路由的目标地址
predicates: # 路由断言,判断请求是否符合规则
- Path=/user/** # 路径断言,判断路径是否以/user开头,如果是则符合
- id: order-service
uri: lb://orderservice
predicates:
- Path=/order/**
default-filters: # 默认过滤器,会对所有的路由请求都生效
- AddRequestHeader= Truth, xiaowu
全局过滤器的作用也是处理一切进入网关的请求和微服务响应,与GatewayFiltering的作用一样。
区别在于GatewayFiltering通过配置定义,处理逻辑是固定的。而GlobalFiltering的逻辑需要自己写代码实现
定义方式是实现GlobalFiltering接口
public interface GlobalFilter{
/**
* 处理当前请求,有必要的话通过{@link GatewayFilterChain}将请求交给下一个过滤器处理
* @param exchange 请求上文,里面可以获取Request、Response等信息
* @param chain 用来把请求委托给下一个过滤器
* @return {@code Mana} 返回表示当前过滤器业务结束
* /
Mono filter(ServerWebExchange exchange,GatewayFilterChain chain);
}
定义全局过滤器,拦截并判断用户身份
需求:定义全局过滤器,拦截请求,判断请求的参数是否满足下面条件:
参数中是否有authorization
authorization参数值是否为admin
如果同时满足则放行,否则拦截
@Order(-1) //优先级
@Component
public class AutherizeFilter implements GlobalFilter {
@Override
public Mono<Void> filter( ServerWebExchange exchange, GatewayFilterChain chain ) {
// 1.获取请求参数
ServerHttpRequest request = exchange.getRequest();
MultiValueMap<String, String> queryParams = request.getQueryParams();
// 2.获取参数中的authorization参数
String auth = queryParams.getFirst("authorization");
// 3.判断参数是否等于 admin
if("admin".equals(auth)){
// 4.是,放行
return chain.filter(exchange);
}
// 5.否,拦截
// 5.1.设置状态码
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
// 5.2.拦截请求
return exchange.getResponse().setComplete();
}
}
请求进入网关会喷到三类过滤器:当前路由的过滤器、DefaultFiltering、GlobalFiltering
请求路由后,会将当前路由过滤器和DefaultFiltering、GlobalFiltering,合并到一个过滤器链(集合)中,排序后依次执行每个过滤器
跨域:域名不一致就是跨域,主要包括:
跨域问题:浏览器禁止请求的发起者与服务端发生跨域Ajax请求,请求被浏览器拦截的问题
解决方案:CORS
网关处理跨域采用的同样是CORS方案,并且只需要简单配置即可实现:
server:
port: 10010
spring:
application:
name: gateway
cloud:
nacos:
discovery:
server-addr: localhost:8848 #nacos地址
gateway:
routes:
- id: user-service # 路由标识,必须唯一
uri: lb://userservice #路由的目标地址
predicates: # 路由断言,判断请求是否符合规则
- Path=/user/** # 路径断言,判断路径是否以/user开头,如果是则符合
- id: order-service
uri: lb://orderservice
predicates:
- Path=/order/**
default-filters: # 默认过滤器,会对所有的路由请求都生效
- AddRequestHeader= Truth, xiaowu
globalcors: # 全局的跨域处理
add-to-simple-url-handler-mapping: true # 解决options请求拦截问题
corsConfigurations:
'[/**]':
allowedOrigins: # 允许哪些网站跨域请求
- "http://localhost:8090"
- "http:127.0.0.1"
allowedMethods: # 允许跨域的Ajax请求方式
- "GET"
- "POST"
- "DELETE"
- "PUT"
- "OPTIONS"
allowedHeaders: "*" # 允许请求中携带头信息
allowCredentials: true # 是否允许携带Cookie
maxAge: 360000 # 这次跨域检查的有效期
同步调用的问题
微服务间基于Feign的调用就属于同步方式,存在一些问题。
异步调用方案
异步调用常见实现就是事件驱动模式
事件驱动优势
异步通信的缺点
什么是MQ
MQ(MessageQueue):消息队列,字面来看就是存放消息的队列。也就是事件驱动架构中的Broker。
RabbitMQ | ActiveMQ | RocketMQ | Kafka | |
---|---|---|---|---|
公司/社区 | Rabbit | Apache | 阿里 | Apache |
开发语言 | Erlang | Java | Java | Scala&Java |
协议支持 | AMQP,XMPP,SMTP,STOMP | OpenWire,STOMP,REST,XMPP,AMQP | 自定义协议 | 自定义协议 |
单机吞吐量 | 一般 | 差 | 高 | 非常高 |
消息延迟 | 微妙级 | 毫秒级 | 毫秒级 | 毫秒以内 |
消息可靠性 | 高 | 一般 | 高 | 一般 |
RabbitMQ是基于Erlang语言开发的开源消息通信中间件,官网
安装
将下载的安装包上传到Linux
安装docker命令: yum -y install docker
启动到dockker:systemctl start docker
加载镜像RabbitMQ: docker load -i mq.tar
执行下面命令来运行MQ容器:
docker run -e RABBITMQ_DEFAULT_USER=xiaowu -e RABBITMQ_DEFAULT_PASS=123456 --name mq --hostname mq1 -p 15672:15672 -p 5672:5672 -d rabbitmq:3-management
查看时候运行成功:docker ps
RabbitMQ中的几个概念:
MQ的官方文档中给出了5个MQ的Demo示例,对应了几种不同的用法:
官方的HelloWorld是基于最基础的消息队列模型来实现的,只包括三个角色:
实现步骤:
基本消息队列的消息发送流程:
基本消息队列的消息接收流程:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
<version>3.1.1</version>
</dependency>
在publisher服务中编写application.yml,添加mq连接信息
spring:
rabbitmq:
host: 192.168.2.102 #主机地址
port: 5672 #端口
virtual-host: / #虚拟主机
username: xiaowu #用户名
password: 123456 #密码
在publisher服务中新建一个测试类,编写测试方法
@RunWith(SpringRunner.class) //声明测试类需要注入容器
@SpringBootTest //单元测试
public class SpringAmqpTest {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void testSendMessageSimpleQueue( ) {
String queueName = "simple.queue";
String message = "Hello,spring amqp";
rabbitTemplate.convertAndSend(queueName,message);
}
}
spring:
rabbitmq:
host: 192.168.2.102 #主机地址
port: 5672 #端口
virtual-host: / #虚拟主机
username: xiaowu #用户名
password: 123456 #密码
@Component //实现Bean注入
public class SpringRabbitListener {
@RabbitListener(queues = "simple.queue") //监听消息,queues:消息的名称,可以是多个
public void listenSimpleQueue(String msg){
System.out.println("消费者接收到simple.queue消息:"+msg);
}
}
模拟WorkQueue,实现一个队列绑定多个消费者
基本思路如下:
通过测试发现,消费者1处理完后需要等待消费者2处理
消费者预取限制
修改application.yml文件,设置preFetch这个值,可以控制预取消息
spring:
rabbitmq:
host: 192.168.2.102 #主机地址
port: 5672 #端口
virtual-host: / #虚拟主机
username: xiaowu #用户名
password: 123456 #密码
listener:
simple:
prefetch: 1 #每次只能取得到一条消息,处理完成ACR之后才能取到下一个消息
Fanout Exchange会将接收到的消息路由到每一个跟其绑定的queue
实现思路如下:
在consumer服务中,利用代码声明队列,交换机,并将两则绑定
@Configuration
public class FanoutConfig {
// 声明xiaowu.fanout 交换机
@Bean
public FanoutExchange fanoutExchange(){
return new FanoutExchange("xiaowu.fanout");
}
// 声明队列fanout.queue1
@Bean
public Queue fanoutQueue1(){
return new Queue("fanout.queue1");
}
// 绑定队列1到交换机
@Bean
public Binding fanoutBinding1(Queue fanoutQueue1,FanoutExchange fanoutExchange){
return BindingBuilder
.bind(fanoutQueue1)
.to(fanoutExchange);
}
// 声明队列fanout.queue2
@Bean
public Queue fanoutQueue2(){
return new Queue("fanout.queue2");
}
// 绑定队列2到交换机
@Bean
public Binding fanoutBinding2(Queue fanoutQueue2,FanoutExchange fanoutExchange){
return BindingBuilder
.bind(fanoutQueue2)
.to(fanoutExchange);
}
}
在consumer服务中,编写两个消费者方法,分别监听fanout.queue1和fanout.queue2
@RabbitListener(queues = "fanout.queue1") //监听消息,queues:消息的名称,可以是多个
public void listenFanoutQueue1(String msg){
System.out.println("消费者接收到fanout.queue1消息:"+msg);
}
@RabbitListener(queues = "fanout.queue2") //监听消息,queues:消息的名称,可以是多个
public void listenFanoutQueue2(String msg){
System.out.println("消费者接收到fanout.queue2消息:"+msg);
}
在publisher中编写测试方法,向xiaowu.fanout发送消息
@Test
public void testSendFanoutExchange(){
// 交换机名称
String exchangeName = "xiaowu.fanout";
//消息
String message = "hello,every one!";
rabbitTemplate.convertAndSend(exchangeName,"",message);
}
实现思路如下:
利用@RabbitListener声明Exchange、Queue、RoutingKey
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "direct.queue1"), //队列名称
exchange = @Exchange(name = "xiaowu.direct",type = ExchangeTypes.DIRECT), //name:交换机名称 type:交换机类型
key = {"red","blue"} //BindingKey
))
在consumer服务中,编写两个消费者方法,分别监听direct.queue1和direct.queue2
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "direct.queue1"), //队列名称
exchange = @Exchange(name = "xiaowu.direct",type = ExchangeTypes.DIRECT), //name:交换机名称 type:交换机类型
key = {"red","blue"} //BindingKey
))
public void listentDirectQueue1(String msg){
System.out.println("消费者接收到direct.queue1消息:"+msg);
}
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "direct.queue2"), //队列名称
exchange = @Exchange(name = "xiaowu.direct",type = ExchangeTypes.DIRECT), //name:交换机名称 type:交换机类型
key = {"red","yellow"} //BindingKey
))
public void listentDirectQueue2(String msg){
System.out.println("消费者接收到direct.queue2消息:"+msg);
在publisher中编写测试方法,向xiaowu.direct发送消息
@Test
public void testSendDirectExchange(){
// 交换机名称
String exchangeName = "xiaowu.direct";
//消息
String message = "hello,blue";
rabbitTemplate.convertAndSend(exchangeName,"blue",message);
}
TopicExchange与DirectExchange类似,区别在于RoutingKey可以是多个单词的列表,并且以
.
分割
Queue与Exchange指定BindingKey时可以使用通配符:
实现思路如下:
利用@RabbitListener声明Exchange、Queue、RoutingKey
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "Topic.queue1"), //队列名称
exchange = @Exchange(name = "xiaowu.Topic",type = ExchangeTypes.TOPIC), //name:交换机名称 type:交换机类型
key = "china.#" //BindingKey
))
在consumer服务中,编写两个消费者方法,分别监听topic.queue1和topic.queue
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "Topic.queue1"), //队列名称
exchange = @Exchange(name = "xiaowu.Topic",type = ExchangeTypes.TOPIC), //name:交换机名称 type:交换机类型
key = "china.#" //BindingKey
))
public void listentTopicQueue1(String msg){
System.out.println("消费者接收到Topic.queue1消息:"+msg);
}
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "Topic.queue2"), //队列名称
exchange = @Exchange(name = "xiaowu.Topic",type = ExchangeTypes.TOPIC), //name:交换机名称 type:交换机类型
key = "#.news" //BindingKey
))
public void listentTopicQueue2(String msg){
System.out.println("消费者接收到Topic.queue2消息:"+msg);
}
在publisher中编写测试方法,向xiaowu.topic发送消息
@Test
public void testSendTopicExchange(){
// 交换机名称
String exchangeName = "xiaowu.Topic";
//消息
String message = "小吴在学Java,敲得都是Bug";
rabbitTemplate.convertAndSend(exchangeName,"china.news",message);
}
在SpringAMQP的发送方法中,接收消息的类型是Object,也就是说我们可以发送任意对象类型的消息,SpringAMQP会帮我们序列化为字节后发送
在consumer中利用@Bean声明一个队列:
@Bean
public Queue objectQueue(){
return new Queue("object.queue");
}
在publisher中发送消息测试
@Test
public void testSendObjectQueue(){
Map<String,Object> msg = new HashMap<>();
msg.put("name","小吴");
msg.put("age",21);
rabbitTemplate.convertAndSend("object.queue",msg);
}
Spring对消息对象的处理事由org.springframework.amqp.support.converter.MessageConverter来处理的。而默认实现是SimpleMessageConverter,基于JDK的ObjectOutputStream完成序列化
如果要修改只需要定义一个Mess阿哥Convert二类型的Bean即可。推荐使用JSON方式序列化,步骤如下:
在publish服务引入依赖
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
<version>2.15.2</version>
</dependency>
在publisher服务(配置类)声明MessageConverter
@Bean
public MessageConverter messageConverter(){
return new Jackson2JsonMessageConverter();
}
接收JSON消息
在consumer服务引入Jackson依赖
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
<version>2.15.2</version>
</dependency>
在consumer服务(配置类)定义MessageConverter
@Bean
public MessageConverter messageConverter(){
return new Jackson2JsonMessageConverter();
}
然后定义一个消费者,监听object.queue队列并消费消息
@RabbitListener(queues = "object.queue")
public void listenObjectQueue( Map<String,Object> msg ){
System.out.println("接收到的object.queue:"+msg);
}
Docker是一个快速交付应用,运行应用的技术:
项目部署问题
大型项目组件较多,运行环境也较为复杂,部署时会碰到一些问题:
Docker如何解决依赖的兼容问题
Docker如何解决不同系统环境的问题
Docker和DockerHub
Docker架构
Docker是一个CS架构的程序,由两部分组成:
企业部署一般都是采用Linux操作系统,而其中CentOS发行版占比最多,因此我们在CentOS下安装Docker
卸载:如果之前安装过旧版本的Docker,可以使用下面命令卸载:
yum remove -y docker \
docker-client \
Docker-client-latest \
docker-common \
docker-latest \
docker-latest-logrotate \
docker-logrotate \
docker-selinux \
docker-engine-selinux \
docker-engine \
docker-ce
安装yum工具
yum install -y yum-utils \
device-mapper-persistent-data \
lvm2 --skip-broken
更新本地镜像源:
# 设置Docker镜像源
yum-config-manager \
--add-repo \
https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
sed -i 's/download.docker.com/mirrors.aliyun.com\/docker-ce/g' /etc/yum.repos.d/docker-ce.repo
yum makecache fast
安装Docker:yum install -y docker-ce
启动Docker:启动Docker前要关闭防火墙或者放行端口
systemctl stop firewalld #关闭防火墙
systemctl disable firewalld #禁止关机重启
systemctl start docker #启动
systemctl stop docker #停止
systemctl restart docker #重启
docker -v
配置镜像:阿里云文档
镜像名称一般分两部分组成:[repository]:[tag]
在没有指定tag时,默认是latest,代表最新版本的镜像
从DockerHub中拉取一个Nginx镜像并查看
利用docker save将Nginx镜像导出磁盘,然后再通过Load加载回来
创建运行一个Nginx容器
去docker hub查看Nginx的容器运行命令
docker run --name mn -p 80:80 -d nginx
进入Nginx容器,修改HTML文件内容,添加“小吴在敲Bug”
进入容器
docker exec -it mn bash
进入Nginx的HTML所在目录 /usr/shar/nginx/html
cd /usr/shar/nginx/html
修改index.html的内容
sed -i 's#Welcome to nginx#小吴在敲Bug#g' index.html
sed -i 's#<head>#<head><meta charset="utf-8">#g' index.html
数据卷(volume): 是一个虚拟目录,指向宿主机文件系统的某个目录
数据卷操作的基本语法如下:
docker volume [COMMAND]
docker volume命令是数据操作,根据命令跟随的command来确定下一步操作:
创建一个数据卷,并查看数据卷在宿主机的目录位置
创建数据卷
docker volume create html
查看所有数据
docker volume ls
查看数据卷详细信息卷
docker volume inspect html
挂载数据卷
我们在创建容器时,可以通过 -v
参数来挂载一个数据到某个容器目录
docker run \ #创建并运行容器
--name mn \ # 给容器起名字
-v html:/root/html \ #把html数据卷挂载到容器内的/root/html/这个目录中
-p 80:80 \ #把宿主机的80端口映射到容器内的80端口
nginx #镜像名称
创建一个Nginx容器,修改容器内的html目录内的index.html内容
创建容器并挂载数据卷到容器内的HTML目录
# 如果容器运行时volume不存在,会自动创建出来
docker run --name mn -p 80:80 -v html:/usr/share/nginx/html -d nginx
进入HTML数据卷所在位置,并修改HTML内容
# 查看HTML数据卷的位置
docker volume inspect html
# 进入该目录
cd /var/lib/docker/volumes/html/_data
# 修改文件
vi index.html
创建并运行一个MySQL容器,将宿主机目录直接挂载到容器
提示:目录挂载与数据卷挂载的语法是类似的:
实现思路如下:
将下载好的mysql.tar文件上传到虚拟机,通过load命令加载为镜像
创建目录/tmp/mysql/data
创建目录/tmp/mysql/conf,将hmy.cnf文件上传到/tmp/mysql/conf
去DockerHub查阅资料,创建运行MySQL容器,要求
docker run \
--name mysql \
-e MYSQL_ROOT_PASSWORD=root \
-p 3306:3306 \
-v /tmp/mysql/conf/hmy.cnf:/etc/mysql/conf.d/hmy.cnf \
-v /tmp/mysql/data:/var/lib/mysql \
-d \
mysql:5.7.25
Dockerfile就是一个文本文件,其中包含一个个的指令(Instruction),用指令来说明要执行什么操作来构建镜像。每一个指令都会形成一层(Layer)
官方文档
指令 | 说明 | 实例 |
---|---|---|
FORM | 指定基础镜像 | FROM centos:6 |
ENV | 设置环境变量,可在后面指令使用 | ENV key value |
COPY | 拷贝本地文件到镜像的指定目录 | COPY ./mysql-5.7.rpm /tmp |
RUN | 执行Linux的shell命令,一般是安装过程的命令 | RUN yum install gcc |
EXPOSE | 指定容器运行时监听的端口,是给镜像使用者看的 | EXPOSE 8080 |
ENTRYPOINT | 镜像中应用的启动命令,容器运行时调用 | ENTRYPOINT java -jar xx.jar |
基于Ubuntu镜像构建一个新镜像,运行一个Java项目
docker build -t javaweb:1.0 .
基于Java:8-alpine镜像,将一个Java项目构建为镜像
新建一个空的目录,然后再目录中新建一个文件,命名为Dockerfile
将docker-demo.jar文件上传到这个目录
编写Dockerfile文件:
# 指定基础镜像
FROM java:8-alpine
COPY ./docker-demo.jar /tmp/app.jar
# 暴露端口
EXPOSE 8090
# 入口,java项目的启动命令
ENTRYPOINT java -jar /tmp/app.jar
使用docker build命令构建镜像
使用docker run创建容器并运行
安装DockerCompose
下载
# 安装
curl -L https://get.daocloud.io/docker/compose/releases/download/1.29.1/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
修改文件权限:chmod +x /usr/local/bin/docker-compose
命令补全设置
echo "199.232.68.133 raw.githubusercontent.com" >> /etc/hosts
curl -L https://raw.githubusercontent.com/docker/compose/1.29.1/contrib/completion/bash/docker-compose -o /etc/bash_completion.d/docker-compose
将之前学习的cloud-demo微服务集群利用DockerCompose部署
使用maven打包工具,将项目中的每个微服务都打包为app.jar
将打包好的app.jar拷贝到cloud-demo中的每一个对应的子目录
在每个对应的子目录创建 Dockerfile
文件并写入
FROM java:8-alpine
COPY ./app.jar /tmp/app.jar
ENTRYPOINT java -jar /tmp/app.jar
数据库文件
在项目根目录创建 docker-compose.yml
文件并编写
version: "3.2"
services:
nacos:
image: nacos/nacos-server
environment:
MODE: standalone
ports:
- "8848:8848"
mysql:
image: mysql:5.7.25
environment:
MYSQL_ROOT_PASSWORD: 123
volumes:
- "$PWD/mysql/data:/var/lib/mysql"
- "$PWD/mysql/conf:/etc/mysql/conf.d/"
userservice:
build: ./user-service
orderservice:
build: ./order-service
gateway:
build: ./gateway
ports:
- "10010:10010"
镜像仓库(Docker Registry)有公共和私有的两种形式:
搭建私有厂库
简化版镜像仓库
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 可以查看当前私有镜像服务中包含的镜像
图形化界面版本
配置docker信任地址,因为私服采用的是http协议,默认不被Docker信任,所以需要做一个配置:
# 打开要修改的文件
vi /etc/docker/daemon.json
# 添加内容:配置之间要用逗号分隔
"insecure-registries":["http://192.168.2.102:8090"]
# 重加载
systemctl daemon-reload
# 重启docker
systemctl restart docker
使用DockerCompose部署带有图象界面的DockerRegistry
新建空文件夹registry-ui:mkdir registr-ui
创建docker-compose.yml文件: vi registr-ui/docker-compose.yml
,内容如下
version: '3.0'
services: #官方Docker Registr
registry:
image: registry
volumes:
- ./registry-data:/var/lib/registry
ui: #图形化界面
image: joxit/docker-registry-ui:static
ports: #暴露端口8090
- 8090:80
environment: #服务部署标题和registry内部访问地址
- REGISTRY_TITLE=小吴私有仓库
- REGISTRY_URL=http://registry:5000
depends_on:
- registry
进入registry-ui运行docker-compose,日志查看
docker-compose up -d
docker-compose logs -f
推送镜像到私有镜像服务必须先tag,步骤如下