1.Stream消息驱动是什么
2.Stream消息驱动案例说明
3.Stream消息驱动之生产者
4.Stream消息驱动之消费者
5.Stream消息驱动分组消费和持久化
1.Stream消息驱动是什么
1.1)队列和服务耦合带来的问题
在我们的日常开发当中,会使用到很多的队列,比如有rabbitmq,kafka,rocketMQ,kafka等等。
所以我们在编写项目的时候,项目会和队列的api进行耦合,当我们切换队列,或者队列与队列之间传输信息的时候,这种中间件的差异性会给我们造成极大的困扰,如果我们用了一种队列,后面又有新的业务需求,我们想往另外一种消息队列进行迁移,这个时候无疑就是一种灾难性的,很多东西都要推倒重做,因为这些队列和我们的系统耦合了。
1.2)springCloud stream是什么?
springCloud stream是一个让开发者调用上层api,就能屏蔽底层队列的区别,构建消息驱动服务的框架。
所以我们只需要搞清楚如何与Spring Cloud Stream 交互就可以方便使用消息驱动的方式。
目前仅支持RabbitMQ、Kafka。
简单地来说,spring cloud stream就是一种屏蔽底层消息中间件的差异,降低切换成本,统一消息的编程模型。
1.3)为什么要使用spring cloud stream
当我们提出为什么要使用一个工具的时候,我们要想不使用这个工具会有什么后果?
我们从上面的描述可以看出,stream消息驱动是一个解耦工具,不使用这个工具,应用程序就会和队列进行耦合,为了解除这种耦合,我们采用了spring cloud stream。
1.4)stream凭什么可以统一底层差异?
我们可以先来看一下,传统的mq是如何工作的?
传统的mq模型分为生产者,消费者,消息通道。
生产者把消息通过消息通道发送给消费者。
我们再来看一下spring Cloud Stream给我们提供了一种解耦的方式。
这里引出了一个很重要的概念Binder,在没有Binder这个概念的情况下,我们的springBoot应用要直接与消息中间件进行信息交互,由于消息中间件构建的理念不同,它们的实现细节上会有较大的差异,使用方法也会有很大的差异,通过定义Binder为中间层,完美地实现了应用程序与消息中间件之间的隔离。
1.5)Spring Cloud Stream的几个重要概念
Binder:
INPUT对应于消费者。
OUTPUT对应于生产者。
这里我们可能会感觉到很奇怪,我们平时不是认为input是生产者,output是消费者吗?
其实我们只要转一个方向就明白了:
out是发送信息的那一方,是生产者。
input是信息的输入方,是消费者。
而我们平时可能是站在消息通道的角度来看的,in是输入,是生产,out是输出,是消费。
我们站消息通道的在外面,思考一下这个问题,就明白了。
Channel:
通道,是队列Queue的一种抽象,在消息通讯系统中就是实现存储和转发的媒介,通过Channel对队列进行配置。
Source和Sink:
Source:消息发送者
Sink:消息接受者
1.6)常用注解
组成 | 说明B |
---|---|
@Input | 消息输入通道,消息的消费者 |
@OutPut | 消息输出通道,消息的生产者 |
@StreamListener | 监听队列,用于消费者的队列的消息接收 |
@EnableBinding | 指信道channel和exchange绑定在一起 |
2.Stream消息驱动案例说明
我们创建三个项目,两个为消费者,一个为生产者。
3.Stream消息驱动之生产者
pom:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>service-stream-provider-8601</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>service-stream-provider-8601</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Hoxton.SR1</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<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.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
yml:
server:
port: 8601
eureka:
instance:
hostname: service-stream-provider #eureka服务端的实例名称
prefer-ip-address: true #访问路径可以显示IP地址
instance-id: ${spring.cloud.client.ip-address}:${server.port}-service-stream-provider #访问路径的名称格式
client:
register-with-eureka: true #false表示不向注册中心注册自己。
fetch-registry: true #false表示自己端就是注册中心,我的职责就是维护服务实例,并不需要去检索服务
service-url:
#集群指向其它eureka
defaultZone: http://127.0.0.1:8001/eureka/,http://127.0.0.1:8002/eureka/
spring:
rabbitmq: #队列的基本配置
host: localhost
port: 5672
username: guest
password: guest
application:
name: service-stream-provider
cloud:
stream:
bindings:
output: #生产者 因为out是出,是发送消息的一方,所以是生产者
destination: studyExchange # 表示要使用的Exchange名称定义
content-type: application/json # 设置消息类型,本次为json,文本则设置“text/plain”
binder: rabbit # 设置要绑定的消息服务的具体设置
feign:
hystrix:
enabled: true
测试类:
public interface IMessageProvider {
String send() ;
}
import com.example.demo.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;
/**
* @author sulingfeng
* @title: MessageProviderImpl
* @projectName CloudFamily
* @description: TODO
* @date 2022/4/13 17:01
*/
@EnableBinding(Source.class) // 可以理解为是一个消息的发送管道的定义
public class MessageProviderImpl implements IMessageProvider {
@Resource
private MessageChannel output; // 消息的发送管道
@Override
public String send() {
String serial = UUID.randomUUID().toString();
this.output.send(MessageBuilder.withPayload(serial).build()); // 创建并发送消息
System.out.println("***serial: " + serial);
return serial;
}
}
@RestController
public class ProviderController {
@Resource
private IMessageProvider messageProvider;
@GetMapping(value = "/sendMessage")
public String sendMessage()
{
return messageProvider.send();
}
}
4.Stream消息驱动之消费者
消费者我们创建两个,不过代码大部分都是一样的。
pom:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>service-stream-consumer-8702</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>service-stream-consumer-8702</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Hoxton.SR1</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<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.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
yml:
server:
port: 8702
eureka:
instance:
hostname: service-stream-provider #eureka服务端的实例名称
prefer-ip-address: true #访问路径可以显示IP地址
instance-id: ${spring.cloud.client.ip-address}:${server.port}-service-stream-provider #访问路径的名称格式
client:
register-with-eureka: true #false表示不向注册中心注册自己。
fetch-registry: true #false表示自己端就是注册中心,我的职责就是维护服务实例,并不需要去检索服务
service-url:
#集群指向其它eureka
defaultZone: http://127.0.0.1:8001/eureka/,http://127.0.0.1:8002/eureka/
spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
application:
name: service-stream-provider
cloud:
stream:
bindings:
input: #消费者 消息的输入方
destination: studyExchange # 表示要使用的Exchange名称定义
content-type: application/json # 设置消息类型,本次为json,文本则设置“text/plain”
binder: rabbit # 设置要绑定的消息服务的具体设置
feign:
hystrix:
enabled: true
测试类
@Component
@EnableBinding(Sink.class)//消息的消费者
public class ReceiveMessageListener {
@Value("${server.port}")
private String serverPort;
@StreamListener(Sink.INPUT)//和队列绑定
public void input(Message message)
{
System.out.println("消费者,------->接收到的消息:" + message.getPayload()+"\t port: "+serverPort);
}
}
5.Stream消息驱动分组消费和持久化
接下来我们启动消费者和生产者,然后生产者发送一条消息,会发现消费者确实收到了消息,不过两个消费者都收到了消息,这可能就会造成消息的重复消费
这个时候,我们就可以用stream中的消息分组来解决:
在stream中,处于同一个group的多个消费者是竞争关系,就能够保证一条消息只能被其中一个服务消费。
不同组是可以重复消费的。
同一组内有竞争关系,只能被其中一个组消费。
修改yml:
server:
port: 8702
eureka:
instance:
hostname: service-stream-provider #eureka服务端的实例名称
prefer-ip-address: true #访问路径可以显示IP地址
instance-id: ${spring.cloud.client.ip-address}:${server.port}-service-stream-provider #访问路径的名称格式
client:
register-with-eureka: true #false表示不向注册中心注册自己。
fetch-registry: true #false表示自己端就是注册中心,我的职责就是维护服务实例,并不需要去检索服务
service-url:
#集群指向其它eureka
defaultZone: http://127.0.0.1:8001/eureka/,http://127.0.0.1:8002/eureka/
spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
application:
name: service-stream-provider
cloud:
stream:
bindings:
input: #消费者 消息的输入方
destination: studyExchange # 表示要使用的Exchange名称定义
content-type: application/json # 设置消息类型,本次为json,文本则设置“text/plain”
binder: rabbit # 设置要绑定的消息服务的具体设置
group: atguiguA
feign:
hystrix:
enabled: true
加入分组之后,我们就可以避免重复消费了。
生产者:
消费者1:
消费者2:
解决了重复的消息,此时我们来看看持久化,其实加上group属性就加上了持久化,我们把两个消费者服务都关掉,修改yml,一个有group,一个没有group,然后连续发几条信息,再启动消费者,发现有group的服务的消费者依然可以消费服务离线时候的消息,但是没有group的消费者,就没法消费那些在它这个活动离线的时候消费的消息了。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。