官方定义Spring Cloud Stream是一个构建消息驱动微服务的框架。
应用程序通过inputs或者outputs来与Spring Cloud Stream中binder对象交互。通过我们配置来binding(绑定),而Spring Cloud Stream的binder对象负责与消息中间件交互。所以,我们只需要搞清楚如何与Spring Cloud Stream交互就可以方便使用消息驱动的方式。
通过使用Spring Integration来连接消息代理中间件以实现消息事件驱动。Spring Cloud Stream为一些供应商的消息中间件产品提供了个性化的自动化配置实现,引用了发布订阅、消费组、分区的三个核心概念。
目前仅支持RabbitMQ、Kafka.
总结一句话:SpringCloud Stream是为了屏蔽底层信息中间件的差异,降低切换成本,统一消息的编程模型
官方地址:
https://spring.io/projects/spring-cloud-stream#overview
https://cloud.spring.io/spring-cloud-static/spring-cloud-stream/3.0.1.RELEASE/reference/html/
https://m.wang1314.com/doc/webapp/topic/20971999.html(中文手册)
比方说我们用到了RabbitMQ和Kafka,由于这两个消息中间件的架构上的不同,比如RabbitMQ有exichange,而kafka有 Topic和Partitions分区;
这些中间件的差异性导致我们实际项目开发给我们造成了一定的困扰, 我们如果用了两个消息队列的其中一种,后面的业务需求,我想往另外一种消息队列进行迁移,这时候无疑就是一个灾难性的; 一大堆东西都要重新推倒重新做,因为它跟我们的系统耦合了,这时候springcloud Stream给我们提供了一种解耦合的方式。
在没有绑定器这个概念的情况下,我们的SpringBoot应用要直接与消息中间件进行信息交互的时候,由于各消息中间件构建的初衷不同,它们的实现细节上会有较大的差异性。
通过定义绑定器作为中间层,完美地实现了应用程序与消息中间件细节之间的隔离。
通过向应用程序暴露统一的Channel通道,使得应用程序不需要再考虑各种不同的消息中间件实现。
通过定义绑定器Binder作为中间层,实现了应用程序与消息中间件细节之间的隔离。
input对应消费者,output对应生产者
Stream中的消息通信方式遵循了发布—订阅模式,topic主题进行广播(在ribbitmq就是Exchange,在kafka中就是topic)
Binder:很方便的连接中间件,屏蔽差异
Channel:通道,是队列Queue的一种抽象,在消息通讯系统中就是实现存储和转发的媒介,通过Channel对队列进行配置
Source和Sink:简单的可理解为参考对象是Spring Cloud Stream的自身,从Stream发布信息就是输出,接受消息就是输入;
<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>springcloud2020artifactId>
<groupId>com.zdw.springcloudgroupId>
<version>1.0-SNAPSHOTversion>
parent>
<modelVersion>4.0.0modelVersion>
<artifactId>cloud-stream-rabbitmq-provider8801artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-stream-rabbitartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
dependencies>
project>
server:
port: 8801
spring:
application:
name: cloud-stream-provider
cloud:
stream:
binders: #在此处配置要绑定的rabbitmq的服务信息
defaultRabbit: #表示定义的名称,用于binding整合
type: rabbit #消息组件类型
environment: #设置rabbitmq的相关环境配置
spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
bindings: #服务的整合处理
output: #这个名字是一个通道的名称
destination: studyExchange #表示要使用的Exchange名称定义
content-type: application/json #设置消息类型,本次为json,本文要设置为“text/plain”
binder: defaultRabbit #设置要绑定的消息服务的具体设置
eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka
instance:
lease-renewal-interval-in-seconds: 2 #设置心跳的时间间隔(默认是30S)
lease-expiration-duration-in-seconds: 5 #如果超过5S间隔就注销节点 默认是90s
instance-id: send-8801.com #在信息列表时显示主机名称
prefer-ip-address: true #访问的路径变为IP地址
package com.zdw.springcloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
/**
* @author ZDW
* @create 2020-08-22 10:52
*/
@SpringBootApplication
@EnableEurekaClient
public class ProviderMQMain8801 {
public static void main(String[] args) {
SpringApplication.run(ProviderMQMain8801.class,args);
}
}
package com.zdw.springcloud.service;
/**
* @author ZDW
* @create 2020-08-22 10:54
*/
public interface IMessageProvider {
public String send();
}
package com.zdw.springcloud.service.impl;
import com.zdw.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;
/**
* @author ZDW
* @create 2020-08-22 10:55
*/
@EnableBinding(Source.class) //定义消息的推送管道
public class MessageProviderImpl implements IMessageProvider {
@Resource
private MessageChannel output; // 消息发送管道
@Override
public String send() {
String serial = UUID.randomUUID().toString();
output.send(MessageBuilder.withPayload(serial).build());
System.out.println("*****serial: " +serial);
return serial+"————消息发送成功";
}
}
package com.zdw.springcloud.controller;
import com.zdw.springcloud.service.IMessageProvider;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
/**
* @Author ZDW
* @Since 2020/8/22 11:21
*/
@RestController
public class SendMessageController {
@Resource
private IMessageProvider messageProvider;
@GetMapping("/sendMessage")
public String sendMessage() {
return messageProvider.send();
}
}
新建消费者之一:cloud-stream-rabbitmq-consumer8802
<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>springcloud2020artifactId>
<groupId>com.zdw.springcloudgroupId>
<version>1.0-SNAPSHOTversion>
parent>
<modelVersion>4.0.0modelVersion>
<artifactId>cloud-stream-rabbitmq-consumer8802artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-stream-rabbitartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
dependencies>
project>
server:
port: 8802
spring:
application:
name: cloud-stream-consumer
cloud:
stream:
binders: #在此处配置要绑定的rabbitmq的服务信息
defaultRabbit: #表示定义的名称,用于binding整合
type: rabbit #消息组件类型
environment: #设置rabbitmq的相关环境配置
spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
bindings: #服务的整合处理
input: #这个名字是一个通道的名称
destination: studyExchange #表示要使用的Exchange名称定义
content-type: application/json #设置消息类型,本次为json,本文要设置为“text/plain”
binder: defaultRabbit #设置要绑定的消息服务的具体设置
eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka
instance:
lease-renewal-interval-in-seconds: 2 #设置心跳的时间间隔(默认是30S)
lease-expiration-duration-in-seconds: 5 #如果超过5S间隔就注销节点 默认是90s
instance-id: receive-8802.com #在信息列表时显示主机名称
prefer-ip-address: true #访问的路径变为IP地址
注意:input: #这个名字是一个通道的名称,在生产者的时候配置的是output(输出)
package com.zdw.springcloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
/**
* @author ZDW
* @create 2020-08-22 11:06
*/
@SpringBootApplication
@EnableEurekaClient
public class ConsumerMQMain8802 {
public static void main(String[] args) {
SpringApplication.run(ConsumerMQMain8802.class,args);
}
}
package com.zdw.springcloud.controller;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.cloud.stream.messaging.Sink;
import org.springframework.messaging.Message;
import org.springframework.web.bind.annotation.RestController;
/**
* @author ZDW
* @create 2020-08-22 11:08
*/
@RestController
@EnableBinding(Sink.class)
public class ReceiverMessageListenerController {
@Value("${server.port}")
private String serverPort;
@StreamListener(Sink.INPUT) //监听
public void input(Message<String> message) {
System.out.println("消费者8802, -----> 接受到的消息: " + message.getPayload()
+ "\t port: " + serverPort);
}
}
参照上面创建8802的步骤,创建8803,这样就搭建了一个生产者,多个消费者的情形。
生产者发布的消息,可以被多个消费者订阅,如果消费者属于不同的组,那么都可以消费该消息,如果消费者属于相同的组,那么就只有组内的某一个消费者可以消费该消息;
启动eureka7001,然后启动生产者8801,在启动消费者8802和8803
浏览器多次访问:http://localhost:8801/sendMessage
查看8802和8803的控制台,都会接收到8801发送的消息
如果一切正常,说明我们的环境是OK的
在上面的测试中,可以看到生产者发布了一条消息,消费8802和8803都消费了这一条消息,相当于这一条消息被消费了两次;
这个情况在MQ的发布订阅当中也属正常现象,如果业务是这样的话,是没有任何问题的。这里之所以称之为重复消费,是因为如果8802和8803是一个系统的多个副本,那么就不应该被消费两次,只能消费一次。
比如:订单系统生产出一个订单(相当于是一个消息),而8802和8803是库存系统的两个副本,那么正常情况就应该是要么8802把库存减一,要么8803把库存减一,不能两个都进行库存的减少操作。
所以要实现上面的需求,我们需要多加一个配置,那就是手动的为消费者进行分组,分组之后,组内的消费者属于竞争关系,同一条消息只能被组内的某一个消费者进行消费。
而且,分组还能达到消息的持久化效果,如果该消费者没有进行手动的分组(在配置文件中配置),那么一旦它宕机,而此时生产者却发送了消息,当消费者重启服务之后,不会接收到在宕机期间,生产者发送的消息,这就造成了消息的丢失。
修改消费8802和8803的application.yml,给消费者指定分组,让他们两处理同一个分组中:
bindings: #服务的整合处理
input: #这个名字是一个通道的名称
destination: studyExchange #表示要使用的Exchange名称定义
content-type: application/json #设置消息类型,本次为json,本文要设置为“text/plain”
binder: defaultRabbit #设置要绑定的消息服务的具体设置
group: consumer8802
加上了 group: consumer8802 ,使8802和8803都属于组consumer8802
不会接收到在宕机期间,生产者发送的消息,这就造成了消息的丢失。**
修改消费8802和8803的application.yml,给消费者指定分组,让他们两处理同一个分组中:
bindings: #服务的整合处理
input: #这个名字是一个通道的名称
destination: studyExchange #表示要使用的Exchange名称定义
content-type: application/json #设置消息类型,本次为json,本文要设置为“text/plain”
binder: defaultRabbit #设置要绑定的消息服务的具体设置
group: consumer8802
加上了 group: consumer8802 ,使8802和8803都属于组consumer8802
重启消费者服务,然后重新测试,会发现8801生成的消息,会被8802和8803轮询的消费掉。