Stream背景和概述
1. 背景
1. 市面上存在着多种消息中间件技术
ActiveMQ,RabbitMQ,RocketMQ,Kafka
那么每多出来一种新的技术,就要付出响应的学习成本
消息中间件技术的多样导致开发者的学习成本很大
2. 不同的系统中会用到不同的消息中间件,那么当需要系统进行整合时,或者系统进行切换时
由于用的是不同的中间件技术,该怎么整合切换。
具体的实现,需要的成本很大。
3. 那么有没有一种新的技术,让我们不再关注具体的MQ的选择,
我们只需要用一种适配绑定的方式,自动的给我们在各种MQ内切换。
4. 引出了SpringCloud Stream
屏蔽底层的细节差异,让我只需要操作一个Cloud Stream,就可以操作底层下面各种各样不同的MQ。
达到我们以更小的代价实现切换,维护,开发。
2. 概述
一句话:
屏蔽底层消息中间件的差异,降低切换成本,统一消息的编程模型。
官网:
官网
文档
中文指导手册
版本要求:
绑定器对象:Binder Implementations
就是靠它屏蔽了我们底层的MQ的差异。
什么是SpringCoudStream:
是一个构建消息驱动微服务的框架
应用程序通过inputs或者outputs来与Spring Cloud Stream中的 binder对象交互
通过我们配置来binding(绑定),而SpringCloudStream的binder对象负责与消息中间件交互
所以我们只需要搞清楚如何与Spring Cloud Stream交互就可以很方便使用消息驱动的方式。
通过使用Spring Integration来连接消息代理中间件以实现消息事件驱动。
Spring Cloud Stream为一些供应商的消息中间件产品提供了个性化的自动配置实现,引用了
发布-订阅,消费组,分区的三个核心概念。
目前仅支持RabbitMQ,Kafka。
3. 设计思想
1. 传统的消息中间件的流程
2. 为什么使用SpringCloud Stream
当我们同时用到了RabbitMQ和Kafka,由于这两个消息中间件的架构上的不同,整合和切换就会有很大的成本。
比如RabbitMQ有exchange,kafka有Topic和Partition分区。
这些中间件的差异性在我们实际项目开发中造成了一定的困扰,我们如果用了两个消息队列的其中一种,
后面也无需要,我们想往另外一种消息队列进行迁移,就会有一大堆东西都要推倒重新做,这时候无疑
是灾难性的。
因为消息中间件已经和我们的系统耦合了,这时候SpringCloudStream给我们提供了一种解耦合的方式。
3. stream是怎么统一底层差异的?
Binder:
1. input 对应于消费者。
2. output对应于生产者。
3. 官方架构图
4. Stream中的消息通信方式遵循了发布-订阅模式
Topic主题进行广播:
在RabbitMQ中就是Exchange
在Kafka中就是Topic
4. SpringCloudStream标准流程套路
Binder:屏蔽消息中间件的连接中间件(连接中间件和生产者/消费者的)
Channel:通道,是队列Queue的一种抽象,在消息通讯系统中就是实现存储和转发的媒介,
通过Channel对队列进行配置。
Queue队列----->Channel对象。
Source和Sink:
简单的可理解为参照对象是SpringCloudStream自身,从Stream发布消息就是输出,接收消息就是输入。
5 编码API和常用注解:
入门案例
1. 案例说明
RabbitMQ环境已经OK
工程中要新建三个子模块:
cloud-stream-rabbitmq-provider8801:作为生产者进行发送消息模块
cloud-stream-rabbitmq-consumer8802:作为消息接收模块
cloud-stream-rabbitmq-consumer8803:作为消息接收模块
2. 消息驱动之生产者
2.1 新建module
cloud-stream-rabbitmq-provider8801:作为生产者进行发送消息模块
2.2 POM
<dependencies>
<!--rabbitMQ-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
<!--eureka client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!--web/actuator这两个一般一起使用,写在一起-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--监控-->
<dependency>
<groupId>
org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--热部署-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true </optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true </optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test </scope>
</dependency>
</dependencies>
2.3 yml
这里有个报错,但是正常用。
- output:改成这样,不报错了,但是MQ中没有studyExchange。
server:
port: 8801
spring:
application:
name: cloud-stream-provider
cloud:
stream:
binders:
defaultRabbit:
type : rabbit
environment:
spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
bindings:
output:
destination: studyExchange
content-type: application/json
binder: defaultRabbit
eureka:
client:
service-url:
defaultZone: http://eureka7001.com:7001/eureka
instance:
lease-renewal-interval-in-seconds: 2
lease-expiration-duration-in-seconds: 5
instance-id: send-8801.com
prefer-ip-address: true
2.4 主启动类
@SpringBootApplication
@EnableEurekaClient
public class StreamMQMain8801 {
public static void main(String[] args) {
SpringApplication.run(StreamMQMain8801.class, args);
}
}
2.5 业务类
我们此时要写的代码,要注意和谁交互,不是传统的controller调用service。
基于SpringCloudStream然后做outputs,再指定通道,交互绑定器,再和消息中间件交互。
2.6 发送消息接口实现类
接口:
public interface IMessageProvider {
public String send();
}
接口实现类:
1. Source
这个Source哪来的呢?
简单的可理解为参照对象是SpringCloudStream自身,
从Stream发出消息就是输出,接收消息就是输入。
这里我们可以理解为我们定义一个消息生产者的发送管道:消息源
2. 构建一个消息,并用output发送
3. 完整serviceImpl代码
@EnableBinding:指信道channerl和exchange绑定在一起。
package com.atguigu.springcloud.service.impl;
import com.atguigu.springcloud.service.IMessageProvider;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.messaging.Source;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.messaging.MessageChannel;
import javax.annotation.Resource;
import java.util.UUID;
//@Service:这里不需要了,这里不是传统的controller调用service。这个service是和rabbitMQ打交道的
@EnableBinding(Source.class) //可以理解为我们定义一个消息生产者的发送管道
public class IMessageProviderImpl implements IMessageProvider {
@Resource
private MessageChannel output; //消息放送管道
@Override
public String send () {
String serial = UUID.randomUUID().toString();
//通过output这个管道向消息中间件发送消息
Message<String> message = MessageBuilder.withPayload(serial).build();
output.send(message);
//output.send(MessageBuilder.withPayload(serial).build());
System.out.println("*********serial: " + serial);
return null;
}
}
2.7 Controller
@RestController
public class SendMessageController {
@Resource
private IMessageProvider messageProvider;
@GetMapping(value = "/sendMessage" )
public String sendMessage (){
return messageProvider.send();
}
}
2.8 看一下是否每调用一次接口,就能向中间件发送一个消息。
启动7001
启动RabbitMQ
启动8801
发送多次:http://localhost:8801/sendMessage
3. 消息驱动之消费者
cloud-stream-rabbitmq-consumer8802
3.1 新建module
cloud-stream-rabbitmq-consumer8802
3.2 POM
<dependencies>
<!--rabbitMQ-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
<!--eureka client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!--web/actuator这两个一般一起使用,写在一起-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--监控-->
<dependency>
<groupId>
org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--热部署-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true </optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true </optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test </scope>
</dependency>
</dependencies>
3.3 yml
server:
port: 8802
spring:
application:
name: cloud-stream-consumer
cloud:
stream:
binders:
defaultRabbit:
type : rabbit
environment:
spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
bindings:
input:
destination: studyExchange
content-type: application/json
binder: defaultRabbit
eureka:
client:
service-url:
defaultZone: http://eureka7001.com:7001/eureka
instance:
lease-renewal-interval-in-seconds: 2
lease-expiration-duration-in-seconds: 5
instance-id: receiver-8802.com
prefer-ip-address: true
3.4 主启动类
@SpringBootApplication
@EnableEurekaClient
public class StreamMQMain8802 {
public static void main(String[] args) {
SpringApplication.run(StreamMQMain8802.class, args);
}
}
3.5 业务类
@StreamListener:监听队列,用于消费者的队列的消息接收。
@Component
@EnableBinding(Sink.class) //可以理解为我们定义一个消息消费者的接收管道
public class ReceiveMessageListenerController {
@Value("${server.port} " )
private String serverPort;
@StreamListener(Sink.INPUT) //输入源:作为一个消息监听者
public void input(Message<String> message){
//获取到消息
String messageStr = message.getPayload();
System.out.println("消费者1号----->接收到的消息: " +messageStr+"\t" +
"port: " + serverPort);
}
}
3.6 启动测试
测试8801发送8802接收消息
http://localhost:8801/sendMessage
请求了8801消息生产者的接口发送一个消息,看看8802消息消费者能否自动收到消息。
测试成功。
4 高级特性:分组消费与持久化
1. cloud-stream-rabbitmq-consumer8803:作为消息接收模块
依照8802,clone出来一份8803
2. 启动测试
RabbitMQ
7001 : 服务注册
8801 : 消息生产
8802 : 消息消费
8803 : 消息消费
此时stidyExchage交换机有两个订阅者:8802,8803
测试成功
3. 运行后的问题1:重复消费问题
1. 重复消费问题
目前是8802/8803同时都收到了8801发送的消息,存在重复消费问题。
2. 背景:为什么要解决该问题
比如8801下一个订单,但是被两个服务获取消费,会多扣一次款。
3. 默认的分组:流水号:
8802和8803默认是两个不同的分组。
不同的微服务,默认是不同的分组,不同的组都可以消费同一个消息。
4. 解决:消息分组
在Stream中处于同一个group中的多个消费者是竞争关系,可以保证每个组只能消费该消息一次。
不同组是可以消费同一个消息。
我们应该自定义分组。
5. 分组:group
atguiguA,atguiguB
6. 修改8802,8803的yml
7. 现在的分组
8. 消费组
分布式微服务应用为了实现高可用性和负载均衡,实际上会部署多个实例。本案例我们启动了两个微服务:
8802/8803。
多数情况,生产者发送消息给某个具体微服务时只希望被消费一次,按照上面我们启动两个应用的例子,
虽然他们同属于一个应用,但是由于在不同组,这个消息出现了被重复消费(消费两次)的情况。
为了解决这个问题,在SpringCloudStream中提供了消费组的概念。
9. 将8802/8803分为同一个组
10. 最后得到的结果:
两个组轮流获取消息:
8802获取一次,8803获取一次。
轮流获取。
4. 运行后的问题2:消息持久化问题
消息持久化问题
通过上述,我们解决了重复消费问题,再看看持久化。
停掉8802/8803并去除掉8802的分组:atguiguA,但是8803的分组没有去掉
8801先发送4条消息给RabbitMQ。
先启动8802,无分组属性配置:后台没有打出来消息。
消息丢失...
再启动8803,有分组属性配置,后台打出来了MQ上的消息。
group属性很重要。
5 Spring Cloud Sleuth
5.1 概述
分布式请求链路跟踪:
为什么会出现这个技术,要解决哪些问题。
在微服务框架中,一个客户端发起的请求在后端系统中会经过多次不同的服务节点调用来协同产生最后的请求
结果,每一个前段请求都会形成一条复杂的分布式服务调用链路,链路中的任何一环出现高延时或错误都会引
起整个请求最后的失败。
SpringCloudSleuth提供了一套完整的服务跟踪的解决方案,在分布式系统中提供乐追踪解决方案并且兼容
支持了zipkin。
官网
5.2 一些概念
Sleuth :来负责跟踪整理
zipkin:负责展现
1. 下载安装zipkin
SpringCloud从F版本起已经不需要自己构建ZipKin Server了,只需要调用jar包即可。
下载
2. 启动
java -jar zipkin-server-2.12.9-exec.jar
http://localhost:9411 : web交互页面
zipkin搭建成功
3. 完整的调用链路
】
Trace:类似于树结构的Span集合,表示一条调用链路,存在唯一标识
Span:表示调用链路来源,通俗的理解Span就是一次请求信息。
各个span通过parentId关联起来。
5.3 搭建链路监控步骤
1. 服务提供者:cloud-provider-payment8001
POM:
<!-- 包含了sleuth zipkin 数据链路追踪-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
yml
zipkin:
base-url: http://localhost:9411
sleuth:
sampler:
probability: 1
controller
/**
* 测试链路监控
*/
@GetMapping("/payment/zipkin" )
public String paymentZipkin (){
return "hi, i'am paymentZipkin server fall back, welcome to here, O(∩_∩)O哈哈~" ;
}
2. 服务调用者:cloud-consumer-order80
pom
<!-- 包含了sleuth zipkin 数据链路追踪-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
yml
zipkin:
base-url: http://localhost:9411
sleuth:
sampler:
probability: 1
controller
/**
* 测试链路监控
*/
public String paymentZipkin (){
String result = template.getForObject(PAYMENT_URL+"/payment/zipkin/" , String.class);
return result;
}
3. 发送一次请求
http://localhost/consumer/payment/zipkin
点进去看看:
说明是:order调用的payment服务,发送的是什么请求,调用的是什么链接,都有详细的记录。