springcloudstream整合rabbitmq及其应用实战,点对点、发布订阅、消息分组与持久化

简介

springcloudstream是啥?它与JMS有何区别?如何使用?它可以理解为对JMS更为易用、实用的进一步抽象与包装的产品,JMS定义的点对点、发布订阅模型它都支持。它借鉴了IO流的设计思想,从而设计了输入流Input、输出流Output为介质来发送和接收消息。它不关心谁来发送消息、持久化消息等等,全部交给其称为Middleware的消息中间件来处理即可,只需要绑定到任何一种消息中间件,即可实现系统之间通过异步消息进行交互,默认支持的消息中间件有rabbitmq、kafka。其继承了springboot的优秀设计,切换消息中间件只需要修改依赖和配置信息即可完成,对业务代码完全无侵入。

本文将介绍其基本原理,介绍如何在实际工作中使用springcloudstream,内容有

  • springcloudstream设计模型
  • 如何绑定消息中间件rabbitmq
  • Input、Output如何理解与使用
  • Input、Output如何关联,对应的队列名是啥?
  • 如何利用Output发送消息
  • 如何利用Input关联StreamListener接收并处理消息
  • 如何实现点对点消息,消息分组?
  • 如何实现消息发布订阅模型?
  • 消息消费失败如何故障转移与重试?
  • @RereshScope与@StreamListener擦出的火花,如何避坑?

原文:传送门

注:本文基于springcloud2.1.3 Greenwich.RELEASE 版本

参考文章:springcloudstream官方使用教程


1、springcloudstream设计模型

SCSt-with-binder.png

> 模型中的组件有:应用、input、output、binder、Middleware,下面逐一对其进行讲解 > * 应用:java服务,例如一个springboot应用实例 > * input: 消息接收通道,通常用于绑定queue或者topic到应用的消息监听器,可配合StreamListener注解一起使用 > * output: 消息发送通道,通常用于绑定queue或者topic目标,将消息发送到指定的目标 > * binder:指连接消息中间件服务与input/output的桥梁,需要添加maven依赖和对应中间件的服务器地址配置信息 > * Middleware: 指消息中间件服务,如rabbitmq或者kafaka等等,springcloud已经提供了rabbitmq和kafka的实现
2、如何绑定到消息中间件rabbitmq
  • 引入maven依赖
		<dependency>
			<groupId>org.springframework.amqp</groupId>
			<artifactId>spring-rabbit</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
		</dependency>
  • 配置rabbitmq服务器连接信息
spring:
  cloud:
    stream:
      binders:#配置多个binder
        defaultRabbit: #配置binder的名字
          type: rabbit #类型
          environment:
            spring:
              rabbitmq: #rabbitmq服务器连接信息
                host: localhost
                port: 5672
                username: admin
                password: admin
                virtual-host: /
      defaultBinder: defaultRabbit #默认绑定器
      default: #默认配置,所有通道可以使用此配置
        content-type: application/json #消息的格式,此处其沿用了springmvc里的messageConverter,跟接口的ContentType配置类似
        binder: defaultRabbit #使用默认的绑定器
      bindings: #针对单个Input或者Output单独进行配置,优先级高于默认配置       
        queueName:
          destination: testQueue #topic的名字
          content-type: application/json #消息的格式,此处其沿用了springmvc里的messageConverter,跟接口的ContentType配置类似
          binder: defaultRabbit #使用默认的绑定器
          group: serviceA #配置同一个应用的实例属于同一个组,消息只消费一次    
  • 如果只引入了一个消息中间件,可以采用如下简约配置即可
spring: 
  rabbitmq:
    host: localhost
    port: 5672
    username: admin
    password: admin
3、 Input、Output如何理解与使用
  • Input : 应用内用来接收消息的通道
  • Output : 应用内用来发送消息的通道
  • 创建Input和Output
//应用serviceA定义消息接收通道
public interface MessageInputChannel {
   
    @Input("testQueue")
    public SubscribableChannel messageInput();    
}
//应用serviceB定义消息发送通道
public interface MessageOutputChannel {   
    @Output("testQueue")
    public MessageChannel messageOutput();
}
  • 绑定通道和应用
  • 应用serviceA如下
@SpringBootApplication
@EnableBinding(MessageInputChannel.class)
public class StartServiceAApplication {
	public static void main(String[] args) {
		SpringApplication.run(StartServiceAApplication.class, args);
	}	
	//接收消息
	@StreamListener("testQueue")
	public void handle(String msg){
		System.out.println("收到来自testQueue的消息:"+msg);
	}
}
  • 应用serviceB如下
@SpringBootApplication
@EnableBinding(MessageOutputChannel.class)
@RestController
public class StartServiceBApplication {
	@Autowired
	private MessageOutputChannel output;
	public static void main(String[] args) {
		SpringApplication.run(StartServiceBApplication.class, args);
	}
	@GetMapping(value="/sendMessage")
	public void sendMessage(@RequestParam("msg") String msg){
        output.messageOutput()
        .send(MessageBuilder.withPayload(msg).build());
	}
}
4、Input、Output如何关联,对应的队列名是啥?
  • Input默认名为@Input(value = “testQueue”)注解的value值,这里便是:testQueue
  • Output默认名为@Output(value = “testQueue”)注解的value值,这里便是:testQueue
  • 当然我们也可以通过配置将queue或者topic名进行更换,配置如下:
spring: 
  cloud: 
    stream: 
      bindings: 
        testQueue: 
          destination: test #修改topic的名字

同一个项目中既要发送消息、又要接收同一个topic的消息时,就需要为input和output配置别名,因为同一个项目里springcloudstream不允许定义同名的input和output,所以要修改

  • 例如:
public interface MessageSingleChannel {
   
    @Input("testQueueInput")
    public SubscribableChannel messageInput();  
    @Output("testQueueOutput")
    public MessageChannel messageOutput();
}
  • 对应的配置如下:
spring: 
  cloud: 
    stream: 
      bindings: 
        testQueueInput: 
          destination: testQueue #配置别名
        testQueueOutput: 
          destination: testQueue #配置别名

这样就把两个通道的topic配置成一样的了,同一应用内也可以使用消息异步驱动业务了

5、如何利用Output发送消息

利用springcloudstream提供的MessageChannel,即可发送消息,示例如下:

output.messageOutput().send(MessageBuilder.withPayload(msg).build());
6、如何利用Input关联StreamListener接收并处理消息

利用StreamListener便可监听消息

	//接收简单string消息
	@StreamListener("testQueue")
	public void handle(String msg){
		System.out.println("收到来自testQueue的消息:"+msg);
	}
	
	//接收复杂消息
	@StreamListener("testQueue1")
	public void handle(Map<String,String> map){
		System.out.println("收到来自testQueue的消息:"+JSON.toJSONString(map));
	}
7、如何实现点对点消息,消息分组与消息的持久化?

为何需要分组?如果不配置分组,springcloudstream默认为所有的input实例分配一个匿名的组名,这样会导致生产者服务发送一条消息,同一项目的每个实例都会各自收到一条相同消息,在一些应用场景,如用户下单,是不允许重复下单的,所以需要将同一个项目的不同实例分配为同一个组,只允许其中一个实例消费消息,进行处理。
开启分组后,rabbitmq默认会开启对消息的持久化能力

  • 配置serviceA实例分组
spring: 
  cloud: 
    stream: 
      bindings: 
        testQueue: 
          destination: test #配置别名
          group: serviceA #配置分组
  • 启动多个serviceA的实例,发送消息,测试,可以看到一条消息,只会被一个serviceA实例接收
8、如何实现消息发布订阅模型?
  • 跟消息分组不一样的地方就是,有的业务场景,生产者发送一条消息,需要通知所有的监听者服务实例,完成状态的更新或者数据的同步等操作,这个时候就需要使用到发布订阅模型了。
  • 如果同一应用的实例不指定分组名,默认都是独立的监听者,自然就满足了发布订阅的要求了,也就不需要做任何配置了
  • 如果消息需要持久化,也可为每个实例单独配置不同的组名,也同样可以实现发布订阅模型
  • 启动多个serviceA服务,通过serviceB发送消息,可以看到每个实例均收到了该消息
9、消息消费失败如何故障转移与重试?

这里的应用场景是可靠消息的发送与接收,消息需要开启持久化,实现方式有如下两种

  • 1、可以开启消息手动确认机制,处理成功后才确认消息
  • 2、也可以通过抛出运行时异常,使消息重新回到消息中间件服务中

消息如果多次消费失败,rabbitmq会认为这条消息是有毒的,会将其丢入死信队列,并可以配置告警,通知业务人员排查问题,失败的次数上限可以进行配置

10、@RereshScope与@StreamListener擦出的火花,如何避坑?

学过springcloudconfig的同学应该都知道@RereshScope是用来动态更新组件信息的,如果在一个需要动态刷新配置的类中定义@StreamListener,会出现什么现象呢?

  • 错误示例,如下所示
@RereshScope
public class Test {
	@Value("${app.username}")
 	private String username;
	//接收消息
	@StreamListener("testQueue")
	public void handle(String msg){
		System.out.println(username+"收到来自testQueue的消息:"+msg);
	}
}

这样导致的结果就是,当消息发送过来时,应用会找不到合适的监听器来处理,导致消息消费不了。

为啥会这样呢,是由于当刷新配置信息的时候,你的StreamListener重复注册了同样的监听器,但是之前的监听器所属的类实例却被销毁了,当然找不到该监听器了

  • 这里郑重提醒,小伙伴们千万不能这么干,不然遇到这种问题也是很头疼的,一不注意,还真发现不了这个坑。
  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值