绥化网站开发公司,程序员外包公司到底值不值得去,二级域名免费发放,东莞seo建站优化费用微服务技术栈
认识微服务
单体架构
单体架构#xff1a;将业务的所有功能集中在一个项目中开发#xff0c;打成一个包部署 优点#xff1a;
架构简单部署成本低
缺点#xff1a;
耦合度高
分布式架构
分布式架构#xff1a;根据业务功能对系统进行拆分#xff0c…微服务技术栈
认识微服务
单体架构
单体架构将业务的所有功能集中在一个项目中开发打成一个包部署 优点
架构简单部署成本低
缺点
耦合度高
分布式架构
分布式架构根据业务功能对系统进行拆分每个业务模块作为独立项目开发称为一个服务
优点
降低服务耦合有利于服务升级拓展
需要考虑的问题
服务拆分粒度如何服务集群地址如何维护服务之间如何实现远程调用服务健康状态如何感知
微服务
微服务是一种经过良好架构设计的分布式架构方案微服务架构特征
单一职责微服务拆分粒度更小每一个服务都对应唯一的业务能力做到单一职责避免重复业务开发面向服务微服务对外暴露业务接口自治团队独立、技术独立、数据独立、部署独立隔离性强服务调用做好隔离、容错、降级避免出现级联问题
微服务架构
微服务这种方案需要技术框架落地全球的互联网公司都在积极尝试自己的微服务落地技术。在国内最知名的就是SpringCloud和阿里巴巴的Dubbo
DubboSpringCloudSpringCloudAlibaba注册中心zookeeper、RedisEureka、ConsulNacos、Eureka服务远程调用Dubbo协议FeignHTTP协议Dubbo、Feign配置中心无SpringCloudCOnfigSpring Cloud Config、Nacos服务网关无SpringCloudGateway、ZuulSpringCloudGayeway、Zuul服务监控和保护dubbo-admin功能弱HystrixSentinel SpringCloud
SpringCloud是目前国内使用最广泛的微服务框架。官网地址Spring Cloud
SpringCloud集成了各种微服务功能组件并基于SpringBoot实现了这些组件的自动装配从而提供了良好的开箱即用体验 服务拆分及远程调用
服务拆分注意事项
不同微服务不要重复开发相同业务微服务数据独立不要访问其它微服务的数据库微服务可以将自己的业务暴露为接口供其它微服务调用
实现远程调用案例
在order-service服务中创建一个根据id查询订单的接口:
import com.dc.order.pojo.Order;
import com.dc.order.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;RestController
RequestMapping(order)
public class OrderController {Autowiredprivate OrderService orderService;GetMapping({orderId})public Order queryOrderByUserId(PathVariable(orderId) Long orderId) {// 根据id查询订单并返回return orderService.queryOrderById(orderId);}
}根据id查询订单返回值是order对象 其中的user为null
在user-service中有一个根据id查询用户的接口
import com.dc.user.pojo.User;
import com.dc.user.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;Slf4j
RestController
RequestMapping(/user)
public class UserController {Autowiredprivate UserService userService;/*** 路径 /user/1** param id 用户id* return 用户*/GetMapping(/{id})public User queryById(PathVariable(id) Long id) {return userService.queryById(id);}
}查询结果如图 需求
修改order-service中的根据id查询订单业务要求在查询订单的同时根据订单中包含的userId查询出用户信息一起返回。
因此需要在order-service中向user-service发起一个http请求调用http://localhost:80/user/{userId}这个接口
步骤
注册一个RestTemplate的实例到Spring容器中修改order-service服务中的OrderService类中的queryOrderById方法根据Order对象中的userId查询user将查询到的User填充到Order对象一起返回
注册RestTemplate
首先需要在order-service服务中的OrderApplication启动类中注册RestTemplate实例
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;MapperScan(com.dc.order.mapper)
SpringBootApplication
public class OrderApplication {public static void main(String[] args) {SpringApplication.run(OrderApplication.class, args);}Beanpublic RestTemplate restTemplate() {return new RestTemplate();}
}实现远程调用
修改order-service服务中的OrderService类中的queryOrderById方法
import com.dc.order.mapper.OrderMapper;
import com.dc.order.pojo.Order;
import com.dc.order.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;Service
public class OrderService {Autowiredprivate OrderMapper orderMapper;Autowiredprivate RestTemplate restTemplate;public Order queryOrderById(Long orderId) {// 1.查询订单Order order orderMapper.findById(orderId);// 远程查询User// url地址String url http://localhost:80/user/ order.getUserId();// 发起调用User user restTemplate.getForObject(url, User.class);order.setUser(user);// 4.返回return order;}
}结果如图 提供者与消费者
在服务调用关系中会有两个不同的角色
服务提供者一次业务中被其它微服务调用的服务。(提供接口给其他微服务)
服务消费者一次业务中调用其他微服务的服务。(调用其它微服务提供的接口)
但是服务提供者与服务消费者的角色并不是绝对的而是相对于业务而言。 对于A调用B的业务而言A是服务消费者B是服务提供者 对于B调用C的业务而言B是服务消费者C是服务提供者
因此服务B既可以是服务提供者也可以是服务消费者
Eureka注册中心
eureka的作用
消费者该如何获取服务提供者具体信息 服务提供者启动时向eureka注册自己的信息eureka保存这些信息消费者提供服务名称向eureka拉取提供者信息 如果有多个服务提供者消费者该如何选择 服务消费者利用负载均衡算法从服务列表中挑选一个 消费者如何感知服务提供者健康状态 服务提供者会每隔30秒向EurekaServer发送心跳请求报告健康状态eureka会更新记录到服务列表信息心跳不正常会被剔除消费者就可以拉取到zui’xin
搭建eureka-server
首先需要在cloud-demo父工程下创建一个子模块eureka-server
引入依赖
引入SpringCloud为eureka提供的starter依赖
dependencygroupIdorg.springframework.cloud/groupIdartifactIdspring-cloud-starter-netflix-eureka-server/artifactId
/dependency编写启动类
给eureka-server服务编写一个启动类一定要添加一个EnableEurekaServer注解开启eureka的注册中心功能
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;SpringBootApplication
EnableEurekaServer
public class DemoApplication {public static void main(String[] args) {SpringApplication.run(DemoApplication.class, args);}
}编写配置文件
编写一个application.yml文件内容如下
server:port: 10086
spring:application:name: euureka-server
eureka:client:service-url:defaultZone: http://127.0.0.1:10086/eureka启动服务
启动微服务然后在浏览器访问http://127.0.0.1:10086
出现如下图的结果就表示成功了 服务注册
将user-service注册到eureka-server中
引入依赖
在user-service的pom.xml文件中引入下面的eureka-client依赖
dependencygroupIdorg.springframework.cloud/groupIdartifactIdspring-cloud-starter-netflix-eureka-client/artifactId
/dependency配置文件
在user-service中修改application.yml配置文件添加服务名称、erueka地址
spring:application:name: userservice
eureka:client:service-url:defaultZone: http://127.0.0.1:10086/eureka启动多个user-service实例
首先复制user-service启动配置
选中user-service右键 配置相关属性 启动两个user-service端口一个80一个82 查看eureka-server管理界面 服务发现
将order-service的逻辑修改向erueka-server拉去user-service的信息实现服务发现
引入依赖
服务发现、服务注册统一都封装在eureka-client依赖
在order-service的pom文件中引入下面的eureka-client依赖
dependencygroupIdorg.springframework.cloud/groupIdartifactIdspring-cloud-starter-netflix-eureka-client/artifactId
/dependency配置文件
在order-service中修改application.yml文件添加服务名称、eureka地址
spring:application:name: orderservice
eureka:client:service-url:defaultZone: http://127.0.0.1:10086/eureka服务拉取和负载均衡
实现负载均衡只需要添加一些注解即可
在order-service的OrderApplication中给RestTemplate这个Bean添加一个LoadBalanced注解
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;MapperScan(com.dc.order.mapper)
SpringBootApplication
public class OrderApplication {public static void main(String[] args) {SpringApplication.run(OrderApplication.class, args);}BeanLoadBalancedpublic RestTemplate restTemplate() {return new RestTemplate();}
}修改order-service服务中的OrderService类中的queryOrderById方法修改访问的url路径用服务名代替ip、端口
import com.dc.order.mapper.OrderMapper;
import com.dc.order.pojo.Order;
import com.dc.order.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;Service
public class OrderService {Autowiredprivate OrderMapper orderMapper;Autowiredprivate RestTemplate restTemplate;public Order queryOrderById(Long orderId) {// 1.查询订单Order order orderMapper.findById(orderId);// 远程查询User// url地址//String url http://localhost:80/user/ order.getUserId();String url http://userservice/user/ order.getUserId();// 发起调用User user restTemplate.getForObject(url, User.class);order.setUser(user);// 4.返回return order;}
}spring自动从eureka-server端根据userservice这个服务名称获取实例列表而后完成负载均衡
结果如下 负载均衡
原理
SpringCloud底层其实利用了一个名为Ribbon的组件来实现负载均衡功能的 Nacos注册中心
国内公司都比较推崇阿里巴巴的技术比如注册中心SpringCloudAlibaba也推出了一个名叫Nacos的注册中心
认识和安装Nacos
Nacos是阿里巴巴的产品现在是SpringCloud中的一个组件。相比于Eureka功能更加丰富在国内受欢迎程度较高
安装
在Nacos的GitHub页面可以下载源码和编译好的服务端
主页地址https://github.com/alibaba/nacos
下载地址https://github.com/alibaba/nacos/releases
如图 红线标记的是服务端
解压
将压缩包解压到非中文路径下 其中
bin启动脚本
conf配置文件
端口配置
Nacos的默认端口是8848如果无法关闭占用8848端口的进程可以进入nacos的conf目录修改配置文件application.properties中的端口 启动
进入bin目录中然后进入命令行
输入
startup.cmd -m standalone在浏览器中输入地址http://127.0.0.1:8848/nacos,用户名和密码为nacos进入主页如下 服务注册到nacos
Nacos是SpringCloudAlibaba的组件而SpringCloudAlibaba也遵循SpringCloud中定义的服务注册、服务发现规范。因此使用Nacos和使用Ereka对于微服务来说并没有太大区别
主要差别在于
依赖不同服务地址不同
引入依赖
在cloud-demo父工程的pom文件中的dependencyManagement中引入SpringCloudAlibaba的依赖
dependencygroupIdcom.alibaba.cloud/groupIdartifactIdspring-cloud-alibaba-dependencies/artifactIdversion2.2.6.RELEASE/versiontypepom/typescopeimport/scope
/dependency在user-service和order-service中的pom文件中引入nacos-discovery依赖
dependencygroupIdcom.alibaba.cloud/groupIdartifactIdspring-cloud-starter-alibaba-nacos-discovery/artifactId
/dependency注意把eureka的依赖注释掉
配置nacos地址
在user-service和order-service的application.yml配置文件中添加nacos地址
spring:cloud:nacos:discovery:server-addr: localhost:8848注意把关于eureka的配置文件注释
重启
重启微服务后登录nacos管理页面可以看到微服务信息 服务分级存储模型 注意一个服务可以有多个实例(即一个服务可以包含多个集群而每个集群下可以有多个实例形成分级模型) 微服务互相访问时应该尽可能访问同集群实例因为本地访问速度更快。当本集群内不可用时才访问其他集群。
给user-service配置集群
修改user-service 的application.yml文件添加集群配置
spring:cloud:nacos:discovery:server-addr: localhost:8848cluster-name: MZ # 集群名称重启两个user-service实例后可以在nacos控制台看到下面结果 这是再复制一个user-service启动配置添加属性
-Dserver.port81 -Dspring.cloud.nacos.discovery.cluster-nameSH这次启动三个启动类再次查看nacos控制台 同集群优先的负载均衡
默认规则是ZoneAvoidanceRule(即基于分区下的服务器的可用性选出可用分区列表再从可用分区列表中随机选择一个分区采用轮询的策略选择该分区的一个服务器)但这种规则并不能实现根据同集群优先来实现负载均衡
因此Nacos中提供了一个NacosRule(1、优先选择同集群服务实例列表2、本地集群中找不到提供者才去其他集群中寻找并且给出警告。3、确定可用实例后在采用随机负载均衡挑选实例)这种规则可以优先从同集群中挑选实例
1、给order-service配置集群信息
修改order-service的application.yml文件添加集群配置
spring:cloud:nacos:server-addr: localhost:8848discovery:cluster-name: MZ # 集群名称2、修改负载均衡规则
修改order-service的application.yml文件修改负载均衡规则
userservice:ribbon:NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule # 负载均衡规则 权重配置
Nacos提供了权重配置来控制访问频率权重越大则访问频率越高
注意如果权重修改为0则该实例永远不会被访问
在nacos控制台找到user-service的实例列表点击编辑即可修改权重 在弹出的窗口中修改权重 环境隔离
Nacos提供了namespace来实现环境隔离功能。
nacos中可以有多个namespacenamespace下可以有group、service等不同namespace之间相互隔离例如不同namespace的服务互相不可见 配置命名空间的步骤如下 创建namespace 默认情况下所有的service、data、group都在同一个namespace下默认是public 点击新增命名空间添加有一个命名空间
命名空间id可以自动生成标红的两项是必填项
点击确定之后就可以在页面中看到一个新的命名空间 给微服务配置namespace
给微服务配置namespace只能通过修改配置来实现
例如给user-service中的application.yml文件中配置namespace 重启user-service后访问nacos控制台会在dev中出现userservice服务 如果此时访问order-service服务会报错这是因为order-service和user-service出现在不同的namespace下
Nacos与Eureka的区别
Nacos的服务实例分为两种类型
临时实例如果实例宕机超过一定时间会从服务列表剔除默认类型非临时实例如果实例宕机不会从列表剔除也可以叫做永久实例
永久实例的配置;
spring:cloud:nacos:discovery:ephemeral: false # 设置为非临时实例Nacos和Eureka整体结构类似服务注册、服务拉取、心跳等待但是也存在一些差异 共同点
都支持服务注册和服务拉取都支持服务提供者心跳方式做健康检测
区别
nacos支持服务端主动检测提供者状态临时实例采用心跳模式非临时实例采用主动检测模式临时实例心跳不正常会被剔除非临时实例则不会被剔除nacos支持服务列表变更的消息推送模式服务列表更新更及时nacos集群默认采用方式当集群中存在非临时实例时采用CP模式Eureka采用AP方式。 AP可用性|分区容错性 CP一致性|分区容错性 CAP原则一致性可用性、分区容错性最多只能同时实现两点不可能三者兼顾 一致性在分布式系统中的所有数据备份在同一个时刻是否同样的值(等同于所有节点访问同一份最新的数据副本)可用性在集群中一部分节点故障后集群整体是否还能响应客户端的读写请求对数据更新具备高可用性。分区容错性大多数分布式系统都分布在多个子网络。每个子网络就叫做一个区。 分区容错区间通信可能失败。 Nacos配置管理
统一配置管理
当微服务部署的实例越来越多达到数十、数百时逐个修改微服务配置就会很麻烦。这时就需要统一配置管理方案可以集中管理所有实例的配置。
nacos一方面可以将配置集中管理及时通知微服务实现配置的热更新
在nacos中添加配置文件
首先在配置详情中的配置列表中点击号 然后在弹出的表单中填写配置信息格式的话目前只支持yaml和properteis文件 从微服务中拉取配置
微服务要拉取nacos中管理的配置并且与本地的application.yml配置合并才能完成项目启动
这个获取nacos地址的过程需要借助外界的帮助这时就需要bootstrap.yml为文件会在application.yml之前被读取流程 引入nacos-config依赖
首先需要在user-service服务中引入nacos-config的客户端依赖
!--nacos配置管理依赖--
dependencygroupIdcom.alibaba.cloud/groupIdartifactIdspring-cloud-starter-alibaba-nacos-config/artifactId
/dependency添加bootstrap.yml
在user-service中resouce目录下创建bootstrap.yml文件内容如下
spring:application:name: userservice # 服务名称profiles:active: dev # 开发环境这里是devcloud:nacos:server-addr: localhost:8848 #nacos地址config:file-extension: yaml # 后缀名namespace: 29de2d5a-2658-4621-ad52-1b32bcff8224 #命名空间其实就是读取userservice-dev.yaml文件 注意这里bootstrap配置文件要与nacos中的配置一一对应否则会出现 读取nacos配置
在userservice中的usercontroller中添加业务逻辑读取pattern.dateformat配置信息 Value(${pattern.dateformat})private String dateformat;GetMapping(now)public String now() {return LocalDateTime.now().format(DateTimeFormatter.ofPattern(dateformat));}
}重启user-service服务在页面访问得到结果 配置热更新
热更新就是修改nacos配置后微服务无需重启即可让配置生效也就是配置热更新
实现方式
方式一
在Value注入的变量所在类上添加RefreshScope注解
package com.dc.user.web;import com.dc.user.pojo.User;
import com.dc.user.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.*;import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;Slf4j
RestController
RequestMapping(/user)
RefreshScope
public class UserController {Value(${pattern.dateformat})private String dateformat;GetMapping(now)public String now() {return LocalDateTime.now().format(DateTimeFormatter.ofPattern(dateformat));}
}这是重启user-service后在nacos控制台修改配置文件如下 再次访问页面结果如下 方式二
使用ConfigurationProperties注解代替Value注解这时需要创建一个类来读取配置信息
package com.dc.user.utils;import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;Component
Data
ConfigurationProperties(prefix pattern)
public class PatternProperties {private String dateformat;
}UserController类
package com.dc.user.web;import com.dc.user.pojo.User;
import com.dc.user.service.UserService;
import com.dc.user.utils.PatternProperties;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.*;import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;Slf4j
RestController
RequestMapping(/user)
RefreshScope
public class UserController {Autowiredprivate PatternProperties patternProperties;GetMapping(/now)public String now() {return LocalDateTime.now().format(DateTimeFormatter.ofPattern(patternProperties.getDateformat()));}
}然后重启user-service服务之后访问页面 这是在naocos控制台修改配置文件 然后刷新页面 配置共享
微服务启动时会去nacos读取多个配置文件
添加一个环境共享配置
在nacos配置列表中添加一个userservice.yaml配置文件 在userservice中读取共享配置
在userservice服务中修改PatternProperties类读取新添加的属性
package com.dc.user.utils;import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;Component
Data
ConfigurationProperties(prefix pattern)
public class PatternProperties {private String dateformat;private String envSharedValue;
}然后修改UserController添加一个方法 import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;Slf4j
RestController
RequestMapping(/user)
RefreshScope
public class UserController {Autowiredprivate PatternProperties patternProperties;GetMapping(/prop)public PatternProperties prop() {return patternProperties;}
}运行两个userservice服务并设置不同的profile配置文件
将一个设置为dev另一个设置为test 启动两个服务并通过页面访问结果 这时可以看到无论是dev还是test都可以读取到enSharedValue这个属性的值
配置共享的优先级
当nacos、服务本地同时出现相同属性时优先级有高低之分 Feign远程调用
之前使用RestTemplete发起远程调用的方式存在以下问题
代码可读性差变成体验不统一参数复杂URL难以维护
Feign是一个声明式的http客户端官网地址https://github.com/OpenFeign/feign
其作用就是实现http请求的发送解决上面的问题
Openfeign:是一种声明式的web工具可以使用它的注解创建接口从而实现服务的远程调用OpenFeign不做任何请求处理通过处理注解相关信息生成Request并对调用返回的数据进行解码从而实现 简化 HTTP API 的开发 需要创建一个接口并对其添加Feign相关注解另外Feign还支持可插拔编码器和解码器致力于打造一个轻量级HTTP客户端,Feign最早是由 Netflix 公司进行维护的后来Netflix不再对其进行维护最终 Feign 由社区进行维护更名为Openfeign
Feign替代RestTemplate
使用步骤
引入依赖
在order-service服务的pom文件中引入fegin的依赖
dependencygroupIdorg.springframework.cloud/groupIdartifactIdspring-cloud-starter-openfeign/artifactId
/dependency添加注解
在order-service服务的启动类上添加注解开启feign的功能
package com.dc.order;import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;MapperScan(com.dc.order.mapper)
SpringBootApplication
EnableFeignClients
public class OrderApplication {public static void main(String[] args) {SpringApplication.run(OrderApplication.class, args);}BeanLoadBalancedpublic RestTemplate restTemplate() {return new RestTemplate();}
}编写feign的客户端
在order-service服务中新建一个UserClient接口
package com.dc.order.client;import com.dc.order.pojo.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;FeignClient(userservice)
public interface UserClient {GetMapping(/user/{id})User findById(PathVariable(id) Long id);
}这个客户端是基于SpringMVC的注解来声明远程调用的信息如
服务名称userservice请求方式Get请求路径/user/{id}请求参数Long id返回值类型 User
这样就无需使用RestTemplate来发送http请求了 测试
修改order-service中的OrderService类中的queryById方法使用Feign客户端来代替RestTemplate
package com.dc.order.service;import com.dc.order.client.UserClient;
import com.dc.order.mapper.OrderMapper;
import com.dc.order.pojo.Order;
import com.dc.order.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;Service
public class OrderService {Autowiredprivate OrderMapper orderMapper;/*Autowiredprivate RestTemplate restTemplate;*/Autowiredprivate UserClient userClient;public Order queryOrderById(Long orderId) {// 1.查询订单Order order orderMapper.findById(orderId);// 远程查询UserUser user userClient.findById(order.getUserId());/*// url地址//String url http://localhost:80/user/ order.getUserId();String url http://userservice/user/ order.getUserId();// 发起调用User user restTemplate.getForObject(url, User.class);*/order.setUser(user);// 4.返回return order;}
}查询结果 自定义配置
feign可以支持很多的自定义配置如下表所示
类型作用说明feign.Logger.Level修改日志级别包含四种不同的级别NONE、BASIC、HEADERS、FULLfeign.codec.Decoder响应结果的解析器http远程调用的结果做解析例如解析json字符串为Java对象feign.codec.Encoder请求参数编码将请求参数编码便于通过http请求发送feign.Contract支持的注解格式默认是SpringMVC的注解feign.Retryer失败重试机制请求失败的重试机制默认是没有不过会使用Ribbon的重试
一般请求下默认值就能满足使用如果使用自定义时只需要创建自定义的 Bean覆盖默认Bean即可
日志案例
配置文件方式
方式一配置文件
基于配置文件修改feign的日志级别可以针对单个服务
feign:client:config:userservice:logger-level: FULL也可以针对所有服务
feign: client:config: default: # 这里用default就是全局配置如果是写服务名称则是针对某个微服务的配置loggerLevel: FULL # 日志级别 注意使用配置文件方式时需要设置log日志级别这样才能看到 结果如下:
07-22 15:59:54:502 DEBUG 15304 --- [nio-8080-exec-1] com.dc.order.client.UserClient : [UserClient#findById] --- HTTP/1.1 200 (1091ms)
07-22 15:59:54:502 DEBUG 15304 --- [nio-8080-exec-1] com.dc.order.client.UserClient : [UserClient#findById] connection: keep-alive
07-22 15:59:54:502 DEBUG 15304 --- [nio-8080-exec-1] com.dc.order.client.UserClient : [UserClient#findById] content-type: application/json
07-22 15:59:54:502 DEBUG 15304 --- [nio-8080-exec-1] com.dc.order.client.UserClient : [UserClient#findById] date: Sat, 22 Jul 2023 07:59:54 GMT
07-22 15:59:54:502 DEBUG 15304 --- [nio-8080-exec-1] com.dc.order.client.UserClient : [UserClient#findById] keep-alive: timeout60
07-22 15:59:54:502 DEBUG 15304 --- [nio-8080-exec-1] com.dc.order.client.UserClient : [UserClient#findById] transfer-encoding: chunked
07-22 15:59:54:503 DEBUG 15304 --- [nio-8080-exec-1] com.dc.order.client.UserClient : [UserClient#findById]
07-22 15:59:54:503 DEBUG 15304 --- [nio-8080-exec-1] com.dc.order.client.UserClient : [UserClient#findById] {id:2,username:文二狗,address:陕西省西安市}
07-22 15:59:54:503 DEBUG 15304 --- [nio-8080-exec-1] com.dc.order.client.UserClient : [UserClient#findById] --- END HTTP (62-byte body)而日志级别可以分为四种 NONE不记录任何日志信息这是默认值 BASIC仅记录请求的方法URL以及响应状态码和执行时间 HEADERS在BASIC的基础上额外记录了请求和响应的头信息 FULL记录请求和响应的明细包括头信息、请求体、元数据
Java代码方式
也可以基于Java代码来修改日志级别先声明一个类然后声明一个Logger.level的对象
public class DefaultFeignConfiguration {Beanpublic Logger.Level feignLogLevel(){return Logger.Level.BASIC; // 日志级别为BASIC}
}如果要全局生效将其字节码放到EnableFeignClients这个注解中
EnableFeignClients(defaultConfiguration DefaultFeignConfiguration .class) 如果是局部生效,则把它放到对应的FeignClient这个注解中
FeignClient(value userservice, configuration DefaultFeignConfiguration .class) Feign使用优化
Feign底层发起http请求依赖于其它框架。其底层客户端实现包括
URLConnection默认实现不支持连接池Apache HttpClient支持连接池OKHttp支持连接池
因此提高Feign的性能主要手段就是使用连接池代替默认的URLConnection
配置
引入依赖
在order-service的pom文件中引入Apache的HttpClient依赖
!--httpClient的依赖 --
dependencygroupIdio.github.openfeign/groupIdartifactIdfeign-httpclient/artifactId
/dependency配置连接池
在order-service的application.yml中添加配置
feign:client:config:userservice:logger-level: FULLhttpclient:enabled: true #开启feign对HttpClient的支持max-connections: 200 #最大的连接数max-connections-per-route: 50 #每个路径的最大连接数总结
日志级别尽量使用basic使用HttpClient或OKHttp代替URLConnection 引入feign-httpClient依赖配置文件开启httpClient功能设置连接池参数
最佳实践
所谓最佳实践就是使用过程中总结的经验最好的一种使用方式
简化Feign客户端代码
继承方式
一样的代码可以通过继承来共享
定义一个API接口利用定义方法并基于SpringMVC注解做声明Feign客户端和Controller都集成该接口 优点
简单实现了代码共享
缺点
服务提供方、服务消费方紧耦合参数列表中的注解映射并不会继承因此Controller中必须再次声明方法、参数列表、注解
抽取方式
将Feign的Client抽取为独立模块并且把接口有关的POJO、默认的Feign配置都放到这个模块中提供给所有消费者使用如 实现基于抽取的最佳实践
抽取
首先创建一个module命名为feign-api
然后导入feign的starter依赖
dependencygroupIdorg.springframework.cloud/groupIdartifactIdspring-cloud-starter-openfeign/artifactId
/dependency然后将order-service中编写的UserClient、User、DefaultFeignConfiguration剪切到feign-api中
在order-service中使用feign-api
在order-service中引入feign-api的依赖
dependencygroupIdorg.springframework.cloud/groupIdartifactIdspring-cloud-starter-openfeign/artifactId
/dependency扫描包
方式一
指定Feign应该扫描的包
EnableFeignClients(basePackages com.dc.feign.client)方式二
指定需要加载的Client接口
EnableFeignClients(clients {UserClient.class})3.4 OpenFeign的底层原理 1.在 Spring 项目启动阶段启动类上的EnableFeignClients注解会引入一个FeignClientsRegistrarFeign客户端注册类它会从指定的目录下扫描并加载所有被 FeignClient 注解修饰的接口类interface然后将这些接口类型注册成 Bean对象统一交给 Spring 来管理。 2.FeignClient 修饰的接口类的方法经过 MVC Contract 协议的解析后放入 MethodMetadata方法元数据数组中。 3.然后创建一个动态代理对象Proxy 指向了一个存放着key为FeignClient 修饰的接口类的方法名和 value为方法名对应的MethodHandler MethodHandler 记录着MethodMetadata方法元数据的引用的 HashMap。然后把动态代理对象Proxy添加到 Spring 容器中并注入到对应的服务里。 4.当服务调用FeignClient接口类的方法时从动态代理对象 Proxy 中找到一个 MethodHandler 实例生成一个包含该方法URL的 Http请求不包含服务的 IP。 5.经过loadbalancer负载均衡算法找到一个服务的 IP 地址拼接出完整的 URL并发起请求。 6.被调用服务收到Http请求就可以响应请求返回数据给调用者了。
GateWay服务网关
Gateway网关是所有微服务的统一入口
网关的核心功能特性
请求路由权限控制限流
架构图 权限控制网关作为微服务入口需要校验用户是否有请求资格如果没有则进行拦截
路由和负载均衡一切请求都必须先经过gateway但网关不处理业务而是根据某种规则把请求转发到某个微服务这个过程就是路由当路由的目标服务有多个时还需要做负载均衡
限流当请求流量过高时在网关中按照下流的微服务能够接受的速度来放行请求避免服务压力过大
在SpringCloud中网关的实现包括两种
gatewayzuul
Zuul是基于Servlet的实现属于阻塞式编程。而SpringCloudGateway则是基于Spring5中提供的WebFlux属于响应式的实现具备更好的性能
gateway入门
步骤
创建SpringBoot工程gateway引入网关依赖编写启动类编写基础配置和路由规则启动网关服务进行测试
创建gateway服务引入依赖
!--网关--
dependencygroupIdorg.springframework.cloud/groupIdartifactIdspring-cloud-starter-gateway/artifactId
/dependency
!--nacos服务发现依赖--
dependencygroupIdcom.alibaba.cloud/groupIdartifactIdspring-cloud-starter-alibaba-nacos-discovery/artifactId
/dependency编写启动类
package com.dc.gate;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;SpringBootApplication
public class GatewayApplication {public static void main(String[] args) {SpringApplication.run(GatewayApplication.class, args);}
}编写配置文件
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/** # 这是按照路径匹配只要以/use/开头就符合要求重启测试
重启服务当访问http://localhost:10010/user/1时符合/user/**的规则请求转发到uri:http://userservice/user/1得到结果 注意若出现如下报错
***************************
APPLICATION FAILED TO START
***************************
Description:
Parameter 0 of method modifyResponseBodyGatewayFilterFactory in org.springframework.cloud.gateway.config.GatewayAutoConfiguration required a bean of type org.springframework.http.codec.ServerCodecConfigurer that could not be found.Action:
Consider defining a bean of type org.springframework.http.codec.ServerCodecConfigurer in your configuration.删除pom文件中的spring-boot-start-web依赖即可。这是因为spring cloud gateway是基于webflux的它与spring cloud网关不兼容。
网关路由的流程图
整个访问的流程如下 总结
网管搭建步骤
创建项目引入nacos服务发现和gateway依赖配置application.yml包括服务基本信息、nacos地址、路由
路由配置包括
路由id路由的唯一标识路由目标(uri)路由的目标地址http代表固定地址lb代表根据服务名负载均衡路由断言(predicates)判断路由的规则路由过滤器(filters)对请求或响应做处理
断言工厂
在配置文件中的断言规则只是字符串这些字符串会被Predicate Factory读取并处理转变为路由判断的条件
如Path/user/**是按照路径匹配的这个规则是由org.springframework.cloud.gateway.handler.predicate.PathRoutePredicateFactory类来处理的像这样的断言工厂在SpringCloudGateway还有十几个
名称说明1示例After是某个时间点后的请求- After2037-01-20T17:42:47.789-07:00[America/Denver]Before是某个时间点之前的请求- Before2031-04-13T15:14:47.43308:00[Asia/Shanghai]Between是某两个时间点之前的请求- Between2037-01-20T17:42:47.789-07:00[America/Denver], 2037-01-21T17:42:47.789-07:00[America/Denver]Cookie请求必须包含某些cookie- Cookiechocolate, ch.pHeader请求必须是指定方式- HeaderX-Request-Id, \dHost请求必须是访问某个域名host- Host.somehost.org,.anotherhost.orgMethod请求方式必须是指定方式- MethodGET,POSTPath请求路径必须符合指定规则- Path/red/{segment},/blue/**Query请求参数必须包含指定参数- Queryname, Jack或者- QuerynameRemoteAddr请求者的ip必须是指定范围- RemoteAddr192.168.1.1/24Weight权重处理
过滤器工厂
GatewayFilter是网关中提供的一种过滤器可以对进入网关的请求和微服务返回的响应做处理 路由过滤器的种类
Spring提供了31种不同的路由过滤器工厂。如
名称说明AddRequestHeader给当前请添加一个请求头RemoveRequestHeader移除请求中的一个请求头AddResponseHeader给响应结果中添加一个响应头RemoveResponseHeader从响应结果中移除一个响应头RequestRateLimiter限制请求的流量
案例 需求给所有访问userservice的请求添加一个请求头Truthhello dc 修改gateway服务的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/** # 这是按照路径匹配只要以/use/开头就符合要求filters:- AddRequestHeaderTruth, hello dc! #请求添加请求头默认过滤
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/** # 这是按照路径匹配只要以/use/开头就符合要求default-filters:- AddRequestHeaderTruth, hello dc! #请求添加请求头修改user-service服务中的UserController中的方法
GetMapping(/{id})
public User queryById(PathVariable(id) Long id, RequestHeader(Truth) String truth) {System.out.println(truth);return userService.queryById(id);
}结果 总结
过滤器的作用是什么
对路由的请求或响应做加工处理比如添加请求头配置在路由下的过滤器只对当前路由的请求生效
defaultFilters的作用是什么
对所有路由都生效的过滤器
全局过滤器作用
全局过滤器的作用是处理一切进入网关的请求和微服务响应与GatewayFilter的作用一样。区别在于GatewayFilter通过配置定义处理逻辑是固定的而GlobalFilter的逻辑需要自己写代码实现
定义方式是需要实现GlobalFilter接口
在filter中编写自定义逻辑可以实现下列功能
登录状态判断权限判断请求限制流等
自定义全局过滤器
需求定义全局过滤器拦截请求判断请求的参数是否满足下面条件
参数中是否有authorizationauthorization参数值是否为admin
如果同时满足则放行否则拦截
实现
在gateway中定义一个过滤器
package com.dc.gate.filter;import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;Order(-1)
Component
public class AuthorizeFilter implements GlobalFilter {Overridepublic MonoVoid filter(ServerWebExchange exchange, GatewayFilterChain chain) {// 1、获取请求参数MultiValueMapString, String queryParams exchange.getRequest().getQueryParams();// 2、获取authorization参数String auth queryParams.getFirst(authorization);// 校验if (admin.equals(auth)){// 放行return chain.filter(exchange);}// 拦截// 禁止访问设置状态码exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);// 结束处理return exchange.getResponse().setComplete();}
}注意order注解的作用是定义Spring IOC容器中Bean的执行顺序的优先级而不是定义Bean的加载顺序Bean的加载顺序不受Order或Ordered接口的影响 order默认是最低优先级值越小优先级越高 此时在访问就会出现禁止访问的提示 如果在请求地址中加上authorizationadmin的条件就可以访问 过滤器执行顺序
请求进入网关会碰到三类过滤器当前路由的过滤器、DefaultFilter、GlobalFilter请求路由后会将当前路由过滤器和DefaultFilter、GlobalFilter合并到一个过滤器链集合中排序后依次执行每个过滤器 排序的规则
每个过滤器都必须指定一个int类型的order值order值越小优先级越高执行顺序越靠前GlobalFilter通过实现Ordered接口或者添加Order注解来指定order值由我们自己指定路由过滤器和defaultFilter的order由Spring指定默认是按照声明顺序从1递增当过滤器的order值一样时会按照defaultFilter 路由过滤器 GlobalFilter的顺序执行
跨域问题
跨域域名不一致就是跨域主要包括
域名不同www.taobao.com 和 www.taobao.org 和 www.jd.com 和 miaosha.jd.com域名相同端口不同localhost:8080和localhost8081
跨域问题浏览器禁止请求的发起者与服务端发生跨域ajax请求请求被拦截器拦截问题
解决跨域问题
在gateway服务的applicaiton.yml文件中添加如下配置
server:port: 10010 #网关端口
spring:application:name: gateway #服务名称cloud:nacos:server-addr: localhost:8848 #nacos地址gateway:# 。。。globalcors: # 全局的跨域处理add-to-simple-url-handler-mapping: true # 解决options请求被拦截问题corsConfigurations:[/**]:allowedOrigins: # 允许哪些网站的跨域请求- http://localhost:8090allowedMethods: # 允许的跨域ajax的请求方式- GET- POST- DELETE- PUT- OPTIONSallowedHeaders: * # 允许在请求中携带的头信息allowCredentials: true # 是否允许携带cookiemaxAge: 360000 # 这次跨域检测的有效期routes: #网关路由配置- id: user-service #路由id自定义只要唯一即可# uri: http://127.0.0.1:8081 # 路由的目标地址 http就是固定地址uri: lb://userservice #路由的目标地址 lb是负载均衡后面是服务名称predicates: # 路由断言也就是判断请求是否符合路由规则的条件- Path/user/** # 这是按照路径匹配只要以/use/开头就符合要求filters:- AddRequestHeaderTruth, hello dc! #请求添加请求头前端页面
!DOCTYPE html
html langen
headmeta charsetUTF-8meta nameviewport contentwidthdevice-width, initial-scale1.0meta http-equivX-UA-Compatible contentieedgetitleDocument/title
/head
body
/body
script srchttps://unpkg.com/axios/dist/axios.min.js/script
scriptaxios.get(http://localhost:10010/user/1?authorizationadmin).then(resp console.log(resp.data)).catch(err console.log(err))
/script
/htmlDocker实用
简介
微服务虽然具备各种各样的优势但服务的拆分通用给部署带来了很多的麻烦
分布式系统中依赖的组件非常多不同组件之间部署时往往会产生一些冲突在数百上千台服务中重复部署环境不一定一致会遇到各种问题
应用部署的环境问题
大型项目组件较多运行环境也较为复杂部署时会遇到一些问题如
依赖关系复杂容易出现兼容性问题开发、测试、生产环境有差异
docker解决依赖兼容问题
两种手段
将应用的libs(函数库)、deps(依赖)、配置与应用一起打包将每个应用放到一个隔离容器中运行避免互相干扰
这样打包好的应用包中既包含应用本身也保护应用所需要的libs、deps无需在操作系统上安装这些自然就不存在不同应用之间的兼容问题
docker解决操作系统环境差异
首先介绍一下操作系统的结构
Ubuntu操作系统
计算机硬件CPU、内存、磁盘等系统内核所有Linux发行版的内核都是Linux例如CentOS、Ubuntu、Fedora等。内核可以与计算机硬件交互对外提供内核命令用于操作计算机硬件系统应用操作系统本身提供的应用、函数库。这些函数库是对内核指令的封装使用更加方便
应用于计算机交互的流程
应用调用操作系统应用(函数库)实现各种功能系统函数是对内核指令集的封装会调用内核指令内核指令操作计算机硬件
Ubuntu和CentOS都是基于Linux内核无非是系统应用不同提供的函数库有差异
如果将一个Ubuntu版本的MySQL应用安装到CentOS系统MySQL在调用Ubuntu函数库时会发现找不到或者不匹配就会报错
docker解决不同系统环境的问题
docker将用户程序于所需要调用的系统函数库一起打包docker运行到不同操作系统时直接基于打包的函数库借助于操作系统的Linux内核来运行
总结
docker如何解决大型项目依赖关系复杂和不同组件依赖的兼容性问题
docker允许开发中将应用、依赖、函数库、配置一起打包形成可移植镜像docker应用运行在容器中使用沙箱机制相互隔离
docker如何解决开发、测试、生产环境有差异的问题
docker镜像中包含完整运行环境包括系统函数库仅依赖系统的Linux内核因此可以在任意Linux操作系统上运行
docker是一个快速交付应用、运行应用的技术具备下列优势
可以将程序及其依赖、运行环境一起打包为一个镜像可以迁移到任意Linux操作系统运行时利用沙箱机制形成隔离容器各个应用互不干扰启动、移除都可以通过一行命令完成方便快捷
docker和虚拟机的区别
虚拟机(virtual machine)是在操作系统中模拟硬件设备然后运行另一个操作系统比如在windows系统中运行Ubuntu系统这样就可以运行任意的ubuntu应用了
docker仅仅是封装函数库并没有模拟完整的操作系统 对比
特性docker虚拟机性能接近原生性能较差硬盘占用一般为MB一般为GB启动妙级分钟级
docker和虚拟机的差异 docker是一个系统进程虚拟机是在操作系统中的操作系统 docker体积小、启动速度快、性能好虚拟机体积大、启动速度慢、性能一般
docker架构
概念
镜像(Image):docker将应用程序及其所需的依赖、函数库、环境、配置等文件打包在一起称为镜像
容器(Container):镜像中的应用程序运行后形成的进程就是容器只是docker会给容器进程做隔离对外不可见
一切应用最终都是代码组成都是硬盘中的一个个的字节形成的文件。只有运行时才会加载到内存形成进程
镜像就是把一个应用在硬盘上的文件、及其运行环境、部分系统函数库文件一起打包形成的文件包。这个文件包是只读的
容器是将这些文件中编写的程序、函数加载到内存中允许形成进程并且需要隔离。因此一个镜像可以启动多次形成多个容器进程
DockerHub
开源的应用有很多但是打包这些应用是重复且乏味的劳动。因此就出现了镜像托管的网站
Dockerhub:DockerHub是一个官方的Docker镜像的托管平台。这种平台称为Docker Registry国内类似的公开服务比如网易云镜像、阿里云镜像等
可以拉取自己需要的镜像
Docker架构
Docker是一个CS架构的程序有两个部分组成
服务端(server):Docker守护进行负责处理Docker指令管理镜像、容器等客户端(client)通过命令或RestAPI向Docker服务端发送指令。可以在本地或远程向服务端发送指令 Docker安装
想要安装docker需要先将依赖的环境下载默认下载的地址是国外的服务器速度较慢可以设置为阿里云的镜像源速度会更快
yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo采用yum的方式安装
yum makecache fast
yum -y install docker-ce注意docker应用到各种端口需要逐一去修改防火墙设置。这里直接关闭防火墙 # 关闭
systemctl stop firewalld
# 禁止开机启动防火墙
systemctl disable firewalld测试
安装成功后需要手动启动设置为开启自启并测试一下docker
# 启动Docker服务
systemctl start docker
# 设置开机自动启动
systemctl enable docker
# 测试 运行hello-world 镜像 根据这个镜像 创建容器
docker run hello-world基本操作
镜像名称
首先镜像的名称组成
镜像名称一般分为两个部分[repository]:[tag]在没有指定tag时默认是latest代表最新版本的镜像
镜像命令
常见的镜像命令如图 案例 需求从dockerhub中拉去一个nginx镜像并查看 步骤
首先去镜像仓库中搜索nginx镜像比如https://hub.docker.com根据查看到的镜像名称拉取自己需要的镜像通过命令docker pull nginx通过命令docker images 查看拉取到的镜像 需求利用docker save 将nginx镜像导出磁盘然后通过load加载回来 步骤 查看save命令用法可以输出命令 docker save --help命令格式 docker save -o [保存的目标文件名称] [镜像名称]使用docker save导出镜像到磁盘 docker save -o nginx.tar nginx:latest使用docker 加载load加载镜像 docker rmi nginx:latest然后运行命令加载本地文件 docker load -i nginx.tar容器操作
容器相关命令 容器保护三个状态
运行进程正常运行暂停进程暂停、CPU不再运行并不释放内存停止进程停止回收进程占用的内存、cpu等资源
其中
docker run创建并运行一个容器处于运行状态docker pause让一个运行的容器暂停docker unpause:r让一个容器从暂停状态恢复运行docker stop停止一个运行的容器docker start让一个停止的容器再次运行docker rm删除一个容器 需求创建并运行一个容器 docker run --name containerName -p 80:80 -d nginx命令解读
docker run创建并运行一个容器–name给容器一个名字-p将宿主机端口与容器端口映射冒号左侧是宿主机端口右侧是容器端口-d后台运行容器nginx镜像名称
这个命令是将容器和宿主机关联这样当访问宿主机是就可以映射到容器中 需求进入nginx容器修改HTML文件内容 步骤 进入容器 docker exec -it mn bash命令解读 docker exec进入容器内部执行一个命令-it给当前进入的容器创建一个标准输入、输出终端允许与容器交互mn要进入的容器的名称bash进入容器后执行的命令bash是一个linux终端交互命令 进入nginx的HTML文件所在目录/usr/share/nginx/html 进入该目录 cd /usr/share/nginx/html修改index.html的内容 sed -i -e s#Welcome to nginx#HELLO#g -e s#head#headmeta charsetutf-8#g index.html数据卷
数据卷(volume)是一个虚拟目录指向宿主机文件系统中的某个目录
一旦完成数据卷挂载对容器的一切操作都会作用在数据卷对应的宿主机目录
操作命令
docker volume [COMMAND]docker volume命令是数据卷操作根据命令后跟随的command来确定下一步的操作
create常见一个volumeinspect显示一个或多个volume的信息ls列出所有的volumeprune删除未使用的volumerm删除一个或多个指定的volume
创建和查看数据卷 需求创建一个数据卷并查看数据卷在宿主机的目录位置 创建数据卷 docker volume create html查看所有数据 docker volume ls查看数据卷的全部信息 docker volume inspect html挂载数据卷
通过-v参数来挂在一个数据卷到某个容器内目录命令格式
docker run \--name mn \-v html:/root/html \-p 8080:80nginx \-v就是挂载数据卷的命令
-v html:/root/html把html数据卷挂载到容器内的/root/html这个目录中
Docker-Compose
下载Docker-Compose 去github官网搜索docker-compose下载1.24.1版本的Docker-Compose 下载路径https://github.com/docker/compose/releases/download/1.24.1/docker-compose-Linux-x86_64 设置权限 需要将DockerCompose文件的名称修改一下给予DockerCompose文件一个可执行的权限 mv docker-compose-Linux-x86_64 docker-compose
chmod 777 docker-compose配置环境变量 方便后期操作配置一个环境变量 将docker-compose文件移动到了/usr/local/bin , 修改了/etc/profile文件给/usr/local/bin配置到了PATH中 mv docker-compose /usr/local/binvi /etc/profile
# 添加内容 export PATH$JAVA_HOME:/usr/local/bin:$PATHsource /etc/profile安装方式(手动)
# 直接联网下载到本地 /usr/local/bin/docker-compose
curl -L https://get.daocloud.io/docker/compose/releases/download/1.26.2/docker-compose-uname -s-uname -m /usr/local/bin/docker-composecd /usr/local/bin # 进入该目录
chmod 777 docker-compose # 给这个文件授权# 在任意目录 测试 docker-compose 命令测试 在任意目录下输入docker-compose 测试效果
Docker-Compose管理MySQL和Tomcat容器 yml文件以key: value方式来指定配置信息 多个配置信息以换行缩进的方式来区分 在docker-compose.yml文件中不要使用制表符 version: 3.1
services:mysql: # 服务的名称restart: always # 代表只要docker启动那么这个容器就跟着一起启动image: daocloud.io/library/mysql:5.7.4 # 指定镜像路径container_name: mysql # 指定容器名称ports:- 3306:3306 # 指定端口号的映射environment:MYSQL_ROOT_PASSWORD: root # 指定MySQL的ROOT用户登录密码TZ: Asia/Shanghai # 指定时区volumes:- /opt/docker_mysql_tomcat/mysql_data:/var/lib/mysql # 映射数据卷tomcat:restart: alwaysimage: daocloud.io/library/tomcat:8.5.15-jre8container_name: tomcatports:- 8080:8080environment:TZ: Asia/Shanghaivolumes:- /opt/docker_mysql_tomcat/tomcat_webapps:/usr/local/tomcat/webapps- /opt/docker_mysql_tomcat/tomcat_logs:/usr/local/tomcat/logs使用docker-compose命令管理容器 在使用docker-compose的命令时 默认会在当前目录下找docker-compose.yml文件 # 1. 基于docker-compose.yml启动管理的容器
docker-compose up -d# 2. 关闭并删除容器
docker-compose down# 3. 开启|关闭|重启已经存在的由docker-compose维护的容器
docker-compose start|stop|restart# 4. 查看由docker-compose管理的容器
docker-compose ps# 5. 查看日志
docker-compose logs -fRabbitMQ
介绍
微服务间的通讯有同步和异步两种方式
同步通讯需要实时响应如打电话
异步通讯不需要马上响应如邮件
优缺点
同步虽然可以立即响应但是不能跟多个人同时通话
异步可以给多个人同时发邮件但是响应会有延迟
同步通讯
Feign调用就是同步方式虽然可以实时得到结果但存在以下问题 总结
同步调用的优点
时效性较强可以立即得到结果
同步调用的问题
耦合度强性能和吞吐能力下降有额外的资源消耗有级联失效问题
异步通讯
异步调用可以避免上述问题:
例如发布者(publisher)和订阅者(consumer)问题。
为了解除事件订阅者和发布者之间的耦合两者并不是直接通讯的而是通过中间人Broker)。发布者发布事件到broker不关心谁来订阅事件。订阅者从broker订阅事件不关心谁发来消息 Broker是一个数据总线一样的东西所有的服务器要接收数据和发送数据都发到这个总线上这个总线就像协议一样让服务间的通讯变得标准和可控
好处
吞吐量提升无需等待订阅者处理完成响应更快速故障隔离服务没有直接调用不存在级联失效问题耦合度极低每个服务都可以灵活插拔可替换流量削峰不管发布事件的流量波动多大都有broker接收订阅者可以按照自己的速度处理事件
缺点
架构复杂业务没有明显的流程线不好管理需要依赖于broker的可靠、安全、性能
技术对比
MQ消息队列(MessageQueue就是存放消息的队列。也就是事件驱动架构中的broker
常见的MQ实现
ActiveMQRabbitMQRocketMQKafka
几种常见MQ的对比
RabbitMQActiveMQRocketMQKafka公司/社区RabbitApache阿里Apache开发语言ErlangJavaJavaScalaJava协议支持AMQP、XMPP、SMTP、STOMPOpenWire、STOMP、REST、XMPP、AMQP自定义协议自定义协议可用性高一般高高单机吞吐量一般差高非常高消息延迟微秒级毫秒级毫秒级毫秒以内消息可靠性高一般高一般
追求可用性Kafka、RocketMQ、RabbitMQ
追求可靠性RabbitMQ、RocketMQ
快速入门
RabbitMQ消息模型
基本结构 RabbitMQ中的一些角色 publisher生产者 consumer消费者 exchange交换机负责消息路由 queue队列存储消息 virtualHost虚拟主机隔离不同租户的exchange、queue、消息的隔离
入门案例
简单队列模式的模型图 官方的helloworld是基于最基础的消息队列模型来实现的只包括三个角色
publisher消息发布者、将消息发送到队列queuequeue消息队列负责接收并缓存消息consumer订阅队列处理队列中的消息
publisher实现
思路
建立连接创建Channel声明队列发送消息关闭连接hechannel
代码实现
package com.dc.mqtest.helloworld;import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import org.junit.Test;
import org.springframework.boot.test.context.SpringBootTest;import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class PublsherTest {Testpublic void testSendMessage() throws IOException, TimeoutException {// 1.建立连接ConnectionFactory factory new ConnectionFactory();// 1.1.设置连接参数分别是主机名、端口号、vhost、用户名、密码factory.setHost(127.0.0.1);factory.setPort(5672);factory.setVirtualHost(/);factory.setUsername(guest);factory.setPassword(guest);// 1.2.建立连接Connection connection factory.newConnection();// 2.创建通道ChannelChannel channel connection.createChannel();// 3.创建队列String queueName simple.queue;channel.queueDeclare(queueName, false, false, false, null);// 4.发送消息String message hello, rabbitmq!;channel.basicPublish(, queueName, null, message.getBytes());System.out.println(发送消息成功【 message 】);// 5.关闭通道和连接channel.close();connection.close();}
}consumer实现
思路
建立连接创建channel声明队列订阅消息
代码实现
package com.dc.mqtest.helloworld;import com.rabbitmq.client.*;import java.io.IOException;
import java.util.concurrent.TimeoutException;public class ConsumerTest {public static void main(String[] args) throws IOException, TimeoutException {// 1.建立连接ConnectionFactory factory new ConnectionFactory();// 1.1.设置连接参数分别是主机名、端口号、vhost、用户名、密码factory.setHost(127.0.0.1);factory.setPort(5672);factory.setVirtualHost(/);factory.setUsername(guest);factory.setPassword(guest);// 1.2.建立连接Connection connection factory.newConnection();// 2.创建通道ChannelChannel channel connection.createChannel();// 3.创建队列String queueName simple.queue;channel.queueDeclare(queueName, false, false, false, null);// 4.订阅消息channel.basicConsume(queueName, true, new DefaultConsumer(channel){Overridepublic void handleDelivery(String consumerTag, Envelope envelope,AMQP.BasicProperties properties, byte[] body) throws IOException {// 5.处理消息String message new String(body);System.out.println(接收到消息【 message 】);}});System.out.println(等待接收消息。。。。);}
}消息模式
点对点消息
发送端直接把消息发送到队列中消费者直接从队列中获取消息消息只能消费一次 代码展示
创建消息队列
package com.dc.mptest01.config;import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;Configuration
public class RabbitCreateConfig {// 创建RabbitMQ的队列 存储消息Beanpublic Queue createQ() {return new Queue(java);}
}实现消息发送
package com.dc.mptest01.controller;import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;RestController
RequestMapping(/api/mq/)
public class RabbitMQSendController {Autowiredprivate RabbitTemplate rabbitTemplate;GetMapping(/send)public String sendMsg(String msg){/*** 发送消息* 参数说明 * 交换器* 路由关键字或队列名称* 消息内容*/rabbitTemplate.convertAndSend(,java,msg);return ok;}}实现消息消费
package com.dc.mptest01.listener;import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;Slf4j
Component
public class ReceiveListener {/*** RabbitListener 设置需要监听的队列名* 一旦队列有了消息修饰的方法自动执行* 方法的参数消息的数据类型*/RabbitListener(queues java)public void handler(String msg) {log.info(接收消息:{}, msg);}}工作队列消息
一个队列可以有多个消费端消息只能被消费一次
核心一个队列有多个消费者 代码展示
创建消息队列
package com.dc.mptest01.config;import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;Configuration
public class RabbitCreateConfig {// 创建RabbitMQ的队列 存储消息Beanpublic Queue createQ() {return new Queue(java);}Beanpublic Queue createQ1(){return new Queue(java-m-01);}
}实现消息发送
package com.dc.mptest01.controller;import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;RestController
RequestMapping(/api/mq/)
public class RabbitMQSendController {Autowiredprivate RabbitTemplate rabbitTemplate;GetMapping(/send)public String sendMsg(String msg){/*** 发送消息* 参数说明 * 交换器* 路由关键字或队列名称* 消息内容*/rabbitTemplate.convertAndSend(,java,msg);return ok;}GetMapping(/send/{msg})public String send(PathVariable String msg) {rabbitTemplate.convertAndSend(, java-m-01, msg - System.currentTimeMillis());return ok;}
}实现消息消费
package com.dc.mptest01.listener;import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;Slf4j
Component
public class ReceiveListener {/*** RabbitListener 设置需要监听的队列名* 一旦队列有了消息修饰的方法自动执行* 方法的参数消息的数据类型*/RabbitListener(queues java)public void handler(String msg) {log.info(接收消息:{}, msg);}RabbitListener(queues java-m-01)public void handler01(String msg) {log.info(接收消息:{}, msg);}}发布订阅消息
发送端发送消息给交换器(Exchange-Fanout)交换器再把消息发送到绑定的队列(可能有一个或多个)每个队列又有自己的消费端所以最终实现一个消息可以被消费多次 代码展示
创建交换器、消息队列、绑定
Configuration
public class MqInitConfig3 {/**1.创建交换器 发布定义 Exchange-Fanout直接转发*/Beanpublic FanoutExchange createFE(){return new FanoutExchange(java-fanout-dc);}/**2.创建队列*/Beanpublic Queue createQ3(){return new Queue(java-m-03);}Beanpublic Queue createQ4(){return new Queue(java-m-04);}/**3.实现绑定*/Beanpublic Binding createBd1(FanoutExchange fe){return BindingBuilder.bind(createQ3()).to(fe);}Beanpublic Binding createBd2(FanoutExchange fe){return BindingBuilder.bind(createQ4()).to(fe);}
}实现消息发送
RestController
RequestMapping(/api/mq3/)
public class SendController3 {Resourceprivate RabbitTemplate template;GetMapping(send/{msg})public String send(PathVariable String msg){template.convertAndSend(java-fanout-dc,,msg-System.currentTimeMillis());return OK;}
}实现消息消费
Component
Slf4j
public class MqListener4 {RabbitListener(queues java-m-03)public void handler(String msg){log.info(发布订阅消息队列03消息内容{},msg);}
}Component
Slf4j
public class MqListener5 {RabbitListener(queues java-m-04)public void handler(String msg){log.info(发布订阅消息队列04消息内容{},msg);}
}路由模式
发送端发送消息给交换器交换器根据消息的路由关键字匹配对应的队列每个队列又有自己的消费端所以最终会实现一个消息被多次消费 代码展示
创建交换器、消息队列、绑定
package com.dc.mptest01.config;import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;Configuration
public class RabbitCreateConfig {/*** 创建队列*/Beanpublic Queue create2() {return new Queue(java-m-02);}Beanpublic Queue create3() {return new Queue(java-m-03);}/*** 创建交换器路由匹配*/Beanpublic DirectExchange createDE() {return new DirectExchange(java-direct-dc);}/*** 实现绑定*/Beanpublic Binding createBd3(DirectExchange de) {return BindingBuilder.bind(create3()).to(de).with(red);}Beanpublic Binding createBd4(DirectExchange de) {return BindingBuilder.bind(create2()).to(de).with(blue);}
}消息发送
package com.dc.mptest01.controller;import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import javax.annotation.Resource;RestController
RequestMapping(/api/mq)
Slf4j
public class RabbitMQSendController {Resourceprivate RabbitTemplate rabbitTemplate;GetMapping(/send/{msg}/{rk})public String send(PathVariable String msg, PathVariable String rk) {rabbitTemplate.convertAndSend(java-direct-dc, rk, msg System.currentTimeMillis());return ok;}}消息消费
package com.dc.mptest01.listener;import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.FanoutExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;/*** -----在希望中绽放在苦难中坚持------** author 暮辰*/
Slf4j
Component
public class ReceiveListener {RabbitListener(queues java-m-02)public void handle1(String msg){log.info(路由消息,red,消息内容:{}, msg);}RabbitListener(queues java-m-03)public void handle2(String msg) {log.info(路由消息blue消息内容{}, msg);}
}主题模式
发送端发送消息给交换器交换器根据消息的路由关键字匹配对应的队列
注意路由关键字匹配支持模糊匹配
*表示一个单词
#表示任意个单词内容随意 代码展示
创建交换器、消息队列、绑定
package com.dc.mptest01.config;import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;/*** -----在希望中绽放在苦难中坚持------** author 暮辰*/
Configuration
public class RabbitCreateConfig {/*** 创建队列*/Beanpublic Queue create2() {return new Queue(java-m-02);}Beanpublic Queue create3() {return new Queue(java-m-03);}/*** 创建交换器路由匹配*/Beanpublic TopicExchange createDE() {return new TopicExchange(java-topic-dc);}/*** 实现绑定*/Beanpublic Binding createBd3(TopicExchange de) {return BindingBuilder.bind(create3()).to(de).with(stu.*);}Beanpublic Binding createBd4(TopicExchange de) {return BindingBuilder.bind(create2()).to(de).with(tea.#);}
}消息发送
package com.dc.mptest01.controller;import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import javax.annotation.Resource;RestController
RequestMapping(/api/mq)
Slf4j
public class RabbitMQSendController {Resourceprivate RabbitTemplate rabbitTemplate;GetMapping(/send/{msg}/{rk})public String send(PathVariable String msg, PathVariable String rk) {rabbitTemplate.convertAndSend(java-topic-dc, rk, msg System.currentTimeMillis());return ok;}}消息消费
package com.dc.mptest01.listener;import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.FanoutExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;Slf4j
Component
public class ReceiveListener {RabbitListener(queues java-m-02)public void handle1(String msg){log.info(主题消息,stu.*,消息内容:{}, msg);}RabbitListener(queues java-m-03)public void handle2(String msg) {log.info(主题消息tea.#消息内容{}, msg);}
}事务
RabbitMQ也支持事务一般用来保证消息的发送。如果出现异常事务就会回滚如果没有出现异常事务就会提交消息就会被发送成功。也可以一次发送多个消息为了保证多个消息的一致性也需要开启事务
RabbitMQ的事务事务可以保证消息传递通过事务的回滚记录日志之后定时发送当前消息。
实现步骤
创建消息队列、事务管理器
package com.dc.mptest01.config;import org.springframework.amqp.core.Queue;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.transaction.RabbitTransactionManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;Configuration
public class MqInitConfig {// 创建队列Beanpublic Queue createQ() {return new Queue(java-m-tran);}/*** 创建事务管理对象*/Beanpublic RabbitTransactionManager createRTM(ConnectionFactory factory) {return new RabbitTransactionManager(factory);}
}发送消息-开启事务
package com.dc.mptest01.controller;import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;RestController
RequestMapping(/api/mq)
public class SendController1 {Autowiredprivate RabbitTemplate rabbitTemplate;TransactionalGetMapping(/send/{msg}/{num})public String send(PathVariable String msg, PathVariable int num) {// 开启RabbitMQ事务rabbitTemplate.setChannelTransacted(true);for (int i 0; i num; i) {if (i 1) {System.err.println(1 / 0); // 模拟异常}rabbitTemplate.convertAndSend(, java-m-tran, msg-i-System.currentTimeMillis());}return OK;}
}消息消费
package com.dc.mptest01.listener;import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;Component
Slf4j
public class MqListener1 {RabbitListener(queues java-m-tran)public void handler(String msg) {log.info(事务消息演示事务消息内容:{}, msg);}
}消费端—手动ACK
为了保证消息从队列可靠的达到消费者RabbitMQ提供了消息确认机制。消费者在订阅队列时可以指定autoACK参数当AutoAck等于false时RabbitMQ会等待消费者显示的回复确认信号后才从内存中移除消息(实际是先打上删除标记之后再删除)。当autoAck等于true时RabbitMQ会自动把发送出去的消息置为确认然后从内存中删除而不管消费者是否真正的消费到了这些消息
采用消息确认机制后只要设置autoAck参数为false消费者就有足够的时间处理消息不用担心处理消息过程中消费者进程挂掉后消息丢失的问题因为RabbitMQ会一直等待持有消息直到消费者显示调用Basic.Ack命令为止
实现步骤
在配置文件中设置手动应答 spring:rabbitmq:host: localhostport: 5672username: guestpassword: guestvirtual-host: /listener: # 监听器 消息消费设置simple:acknowledge-mode: manual # 手动确认 ackpublisher-confirm-type: simple # 通过确认机制publisher-returns: true # 重新发送监听server:port: 8686编写代码初始化创建、消息发送
package com.dc.mptest01.config;import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;/*** -----在希望中绽放在苦难中坚持------** author 暮辰*/
Configuration
public class MqInitConfig2 {Beanpublic Queue create() {return new Queue(java-m-ack);}
}package com.dc.mptest01.controller;import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;RestController
RequestMapping(/api/mq)
public class SendController2 {Autowiredprivate RabbitTemplate rabbitTemplate;GetMapping(/send/{msg})public String send(PathVariable String msg) {rabbitTemplate.convertAndSend(, java-m-ack, msgSystem.currentTimeMillis());return ok;}}消息消费-手动应答
package com.dc.mptest01.listener;import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;import java.io.IOException;Component
Slf4j
public class MqListener2 {RabbitListener(queues java-m-ack)public void handler(String msg, Channel channel, Message message) throws IOException {// 手动应答log.info(获取消息:{} , msg);// 消息成功 RabbitMQ就会删除消息
// channel.basicAck(message.getMessageProperties().getDeliveryTag(), true);//拒绝消息 RabbitMQ就不会删除消息 参数说明1.消息的唯一id 2.是的应答 false:拒绝 3.是否把消息重新放回到队列 true:放回 下次继续消费 false:unAckedchannel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);}
}死信和延迟队列
死信RabbitMQ的消息队列中的消息如果满足一定的条件就会变成死信死信不会被消费只能转发到死信转发器
死信交换器DLX(Dead-Letter-Exchange)当消息在一个队列中变成死信之后被重新发送到另一个交换器中这个交换器就是DLX绑定DLX的队列就称为死信队列
死信产生的条件
消息被拒绝消息过期队列达到最大长度
如果使用死信队列的话需要在定义队列中设置队列参数x-dead-letter-exchange
RabbitMQ支持两种ttl(有效期)设置
单独消息进行配置ttl整个队列进行配置ttl(居多)
如果队列也有有效期消息也有有效期以时间短的为准
实际应用的业务超时自动处理的业务(订单超时未支付自动取消) 代码展示
创建队列、交换器、绑定
package com.dc.mptest01.config;import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;import java.util.HashMap;Configuration
public class RabbitInitConfig {/*** 创建队列* 生成死信有效期 设置死信交换器*/Beanpublic Queue create() {HashMapString, Object map new HashMap();// 设置有效期 15秒map.put(x-message-ttl, 15000);// 设置 对应的死信交换器的名称map.put(x-dead-letter-exchange, dead-exchange);// 设置 死信消息的路由关键字map.put(x-dead-letter-routing-key, first);// 创建消息队列return QueueBuilder.durable(x-dead-ttl).withArguments(map).build();}// 接收死信消息Beanpublic Queue create1() {return new Queue(x-dead-msg);}/*** 创建死信交换器* 死信交换 路由模式*/Beanpublic DirectExchange createDe() {return new DirectExchange(dead-exchange);}/*** 绑定 实现消息从交换器到队列*/Beanpublic Binding createB1(DirectExchange de) {return BindingBuilder.bind(create1()).to(de).with(first);}
}实现消息发送
package com.dc.mptest01.controller;import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;RestController
RequestMapping(/api/mq)
public class DeadMsgController {Autowiredprivate RabbitTemplate rabbitTemplate;// 队列的有效期GetMapping(/send/{msg})public String send(PathVariable String msg) {rabbitTemplate.convertAndSend(, x-dead-ttl, msg-System.currentTimeMillis());return OK;}// 消息的有效期GetMapping(/send2/{msg})public String send2(PathVariable String msg) {// 消息对象Message message new Message((msg - System.currentTimeMillis()).getBytes());// 设置消息的有效期message.getMessageProperties().setExpiration(25000);rabbitTemplate.convertAndSend(x-dead-ttl, message);return ok;}
}实现消息消费
package com.dc.mptest01.listener;import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;import java.util.HashMap;Component
Slf4j
public class DelayMsgListener {RabbitListener(queues x-dead-msg)public void handler(String msg) {log.info(延迟时间:{}, {}, msg, System.currentTimeMillis());}
}面试总结
生产者丢失消息
生产者将数据发送到rabbitmq的时候可能因为网络问题导致数据就在半路给搞丢了 1.使用事务性能差
RabbitMQ 客户端中与事务机制相关的方法有三个 channel.txSelect 、channel.txCommit 和 channel.txRollback。channel.txSelect 用于将当前的信道设置成事务模式channel.txCommit 用于提交事务channel.txRollback 用于事务回滚。在通过 channel.txSelect 方法开启事务之后我们便可以发布消息给 RabbitMQ 了如果事务提交成功则消息一定到达了 RabbitMQ 中如果在事务提交执行之前由于 RabbitMQ异常崩溃或者其他原因抛出异常这个时候我们便可以将其捕获进而通过执行channel.txRollback 方法来实现事务回滚。注意这里的 RabbitMQ 中的事务机制与大多数数据库中的事务概念并不相同需要注意区分。
事务确实能够解决消息发送方和 RabbitMQ 之间消息确认的问题只有消息成功被RabbitMQ 接收事务才能提交成功否则便可在捕获异常之后进行事务回滚与此同时可以进行消息重发。但是使用事务机制会“吸干”RabbitMQ 的性能。
报文
2.发送回执确认推荐
生产者将信道设置成 confirm确认模式一旦信道进入 confirm 模式所有在该信道上面发布的消息都会被指派一个唯一的 ID从 1 开始一旦消息被投递到所有匹配的队列之后RabbitMQ 就会发送一个确认Basic.Ack给生产者包含消息的唯一 ID这就使得生产者知晓消息已经正确到达了目的地了。如果消息和队列是可持久化的那么确认消息会在消息写入磁盘之后发出。RabbitMQ 回传给生产者的确认消息中的 deliveryTag 包含了确认消息的序号此外 RabbitMQ 也可以设置 channel.basicAck 方法中的 multiple 参数表示到这个序号之前的所有消息都已经得到了处理注意辨别这里的确认和消费时候的确认之间的异同。
2.RabbitMQ弄丢了数据
为了防止rabbitmq自己弄丢了数据这个你必须开启rabbitmq的持久化就是消息写入之后会持久化到磁盘哪怕是rabbitmq自己挂了恢复之后会自动读取之前存储的数据一般数据不会丢。除非极其罕见的是rabbitmq还没持久化自己就挂了可能导致少量数据会丢失的但是这个概率较小。
设置持久化有两个步骤第一个是创建queue的时候将其设置为持久化的这样就可以保证rabbitmq持久化queue的元数据但是不会持久化queue里的数据第二个是发送消息的时候将消息的deliveryMode设置为2就是将消息设置为持久化的此时rabbitmq就会将消息持久化到磁盘上去。必须要同时设置这两个持久化才行rabbitmq哪怕是挂了再次重启也会从磁盘上重启恢复queue恢复这个queue里的数据。
而且持久化可以跟生产者那边的confirm机制配合起来只有消息被持久化到磁盘之后才会通知生产者ack了所以哪怕是在持久化到磁盘之前rabbitmq挂了数据丢了生产者收不到ack你也是可以自己重发的。
若生产者那边的confirm机制未开启的情况下哪怕是你给rabbitmq开启了持久化机制也有一种可能就是这个消息写到了rabbitmq中但是还没来得及持久化到磁盘上结果不巧此时rabbitmq挂了就会导致内存里的一点点数据会丢失。
3.消费端弄丢了数据
为了保证消息从队列可靠地达到消费者RabbitMQ 提供了消息确认机制message acknowledgement。消费者在订阅队列时可以指定 autoAck 参数当 autoAck 等于 false时RabbitMQ 会等待消费者显式地回复确认信号后才从内存或者磁盘中移去消息实质上是先打上删除标记之后再删除。当 autoAck 等于 true 时RabbitMQ 会自动把发送出去的消息置为确认然后从内存或者磁盘中删除而不管消费者是否真正地消费到了这些消息。
采用消息确认机制后只要设置 autoAck 参数为 false消费者就有足够的时间处理消息任务不用担心处理消息过程中消费者进程挂掉后消息丢失的问题因为 RabbitMQ 会一直等待持有消息直到消费者显式调用 Basic.Ack 命令为止。
总结RabbitMQ消息可靠性保障策略
1、生产者开启消息确认机制
2、消息队列数据持久化
3、消费者手动ack
4、生产者消息记录定期补偿机制
5、服务幂等处理
6、消息积压处理等 dSend(“”, “x-dead-ttl”, msg“-”System.currentTimeMillis()); return “OK”; }
// 消息的有效期
GetMapping(/send2/{msg})
public String send2(PathVariable String msg) {// 消息对象Message message new Message((msg - System.currentTimeMillis()).getBytes());// 设置消息的有效期message.getMessageProperties().setExpiration(25000);rabbitTemplate.convertAndSend(x-dead-ttl, message);return ok;
}} 实现消息消费java
package com.dc.mptest01.listener;import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;import java.util.HashMap;Component
Slf4j
public class DelayMsgListener {RabbitListener(queues x-dead-msg)public void handler(String msg) {log.info(延迟时间:{}, {}, msg, System.currentTimeMillis());}
}面试总结
生产者丢失消息
生产者将数据发送到rabbitmq的时候可能因为网络问题导致数据就在半路给搞丢了
[外链图片转存中…(img-sRUfGMVd-1691365835666)]
1.使用事务性能差
RabbitMQ 客户端中与事务机制相关的方法有三个 channel.txSelect 、channel.txCommit 和 channel.txRollback。channel.txSelect 用于将当前的信道设置成事务模式channel.txCommit 用于提交事务channel.txRollback 用于事务回滚。在通过 channel.txSelect 方法开启事务之后我们便可以发布消息给 RabbitMQ 了如果事务提交成功则消息一定到达了 RabbitMQ 中如果在事务提交执行之前由于 RabbitMQ异常崩溃或者其他原因抛出异常这个时候我们便可以将其捕获进而通过执行channel.txRollback 方法来实现事务回滚。注意这里的 RabbitMQ 中的事务机制与大多数数据库中的事务概念并不相同需要注意区分。
事务确实能够解决消息发送方和 RabbitMQ 之间消息确认的问题只有消息成功被RabbitMQ 接收事务才能提交成功否则便可在捕获异常之后进行事务回滚与此同时可以进行消息重发。但是使用事务机制会“吸干”RabbitMQ 的性能。
报文
2.发送回执确认推荐
生产者将信道设置成 confirm确认模式一旦信道进入 confirm 模式所有在该信道上面发布的消息都会被指派一个唯一的 ID从 1 开始一旦消息被投递到所有匹配的队列之后RabbitMQ 就会发送一个确认Basic.Ack给生产者包含消息的唯一 ID这就使得生产者知晓消息已经正确到达了目的地了。如果消息和队列是可持久化的那么确认消息会在消息写入磁盘之后发出。RabbitMQ 回传给生产者的确认消息中的 deliveryTag 包含了确认消息的序号此外 RabbitMQ 也可以设置 channel.basicAck 方法中的 multiple 参数表示到这个序号之前的所有消息都已经得到了处理注意辨别这里的确认和消费时候的确认之间的异同。
2.RabbitMQ弄丢了数据
为了防止rabbitmq自己弄丢了数据这个你必须开启rabbitmq的持久化就是消息写入之后会持久化到磁盘哪怕是rabbitmq自己挂了恢复之后会自动读取之前存储的数据一般数据不会丢。除非极其罕见的是rabbitmq还没持久化自己就挂了可能导致少量数据会丢失的但是这个概率较小。
设置持久化有两个步骤第一个是创建queue的时候将其设置为持久化的这样就可以保证rabbitmq持久化queue的元数据但是不会持久化queue里的数据第二个是发送消息的时候将消息的deliveryMode设置为2就是将消息设置为持久化的此时rabbitmq就会将消息持久化到磁盘上去。必须要同时设置这两个持久化才行rabbitmq哪怕是挂了再次重启也会从磁盘上重启恢复queue恢复这个queue里的数据。
而且持久化可以跟生产者那边的confirm机制配合起来只有消息被持久化到磁盘之后才会通知生产者ack了所以哪怕是在持久化到磁盘之前rabbitmq挂了数据丢了生产者收不到ack你也是可以自己重发的。
若生产者那边的confirm机制未开启的情况下哪怕是你给rabbitmq开启了持久化机制也有一种可能就是这个消息写到了rabbitmq中但是还没来得及持久化到磁盘上结果不巧此时rabbitmq挂了就会导致内存里的一点点数据会丢失。
3.消费端弄丢了数据
为了保证消息从队列可靠地达到消费者RabbitMQ 提供了消息确认机制message acknowledgement。消费者在订阅队列时可以指定 autoAck 参数当 autoAck 等于 false时RabbitMQ 会等待消费者显式地回复确认信号后才从内存或者磁盘中移去消息实质上是先打上删除标记之后再删除。当 autoAck 等于 true 时RabbitMQ 会自动把发送出去的消息置为确认然后从内存或者磁盘中删除而不管消费者是否真正地消费到了这些消息。
采用消息确认机制后只要设置 autoAck 参数为 false消费者就有足够的时间处理消息任务不用担心处理消息过程中消费者进程挂掉后消息丢失的问题因为 RabbitMQ 会一直等待持有消息直到消费者显式调用 Basic.Ack 命令为止。
总结RabbitMQ消息可靠性保障策略
1、生产者开启消息确认机制
2、消息队列数据持久化
3、消费者手动ack
4、生产者消息记录定期补偿机制
5、服务幂等处理
6、消息积压处理等