单体架构
单体架构:将业务的所有功能集中在一个项目中开发,打成-一个包部署。
分布式架构:根据业务功能对系统进行拆分,每个业务模块作为独立项目开发,称为-一个服务。
分布式架构的要考虑的问题:
服务拆分粒度如何?
服务集群地址如何维护?
服务之间如何实现远程调用?
服务健康状态如何感知?
微服务是一种经过良好架构设计的分布式架构方案,微服务架构特征:
单一职责: 微服务拆分粒度更小,每一个服务都对应唯一 的业务能力,做到单一职责,避免重复业务开发
面向服务: 微服务对外暴露业务接口
自治: 团队独立、技术独立、数据独立、部署独立
隔离性强: 服务调用做好隔离、容错、降级,避免出现级联问题
微服务这种方案需要技术框架来落地,全球的互联网公司都在积极尝试自己的微服务落地技术。在国内最知名的就是SpringCloud和阿里巴巴的Dubbo。
SpringCloud是目前国内使用最广泛的微服务框架。
官网地址: https://spring.io/projects/spring-cloud
SpringCloud集成了各种微服务功能组件,并基于SpringBoot实现了这些组件的自动装配,从而提供了良好的开箱即用体验:
服务拆分注意事项
不同微服务,不要重复开发相同业务
微服务数据独立,不要访问其它微服务的数据库
微服务可以将自己的业务暴露为接口,供其它微服务调用
1. 微服务需要根据业务模块拆分,做到单一职责,不要重复开发相同业务
2. 微服务可以将业务暴露为接口,供其它微服务使用
3. 不同微服务都应该有自己独立的数据库
案例:根据订单id查询订单功能
需求:根据订单id查询订单的同时,把订单所属的用户信息一起返回
注册RestTemplate
在order-service的OrderApplication中注册RestTemplate
服务远程调用RestTemplate
修改order-service中的OrderService的queryOrderByld方法:
基于RestTemplate发起的http请求实现远程调用
http请求做远程调用是与语言无关的调用,只要知道对方的ip、端口、接口路径、请求参数即可。
提供者与消费者
服务提供者: 一次业务中,被其它微服务调用的服务。( 提供接口给其它微服务)
服务消费者: 一次业务中,调用其它微服务的服务。(调 用其它微服务提供的接口)
问:
服务A调用服务B,服务B调用服务C,那么服务B是什么角色?
答:
服务调用关系
服务提供者:暴露接口给其它微服务调用
服务消费者:调用其它微服务提供的接口
提供者与消费者角色其实是相对的
服务调用出现的问题
服务消费者该如何获取服务提供者的地址信息?
如果有多个服务提供者,消费者该如何选择?
消费者如何得知服务提供者的健康状态?
消费者该如何获取服务提供者具体信息?
服务提供者启动时向eureka注册自己的信息
eureka保存这些信息
消费者根据服务名称向eureka拉取提供者信息
如果有多个服务提供者,消费者该如何选择?
服务消费者利用负载均衡算法,从服务列表中挑选-个
消费者如何感知服务提供者健康状态?
服务提供者会每隔30秒向EurekaServer发送心跳请求,报告健康状态
eureka会更新记录服务列表信息,心跳不正常会被剔除
消费者就可以拉取到最新的信息
在Eureka架构中,微服务角色有两类:
● EurekaServer: 服务端,注册中心
◆记录服务信息
◆心跳监控
● EurekaClient: 客户端
◆Provider: 服务提供者,例如案例中的user-service
◆注册自己的信息到EurekaServer
◆每隔30秒向EurekaServer发 送心跳
● consumer:服务消费者,例如案例中的order-service
◆根据服务名称从EurekaServer拉取服务列表
◆基于服务列表做负载均衡,选中一个微服务后发起远程调用
3.添加application.yml文件, 编写下面的配置:
注册user-service
将user-service服务注册到EurekaServer步骤如下:
在user-service项目引入spring-cloud-starter- netflix-eureka-client的依赖
在application.yml文件, 编写下面的配置:
另外,我们可以将user-service多次启动,模拟多实例部署,但为了避免端口冲突,需要修改端口设置:
在order-service完成服务拉取
服务拉取是基于服务名称获取服务列表,然后在对服务列表做负载均衡
负载均衡策略
Ribbon的负载均衡规则是- -一个叫做lRule的接口来定义的,每一个子接口都是一种规则:
通过定义IRule实现可以修改负载均衡规则,有两种方式:
饥饿加载
Ribbon默认是采用懒加载,即第一次访问时才 会去创建LoadBalanceClient,请求时间会很长。
而饥饿加载则会在项目启动时创建,降低第一次访问的耗时, 通过下面配置开启饥饿加载:
Nacos是阿里巴巴的产品,现在是SpringCloud中的一一个组件。相比Eureka功能更加丰富,在国内受欢迎程度较高。
注释掉order-service和user-service中 原有的eureka依赖。
修改user-service&order-service中的application.yml文件, 注释eureka地址,添加nacos地址:
服务跨集群调用问题
服务调用尽可能选择本地集群的服务,跨集群调用延迟较高
本地集群不可访问时,再去访问其它集群
注意将user-service的权重都设置为1
NacosRule负载均衡策略
①优先选择同集群服务实例列表
②本地集群找不到提供者,才去其它集群寻找,并且会
报警告
③确定了可用实例列表后,再采用随机负载均衡挑选实
例
根据权重负载均衡
实际部署中会出现这样的场景:
服务器设备性能有差异, 部分实例所在机器性能较好,另一些较差,我们希望性能好的机器承担更多的用户请求
实例的权重控制
①Nacos控制台可以设置实例的权重值,0~1之间
②同集群内的多个实例,权重越高被访问的频率越高
③权重设置为0则完全不会被访问
Nacos中服务存储和数据存储的最外层都是一个名为namespace的东西, 用来做最外层隔离
Nacos环境隔离
①namespace用来做环境 隔离
②每个namespace都有唯一id
③不同namespace 下的服务不可见
服务注册到Nacos时,可以选择注册为临时或非临时实例,通过下面的配置来设置:
Nacos与Eureka的共同点
①都支持服务注册和服务拉取
②都支持服务提供者心跳方式做健康检测
Nacos与Eureka的区别
①Nacos支 持服务端主动检测提供者状态:临时实例采用心跳模式,非临时实例采用主动检测模式
②临时实例心跳不正常会被剔除,非临时实例则不会被剔除
③Nacos支 持服务列表变更的消息推送模式,服务列表更新更及时
④Nacos集群 默认采用AP方式,当集群中存在非临时实例时,采用CP模式; Eureka采用AP方式
Nacos中的配置文件变更后,微服务无需重启就可以感知。不过需要通过下面两种配置实现:
方式一: 在@Value注入的变量所在类上添加注解@RefreshScope
方式二: 使用@ConfiqurationProperties注解
微服务启动时会从nacos读取多个配置文件:
无论profile如何变化,[spring. application.name].yaml这个文件一定会加载,因此多环境共享配置可以写入这个文件
多种配置的优先级:
服务名-profile.yaml > 服务名称.yaml >本地配置
Nacos生产环境下一-定要部署为集群状态,部署方式参考课前资料中的文档:
以前利用RestTemplate发起远程调用的代码:
代码可读性差,编程体验不统一
参数复杂URL难以维护
Feign是一个声明式的http客户端,官方地址:https://github.com/OpenFeign/feign
公其作用就是帮助我们优雅的实现http请求的发送,解决上面提到的问题。
主要是基于SpringMVC的注解来声明远程调用的信息,比如:
服务名称: userservice
请求方式: GET
请求路径: /user/{id}
请求参数: Long id
返回值类型: User
Feign运行自定义配置来覆盖默认配置,可以修改的配置如下:
一般我们需要配置的就是日志级别。
配置Feign日志有两种方式:
方式一:配置文件方式
配置Feign日志的方式二: java代码方式,需要先声明一-个Bean:
①而后如果是全局配置,则把它放到@EnableFeignClients这个注解中:
②如果是局部配置,则把它放到@FeignClient这个注解中:
Feign底层的客户端实现:
● URLConnection: 默认实现,不支持连接池
● Apache HttpClient :支持连接池
● OKHttp:支持连接池
因此优化Feign的性能主要包括:
①使用连接池代替默认的URLConnection
②日志级别, 最好用basic或none
Feign的性能优化-连接池配置
方式一(继承) :给消费者的FeignClient和提供者的controller定义统一的父接口作为标准。
方式二(抽取) :将FeignClient抽取为独立模块, 并且把接口有关的POJO、默认的Feign配置都放到这个模块中,提供给所有消费者使用
实现最佳实践方式二的步骤如下:
1.首先创建个module, 命名为feign-api, 然后引入feign的starter依赖
2.将order-service中 编写的UserClient、User、 DefaultFeignConfiguration都复制到feign-api项目中
3.在order-service中 引入feign-api的依赖
4.修改order-service中的所有与上述三个组件有关的import部分,改成导入feign-api中的包
当定义的FeignClient不在SpringBootApplication的扫描包范围时,这些FeignClient无法使用。 有两种方式解决:
在SpringCloud中网关的实现包括两种:
● gateway
● zuul
Zuul是基于Servlet的实现,属于阻塞式编程。而SpringCloudGateway则是基于Spring5中提供的WebFlux,属于响应式编程的实现,具备更好的性能。
搭建网关服务的步骤:
网关搭建步骤:
1. 创建项目,引入nacos服务发现和gateway依赖
2. 配 置application.yml,包括服务基本信息、nacos地址、路由
路由配置包括:
1.路由id:路由的唯-标示
2.路由目标 (uri) :路由的目标地址,http代表固定地址,lb代表根据服务名负载均衡
3.路由断言( predicates) :判断路由的规则
4.路由过滤器(filters) :对请求或响应做处理
网关路由可以配置的内容包括:
路由id: 路由唯一标示
uri: 路由目的地,支持Ib和http两种
predicates: 路由断言,判断请求是否符合要求,符合则转发到路由目的地
filters: 路由过滤器,处理请求或响应
我们在配置文件中写的断言规则只是字符串,这些字符串会被Predicate Factory读取并处理,转变为路由判断的条件
例如Path=/user/**是按照路径匹配,这个规则是由orq.springframework .cloud.gateway.handler.predicate.PathRoutePredicateFactory类来处理的
像这样的断言工厂在SpringCloudGateway还有十几个
GatewayFilter是网关中提供的一种过滤器,可以对进入网关的请求和微服务返回的响应做处理:
案例:给所有进入userservice的请求添加一个请求头
给所有进入userservice的请求添加一一个请求头: Truth=itcast is freaking awesome!
实现方式:在gateway中修改application.yml文件,给userservice的路 由添加过滤器:
默认过滤器
全局过滤器的作用也是处理一切进入网关的请求和微服务响应, 与GatewayFilter的作用一 样。
区别在于GatewayFilter通过配置定义,处理逻辑是固定的。而GlobalFilter的逻辑需要 自己写代码实现。
案例: 定义全局过滤器,拦截并判断用户身份
需求:定义全局过滤器,拦截请求,判断请求的参数是否满足下面条件:
● 参数中是否有authorization,
● authorization参数值是否为admin
如果同时满足则放行,否则拦截
自定义类,实现GlobalFilter接口, 添加@Order注解:
请求进入网关会碰到三类过滤器:当前路由的过滤器、DefaultFilter、 GlobalFilter
请求路由后,会将当前路由过滤器和DefaultFilter、 GlobalFilter, 合并到一个过滤器链(集合)中,排序后依次执行每个过滤器
每一个过滤器都必须指定一个int类型的order值, order值越小,优先级越高,执行顺序越靠前。
GlobalFilter通过实现Ordered接口,或者添加@Order注解来指定order值,由我们自己指定
路由过滤器和defaultFilter的order由Spring指定,默认是按照声明顺序从1递增。
当过滤器的order值一样时,会按照defaultFilter >路由过滤器>、GlobalFilter的顺序执行。
跨域:域名不一致就是跨域,主要包括:
域名不同: www.taobao.com 和 www.taobao.org 和 wwwjd.com 和 miaoshajd.com
域名相同,端口不同: localhost:8080和localhost8081
跨域问题:浏览器禁止请求的发起者与服务端发生跨域ajax请求,请求被浏览器拦截的问题
解决方案: CORS
网关处理跨域采用的同样是CORS方案,并且只需要简单配置即可实现:
项目部署的问题
大型项目组件较多,运行环境也较为复杂,部署时会碰到一些问题 :
Docker如何解决依赖的兼容问题的?
●将应用的Libs (函数库)、Deps (依赖)、配置与应用一起打包
●将每个应用放到一个隔离容器去运行,避免互相干扰
Docker如何解决大型项目依赖关系复杂,不同组件依赖的兼容性问题?
1. Docker允许开发中将应用、依赖、函数库、配置-起打包,形成可移植镜像
2. Docker应用运行在容器中,使用沙箱机制,相互隔离
Docker如何解决开发、测试、生产环境有差异的问题
1. Docker镜像中包含完整运行环境,包括系统函数库,仅依赖系统的Linux内核,因此可以在任意Linux操作系统上运行
Docker是一个快速交付应用、运行应用的技术:
1.可以将程序及其依赖、运行环境一 起打包为一个镜像,可以迁移到任意Linux操作系统
2.运行时利用沙箱机制形成隔离容器,各个应用互不干扰
3.启动、移除都可以通过一行命令完成,方便快捷
Docker和虚拟机的差异:
●docker是一 个系统进穆;虚拟机是在操作系统中的操作系统
●docker体积小、启动速度快、性能好;虚拟机体积大、启动速度慢、性能一般
镜像和容器
镜像(Image) : Docker将 应用程序及其所需的依赖、函数库、环境、配置等文件打包在一起,称为镜像。
容器(Container) :镜像中的应用程序运行后形成的进程就是容器,只是Docker会给容 器做隔离,对外不可见。
Docker和DockerHub
DockerHub: DockerHub是一个Docker镜像的托管平台。这样的平台称为Docker Registry。
国内也有类似于DockerHub的公开服务,比如网易云镜像服务、阿里云镜像库等。
docker架构
Docker是一个CS架构的程序,由两部分组成:
◆ 服务端(server): Docker守护进程,负责处理Docker指令,管理镜像、容器等
◆ 客户端(client): 通过命令或RestAPI向Docker服务端发送指令。可以在本地或远程向服务端发送指令。
镜像相关命令
镜像名称一般分两 部分组成: [repository]:[tag]。
在没有指定tag时,默认是latest,代表最新版本的镜像
镜像操作命令
案例:利用docker save将nginx镜像导出磁盘,然后再通过load加载回来
步骤一: 去docker hub查看Nginx的容器运行命令
案例:进入Nginx容器,修改HTML文件内容,添加“欢迎您”
步骤一: 进入容器。进入我们刚刚创建的nginx容器的命令为:
命令解读:
docker exec :进入容器内部,执行一个命令
-it :给当前进入的容器创建一个标准输入、输出终端,允许我们与容器交互
mn:要进入的容器的名称
bash:进入容器后执行的命令,bash是一个linux终端交互命令
步骤二:进入nginx的HTML所在目录/usr/share/ nginx/html
查看容器状态:
●docker ps
●添加-a参数查看所有状态的容器
删除容器:
●docker rm
不能删除运行中的容器,除非添加-f参数
进入容器:
命令是docker exec -it [容器名] [要执行的命令]
exec命令可以进入容器修改文件,但是在容器内修改文件是不推荐的
数据卷(volume)是一个虚拟目录,指向宿主机文件系统中的某个目录。
案例:创建一个数据卷,并查看数据卷在宿主机的目录位置
挂载数据卷
我们在创建容器时,可以通过-V参数来挂载一个数据卷到某个容器目录
案例:创建一个nginx容器, 修改容器内的html目录内的index.html内容
需求说明:上个案例中, 我们进入nginx容器内部,已经知道nginx的html目录所在位置/usr/share/nginx/html,我们需要把这个目录挂载到html这个数据卷上,方便操作其中的内容。
提示: 运行容器时使用-V参数挂载数据卷
步骤:
①创建容器并挂载数据卷到容器内的HTML目录
②进入html数据卷所在位置,并修改HTML内容
1
● -V volumeName: /targetContainerPath
● 如果容器运行时volume不存在,会自动被创建出来
案例:创建并运行一-个MySQL容器,将宿主机目录直接挂载到容器
提示:目录挂载与数据卷挂载的语法是类似的
●tV [宿主机目录]:[容器内目录]
●-V [宿主机文件]:[容器内文件]
实现思路如下:
- 在将课前资料中的mysql.tar文件上传到虚拟机,通过load命令加载为镜像
- 创建目录/tmp/myql/data
- 创建目录/tmp/myql/conf, 将课前资料提供的hmy.cnf文件.上传到/tmp/myql/conf
4.去DockerHub查阅资料, 创建并运行MySQL容器,要求:
①挂载/tmp/myql/data到mysql容器内数据存储目录
②挂载/tmp/myql/conf/hmy.cnf到mysql容 器的配置文件
③设置MySQL密码
数据卷挂载的方式对比
docker run的命令中通过-V参数挂载文件或目录到容器中:
①-V volume名称:容器内目录
②-V 宿主机文件:容器内文件
③-V宿主机目录:容器内目录
数据卷挂载与目录直接挂载的
①数据卷挂载耦合度低,由docker来管理目录,但是目录较深,不好找
②目录挂载耦合度高,需要我们自己管理目录,不过目录容易寻找查看
镜像结构
镜像是将应用程序及其需要的系统函数库、环境、配置、依赖打包而成。
镜像是分层结构,每一层称为一-个Layer
Baselmage层:包含基本的系统函数库、环境变量、文件系统
Entrypoint:入口,是镜像中应用启动的命令
其它:在Baselmage基础上添加依赖、安装程序、完成整个应用的安装和配置
什么是Dockerfile
Dockerfile就是一个文本文件, 其中包含一个个的指令(Instruction),用指令来说明要执行什么操作来构建镜像。每一个指令都会形成一层Layer。
更新详细语法说明,请参考官网文档: https://docs.docker.com/ engine/ reference/builder
案例:基于Ubuntu镜像构建一个新镜像, 运行一个java项目
案例:基于java:8-alpine镜像,将一个Java项目构建为镜像
实现思路如下:
①新建一个空的目录,然后在目录中新建一一个文件,命名为Dockerfile
②拷贝课前资料提供的docker-demo.jar到这个目录中
③编写Dockerfile文件:
a)基于java:8-alpine作为基础镜像
b)将app.jar拷 贝到镜像中
c)暴露端口
d)编写入口ENTRYPOINT
④使用docker build命令构建镜像
公
⑤使用docker run创建容器并运行
1. Dockerfile的本质是一一个文件,通过指令描述镜像的构建过程
3. Dockerfile的第 行必 须是FROM, 从一个基础镜像来构建
5. 基础镜像可以是基本操作系统,如Ubuntu。也可以是其他人制作好的镜像,例如: java:8-alpine
什么是DockerCompose
Docker Compose可以基于Compose文件帮我们快速的部署分布式应用,而无需手动一个个创建和运行容器!
Compose文件是个文本文件, 通过指令定义集群中的每个容器如何运行。
DockerCompose的详细语法参考官网: https://docs.docker.com/compose/compose-file/
镜像仓库( Docker Registry )有公共的和私有的两种形式:
● 公共仓库:例如Docker官方的DockerHub,国内也有–些云服务商提供类似于DockerHub的公开服务,比如网易云镜像服务、DaoCloud 镜像服务、阿里云镜像服务等。
● 除了使用公开仓库外,用户还可以在本地搭建私有Docker Registry。 企业自己的镜像最好是采用私有DockerRegistry来实现。
在私有镜像仓库推送或拉取镜像
1. 推送本地镜像到仓库前都必须重命名(docker tag)镜像,以镜像仓库地址为前缀
2. 镜像仓库推送前需要把仓库地址配置到docker服务的daemon.json文件中,被docker信任
3.推送使用docker push命令
4.拉取使用docker pull命令
微服务间基于Feign的调用就属于同步方式,存在一些问题。
同步调用存在的问题
异步调用常见实现就是事件驱动模式
事件驱动优势
优势 一 : 服务解耦
优势二 : 性能提升,吞吐量提高
优势三 : 服务没有强依赖,不担心级联失败问题
优势四 : 流量削峰
异步通信的优点:
● 耦合度低
● 吞吐量提升
● 故障隔离
● 流量削峰
异步通信的缺点:
● 依赖于Broker的可靠性、安全性、吞吐能力
● 架构复杂了,业务没有明显的流程线,不好追踪管理
什么是MQ
MQ (MessageQueue) ,中文是消息队列,字面来看就是存放消息的队列。也就是事件驱动架构中的Broker。
RabbitMQ概述
RabbitMQ是基于Erlang语言开发的开源消息通信中间件,官网地址: https://www.rabbitmq.com/
安装RabbitMQ,参考课前资料: .
方式二: 从本地加载
在课前资料已经提供了镜像包:
安装MQ
RabbitMQ中的几个概念:
● channel:操作MQ的工具
● exchange:路由消息到队列中
● queue:缓存消息
● virtual host:虚拟主机,是对queue、exchange等资源的逻辑分组
常见消息模型
MQ的官方文档中给出了5个MQ的Demo示例,对应了几种不同的用法:
基本消息队列(BasicQueue )
工作消息队列(WorkQueue )
HelloWorld案例
官方的HelloWorld是基于最基础的消息队列模型来实现的,只包括三个角色:
发送消息
package cn.itcast.mq.helloworld;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import org.junit.Test;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class PublisherTest {
@Test
public void testSendMessage() throws IOException, TimeoutException {
// 1.建立连接
ConnectionFactory factory = new ConnectionFactory();
// 1.1.设置连接参数,分别是:主机名、端口号、vhost、用户名、密码
factory.setHost("192.168.60.130");
factory.setPort(5672);
factory.setVirtualHost("/");
factory.setUsername("root");
factory.setPassword("123456");
// 1.2.建立连接
Connection connection = factory.newConnection();
// 2.创建通道Channel
Channel 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();
}
}
接收消息
package 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("192.168.60.130");
factory.setPort(5672);
factory.setVirtualHost("/");
factory.setUsername("root");
factory.setPassword("123456");
// 1.2.建立连接
Connection connection = factory.newConnection();
// 2.创建通道Channel
Channel channel = connection.createChannel();
// 3.创建队列
String queueName = "simple.queue";
channel.queueDeclare(queueName, false, false, false, null);
// 4.订阅消息
channel.basicConsume(queueName, true, new DefaultConsumer(channel){
@Override
public 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("等待接收消息。。。。");
}
}
基本消息队列的消息发送流程:
1.建立connection
2.创建channel
3.利用channel声明队列
4.利用channel向 队列发送消息
基本消息队列的消息接收流程:
1.建立connection
2.创建channel
3.利用channel声明队列
4.定义consumer的消 费行为handleDelivery()
5.利用channel将 消费者与队列绑定
什么是SpringAMQP
SpringAmqp的官方地址: https://spring.io/projects/spring-amqp
案例:利用SpringAMQP实现HelloWorld中的基础消息队列功能
流程如下:
1.在父工程中引入spring-amqp的依赖
2.在publisher服务 中利用RabbitTemplate发送消息到simple.queue这个队列
3. 在consumer服务中编写消费逻辑,绑定simple.queue这个队列
步骤1 : 引入AMQP依赖
因为publisher和consumer服务都需要amqp依赖,因此这里把依赖直接放到父工程mq-demo中:
步骤2:在publisher中编写测试方法,向simple.queue发送消息
1.在publisher服务中编写application.yml, 添加mq连接信息:
2.在publisher服务 中新建一一个测试类 ,编写测试方法:
步骤3:在consumer中编写消费逻辑,监听simple.queue
1.在consumer服务中编写application.yml, 添加mq连接信息:
2.在consumer服务中新建一 个类,编写消费逻辑:
SpringAMQP如何接收消息?
引入amqp的starter依赖
配置RabbitMQ地址
定义类,添加@Component注解
类中声明方法,添加@RabbitListener注解,方法参数就时消息
注意:消息一旦消费就会从队列删除,RabbitMQ没有消息回溯功能
Work Queue工作队列
Work queue,工作队列,可以提高消息处理速度,避免队列消息堆积
案例:模拟WorkQueue,实现-个队列绑定多个消费者
基本思路如下:
1.在publisher服务中定义测试方法,每秒产生50条消息,发送到simple.queue
2.在consumer服务 中定义两个消息监听者,都监听simple.queue队列
3.消费者1每秒处理50条消息,消费者2每秒处理10条消息
@Test
public void testSendMessage2WorkQueue() throws InterruptedException {
String queueName = "simple.queue";
String message = "hello,spring amqp__";
for (int i=0;i<=50;i++){
rabbitTemplate.convertAndSend(queueName,message + i);
Thread.sleep(20);
}
}
@RabbitListener(queues = "simple.queue")
public void listenSimpleQueue1(String msg) throws InterruptedException {
System.out.println("消费者1接收到simple.queue的消息:【" + msg + "】" + LocalTime.now());
Thread.sleep(20);
}
@RabbitListener(queues = "simple.queue")
public void listenSimpleQueue2(String msg) throws InterruptedException {
System.err.println("消费者2接收到simple.queue的消息:【" + msg + "】" + LocalTime.now());
Thread.sleep(200);
}
消费预取限制
Work模型的使用:
多个消费者绑定到一个队列,同一条消息只会被一-个
消费者处理
通过设置prefetch来控制消费者预取的消息数量
发布( Publish )、订阅( Subscribe )
发布订阅-Fanout Exchange
Fanout Exchange会将接收到的消息路由到每一个跟其绑定的queue
案例:利用SpringAMQP演示FanoutExchange的使用
实现思路如下:
步骤1:在consumer服务声明Exchange、Queue、 Binding
SpringAMQP提供了声明交换机、队列、绑定关系的API,例如:
在consumer服务常见-个类,添加@Configuration注解, 并声明FanoutExchange、Queue和绑定关系对象Binding,代码如下:
接收消息
@RabbitListener(queues = "fanout.queue1")
public void listenFanoutQueue1(String msg) {
System.err.println("消费者接收到fanout.queue1的消息:【" + msg + "】");
}
@RabbitListener(queues = "fanout.queue2")
public void listenFanoutQueue2(String msg) {
System.err.println("消费者接收到fanout.queue2的消息:【" + msg + "】");
}
发送消息
@Test
public void testSendMessageFanoutExchange() {
//交换机名称
String exchangeName = "itcast.fanout";
//消息
String message = "hello every one!";
//发送消息
rabbitTemplate.convertAndSend(exchangeName,"",message);
}
交换机的作用是什么?
● 接收publisher发送的消息
● 将消息按照规则路由到与之绑定的队列
● 不能缓存消息,路由失败,消息丢失
● FanoutExchange的会将消息路由到每个绑定的队列
声明队列、交换机、绑定关系的Bean是什么?
● Queue
● FanoutExchange
● Binding
发布订阅-DirectExchange
Direct Exchange会将接收到的消息根据规则路由到指定的Queue,因此称为路由模式(routes) 。
● 每一个Queue都与Exchange设 置一个BindingKey
● 发布者发送消 息时,指定消息的RoutingKey
● Exchange将消息路由到BindingKey与消息RoutingKey- 致的队列
案例:利用SpringAMQP演示DirectExchange的使用
实现思路如下:
@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 + "】");
}
发送消息
@Test
public void testSendMessageDirectExchange() {
//交换机名称
String exchangeName = "itcast.direct";
//消息
String message = "hello blue!";
//发送消息
rabbitTemplate.convertAndSend(exchangeName,"red",message);
}
描述下Direct交换机与Fanout交换机的差异?
● Fanout交换机将消息路由给每一个与之绑定的队列
● Direct交换机根据RoutingKey判断路由给哪个队列
● 如果多个队列具有相同的RoutingKey,则与Fanout功能类似
基于@RabbitListener注解声明队列和交换机有哪些常见注解?
● @Queue
● @Exchange
发布订阅-TopicExchange
TopicExchange与DirectExchange类似,区别在于routingKey必须是多个单词的列表,并且以 . 分割。
Queue与Exchange指定BindingKey时可以使用通配符
#: 代指0个或多个单词
*: 代指一个单词
案例:利用SpringAMQP演示TopicExchange的使用
实现思路如下:
- 并利用@RabbitListener声 明Exchange、Queue、RoutingKey
- 在consumer服务中, 编写两个消费者方法,分别监听topic.queue 1和topic.queue2
- 在publisher中 编写测试方法,向itcast. topic发送消息
@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 + "】");
}
发送消息
@Test
public void testSendMessageTopicExchange() {
//交换机名称
String exchangeName = "itcast.topic";
//消息
String message = "hello blue!";
//发送消息
rabbitTemplate.convertAndSend(exchangeName,"sdjfhldsjk.news",message);
}
案例:测试发送Object类型消息
说明:在SpringAMQP的发送方法中,接收消息的类型是Object,也就是说我们可以发送任意对象类型的消息,SpringAMQP会帮我们序列化为字节后发送。
声明一个队列
//声明一个队列
@Bean
pubLic Queue objectQueue(){
return new Queue( name: "object. queue") ;
}
发送消息
@Test
public void testSendMessageObjectQueue() {
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完成序列化。
如果要修改只需要定义一个MessageConverter类型的Bean即可。推荐用JSON方式序列化,步骤如下: