消息驱动——Stream

文章内容来自 《springcloud微服务架构开发实战》 董超 胡炽维

解决分布式系统中消息传递方案最好的选择就是消息中间件

通过消息中间件所提供的松散耦合的方式——存储和转发微服务之间的异步数据

9.1 什么是消息驱动开发

异步消息中间件的消息传递模式又可以分为两种:点对点模式和“发布-订阅”模式。

·点对点模式:该模式常用于消息生产者和消息消费者之间点到点的通信;·“发布-订阅”模式:该模式使用主题(Topic)代替点对点中的目的消费者。此时消息生产者只需要将消息发布到主题中即可,而不需要关心是谁消费该消息;而消费者如果需要消费消息,只需要订阅相应的主题,当有消息时消息中间件就会推送该消息。

9.1.1 基于消息中间件开发的优点

1.降低应用耦合

9.1.2 基于消息中间件开发的缺点


9.2 Spring Cloud Stream简介

Spring Cloud Stream支持与多种消息中间件整合,如Kafka、RabbitMQ等,使用Spring Integration提供与消息代理之间的连接,为应用程序的消息发布和消费提供了一个平台中立的接口,将实现的细节独立于应用代码之外

9.2.1 应用模型
消息驱动——Stream_第1张图片
1.消息发送通道接口Source

消息发送通道接口用于Spring Cloud Stream与外界通道的绑定,我们可以在该接口中通过注解的方式定义消息通道的名称。当使用该通道接口发送一个消息时,SpringCloud Stream会将所要发送的消息进行序列化,然后通过该接口所提供的MessageChannel将所要发送的消息发送到相应的消息中间件中。

2.消息通道Channel

消息通道是对消息队列的一种抽象,用来存放消息发布者发布的消息或者消费者所要消费的消息。在向消息中间件发送消息时,需要指定所要发送的消息队列或主题的名称,而在这里Spring Cloud Stream进行了抽象,开发者只需要定义好消息通道,消息通道具体发送到哪个消息队列则在项目配置文件中进行配置,这样一方面可以将具体的消息队列名称与业务代码进行解耦,另外一方面也可以让开发者方便地根据项目环境切换不同的消息队列。

3.消息绑定器Binder

Spring Cloud Stream通过定义绑定器作为中间层,实现了应用程序与具体消息中间件细节之间的隔离,向应用程序暴露统一的消息通道,使应用程序不需要考虑与各种不同的消息中间件的对接。当需要升级或者更改不同的消息中间件时,应用程序只需要更换对应的绑定器即可,而不需要修改任何应用逻辑。

Spring Cloud Stream默认提供了对RabbitMQ和ApacheKafka的绑定器,在应用中开发者只需要引入相应的绑定器就可以实现与RabbitMQ或者Kafka的对接,从而进行消息的发送与监听。Spring Cloud Stream会根据类路径自动侦测开发者使用何种绑定器,当然,开发者也可以在项目中同时使用不同的绑定器,只要把相关的依赖代码包含进来即可,甚至可以让项目在运行时动态地将不同的消息通道绑定到不同的绑定器上。

4.消息监听通道接口Sink

与消息发送通道接口(Source)相似,消息监听通道接口则是Spring Cloud Stream提供应用程序监听通道消息的抽象处理接口。当从消息中间件中接收到一个待处理消息时,该接口将负责把消息数据反序列化为Java对象,然后交由业务所定义的具体业务处理方法进行处理。


9.2.2 编程模型

Spring Cloud Stream还提供很多开箱即用的接口声明及注解,来声明约束消息发送和监听通道。

使用步骤:

1.声明和绑定消息通道
该步骤告诉Spring Cloud Stream框架,要连接到消息中间件的哪个通道上,这里涉及下面几个注解:·@EnableBinding·@Input·@Output

@EnableBinding注解是告诉应用需要触发消息通道的绑定,将我们的应用变成一个Spring Cloud Stream应用。@EnableBinding可以应用到Spring的任意一个配置类中,此外,@EnableBinding注解中可以声明一个或多个消息发送通道接口或消息监听通道接口参数。

@Input注解是用在消息监听通道接口的方法定义上,用来绑定一个具体的消息通道。比如Sink,见源码;

        // Spring Cloud Stream Sink接口源码
        public interface Sink {
              String INPUT = "input";
            //Sink接口最重要的就是提供一个消息订阅通道,通过该通道进行消息订阅
              @Input(Sink.INPUT)
              SubscribableChannel input();
          }

可以看到,Sink接口处定义了一个名称为input的消息监听通道。因此只需在应用配置中设置该消息通道所绑定的Kafka或RabbitMQ的消息队列(主题),就可以进行消息监控了。

@Output注解是用在消息发送通道接口的方法定义上,用来绑定消息发送的通道,见源码:

    // Spring Cloud Stream Source接口源码
    public interface Source {
      String OUTPUT = "output";
      //Source接口最重要的就是提供一个消息通道,可以进行消息发送
      @Output(Source.OUTPUT)
      MessageChannel output();
    }

和Sink接口一样,Source接口里定义了一个名称为output的消息发送通道。

Spring Cloud Stream还提供了一个开箱即用的消息通道接口定义Processor,同时继承了Source和Sink这两个接口,该接口所定义的通道是一个消息发送通道同时也是一个消息监听通道。

2.访问消息通道

对于使用@EnableBinding绑定的每一个接口,SpringCloud Stream都会自动构建一个Bean,并实现该接口。当我们通过该Bean调用那些注解了@Input或@Output的方法时,就会返回相应的消息发送或订阅通道

比如,我们可以通过下面的示例代码,将Source Bean注入到HelloWorldSender中,然后通过source.output()方法获取消息发送通道(MessageChannel),就可以发送消息了。

  // 通过Source接口发送消息
    @Component
    public class HelloWorldSender {
        private Source source;
        @Autowired
        public HelloWorldSender(Source source) {
          this.source = source;
        }
         //调用Source中所提供的MessageChannel发送消息
    public void sayHello(String name) {
         source.output().send(MessageBuilder.withPayload(name).build());
            }
        }

如果开发者不想每次发送的时候都使用source.output(),可以直接在业务类中直接织入MessageChannel

private MessageChannel output;

output.send(MessageBuilder.withPayload(name).build());

如果在项目中定义了多个消息通道,在注入的时候还可以增加限定,

  // 可以通过@Qualifier限定所要使用的消息通道
        @Autowired
        public HelloWorldSender(@Qualifier("myOutput") MessageChannel output) {
            this.output = output;
        }

3.发布或监听消息

在消息监听处理时可以使用Spring Integration的注解或者Spring Cloud Stream的@StreamListener注解来实现。

@StreamListener注解模仿Spring的其他消息注解(如@MessageMapping、@JmsListener和@RabbitListener等)。同时@StreamListener注解还提供了一种更简单的模型来处理输入消息,尤其当所要处理的消息包含了强类型信息时。一个简单的消息监听处理代码示例如下:

    @EnableBinding(Sink.class)
    public class UserMsgHandler {
        @Autowired
        UserService userService;
        // 使用StreamListener注册一个消息监听处理
        @StreamListener(Sink.INPUT)
        public void onUserMsg(UserMsg usermsg) {
          userService.log(usermsg);
        }
    }

Spring Cloud Stream提供了一个扩展的MessageConverter机制。该机制对所绑定的通道进行消息数据的处理,也就是说MessageConverter机制会使用contentType头所指定的消息内容格式(默认为application/json),将所接收的消息负载进行反序列化,解析为Java对象。

消息监听返回数据到其他消息通道时,可以使用@SendTo注解指定返回数据的输出通道。

// 绑定接口需要替换成Processor(因为是发送)
        @EnableBinding(Processor.class)
        public class TransformProcessor {
            @Autowired
            UserService userService;

            // @StreamListener指定监听的通道,@SendTo指定消息发送的通道
            @StreamListener(Processor.INPUT)
            @SendTo(Processor.OUTPUT)
            public UserResult handle(UserMsg usermsg) {
              return userService.log(usermsg);
            }
        }

9.2.3 使用“发布-订阅”模式

发布-订阅模式可以将两个或多个互相依赖的应用进行解耦,使它们可以各自独立地改变和复用,Spring Cloud Stream进行了一些扩展将发布-订阅模式作为应用的一种可选,并且通过原生中间件的支持,简化了在不同平台使用发布-订阅模式的复杂性。

消息驱动——Stream_第2张图片

Spring Cloud Stream应用之间使用发布-订阅模式典型的部署模式结构图,所要交互的数据在共享的主题上进行广播。

图9-3中传感器所采集的数据通过一个HTTP端点发布到raw-sensor-data主题上。另外有两个独立的微服务,一个用来计算传感数据的平均值,另一个是将这些原始数据存放到HDFS中,这两个微服务都分别订阅了raw-sensor-data主题上的消息。

因此,Spring Cloud推荐在搭建微服务时,如果微服务之间需要进行通信,应尽量采取发布-订阅模式。


9.3 Kafka使用指南

Kafka使用Scala和Java进行编写,具有快速、可扩展、高吞吐量、内置分区、支持数据副本和可容错等特性,能够支撑海量数据的高效传递。同时Kafka支持消息持久化

9.3.1 Kafka基础知识

术语:

1.主题:Topic

在Kafka中将每一个不同类别的消息称为一个主题Topic。在物理上,不同主题(Topic)的消息是分开存储的。在逻辑上,同一个主题(Topic)的消息可能保存在一个或多个代理(Broker)中,但对于生产者或消费者来说,只需指定消息的主题(Topic)就可生产或消费数据,而不用关心消息数据到底存于何处。

2.生产者:Producer

生产者也就是消息的发布者。负责将消息发布到Kafka中的某个主题(Topic)中,消息代理(Broker)在接收到生产者所发送的消息后,将该消息追加到当前分区中。生产者在发布消息的时候也可以选择将消息发布到主题上的哪一个分区上。

3.消费者:Consumer

消费者从消息代理(Broker)中读取消息数据并进行处理。一个消费者可以同时消费多个主题(Topic)中的消息。

此外,Kafka还提供了消费者组(Consumer Group)的概念,发布在主题上消息的可以分发给此消费者组中的任何一个消费者进行消费。

4.消息代理:Broker

生产者所发布的消息将保存在一组Kafka服务器中,称之为Kafka集群。而集群中的每一个Kafka服务器节点就是一个消息代理Broker。消费者通过消息代理从中获取所订阅的消息并进行消费。

5.消息分区:Partition

主题所发布的消息数据将会被分割为一个或多个分区(Partition),每一个分区的数据又可以使用多个Segment文件进行存储。在一个分区中的消息数据是有序的,而多个分区之间则没有消息数据顺序。如果一个主题的数据需要严格保证消息的消费顺序,那么需要将分区数目设为1。

当生产者将消息存储到一个分区中时,Kafka会为每条消息数据建立一个唯一索引号(index),这个索引号称为偏移量(offset)。对于消费者来说都会在本地保存该offset,这样当消费者正常消费时,相应本地的偏移量也会增加。同时消费者可以自己控制该偏移量,以便进行消息的重新消费等处理。Kafka的这种设计对消费者来说非常实用。

当新的消息数据追加到分区中时,Kafka集群就会在不同的消息代理(Broker)之间做个备份,从而保证了消息数据的可靠性。

此外,Kafka还支持实时的流处理。通过流处理可以持续从某个主题中获取输入数据,并进行处理加工,然后将其写入输出主题中。对于复杂的转换,Kafka提供了StreamsAPI来辅助流处理。通过这种实时的流处理,可以构建聚合计算或者将流连接到一起,形成复杂的应用。

9.3.2 搭建Kafka环境

https://kafka.apache.org/下载;

tar -xzf kafka_2.11-1.1.0.tgz 解压 cd kafka_2.11-1.1.0

因为Kafka是一个分布式的消息系统,消息代理(Broker)、消费者(Consumer)等都需要ZooKeeper来提供分布式支持,因此在启动Kafka服务器之前需要先启动一个ZooKeeper服务。

ZooKeeper服务器搭建:在上面目录有内嵌的zookeeper,

zookerper-server-start.sh config/zookeeper.properties启动

kafka-server-start.sh config/server.properties启动Kafka服务器;

Kafka已经连接到ZooKeeper并进行注册了。

创建主题:

bin/kafka-topics.sh --create --zookeeper localhost:2181 --replication-
        factor 1 --partitions 1 --topic springcloud-msg

向主题发送消息:
在这里插入图片描述通过Kafka所提供的命令行工具进行消费
在这里插入图片描述


9.4 使用消息对应用重构

可用的缓存框架,如Ehcache、Memcached、GuavaCache、Redis等

当用户数据一旦缓存之后,当在用户微服务中对用户信息执行更新、删除等操作时,就可以使用SpringCloud Stream通知商品微服务进行缓存更新。

接下来对原来的示例项目进行如下几点改进:

·改造商品微服务增加缓存处理功能

·改造用户微服务,当一个用户信息被更新、删除时,可以通过Kafka发送一条消息给商品微服务。

·改造商品微服务,增加用户信息变更消息监听功能

9.4.1 为商品服务增加缓存功能

Redis,对string、list、set、zset(有序集合)及Hash的数据类型都支持push、pop、add、remove、取交集、并集和差集等操作。会周期性地把更新的数据写入磁盘或者把修改操作写入追加的记录文件中。

商品微服务,添加依赖,spring-data-redis(支持RedisTemplate),jedis(链接数据库服务器)

连接上服务器后,就创建工厂,并使用JedisConnectionFactory类就可以创建出所需要的RedisTemplate;

同时,需要在配置文件中增加Redis数据库地址等配置spring.redis.XXX=??

对Redis来说存储的数据只是一个字节数,各种数据类型都必须转换为String,在Spring DataRedis默认实现中提供了多种序列化处理工具。

1·StringRedisSerializer:将字符串对象序列化为比特数组

2·JdkSerializationRedisSerializer:Java默认的对象序列化方式,对象需要实现Serializer接口。

3·GenericToStringSerializer:通用的字符串与比特对象的序列化转换处理,与String RedisSerializer的区别是使用Spring提供的转换器进行转换,可以支持将更多类型的对象进行转换,而前者只能转换字符串类型的对象。

4·Jackson2JsonRedisSerializer:将对象序列化为JSON字符串。5·JacksonJsonRedisSerializer:已废弃。

6·OxmSerializer:使用Spring的O/X映射,将对象序列化为XML字符串。7·GenericJackson2JsonRedisSerializer:序列化为JSON字符串,同时包含了对象类型信息

key使用第一个,value使用第七个;第七个会产生@Class属性,用来记录相应对象的类名,所以当反序列化时不需要再指定对象类型信息了,会引发一个问题,当序列化和反序列化的对象类全名称不一致时,就会造成序列化错误。多发生在分布式应用中,所以项目中最好实现RedisSerializer接口。

1为要缓存的对象UserDto编写相应的RedisTemplate:

@Bean(name=“userRedisTemplate”)…

2.reids存储、访问每一个值都是通过key来实现的,所以定义一个数据仓库(Repository)——UserRedisRepository用来统一处理UserDto与Redis的操作:

@Repository 

public class UserRedisRepository{

@Autowired 

@Qualifier("userRedisTemplate")织入服务

protected String buildKey(){}

}

改造用户微服务

@Service
        public class UserServiceImpl implements UserService {
            protected Logger logger = LoggerFactory.getLogger(this.getClass());
            @Autowired
            protected UserRemoteClient userRemoteClient;
            @Autowired
            protected UserRedisRepository userRedisRepository;
            @Override
            public UserDto load(Long id) {
              // 首先从Redis中获取
              UserDto userDto = this.userRedisRepository.findOne(id);
              if (null ! = userDto) {
                  this.logger.debug("已从Redis缓存中获取到用户:{} 的信息", id);
                  return userDto;
              }
                // 如果Redis中不存在,就从远程获取
              this.logger.debug("Redis缓存中不存在用户:{} 的信息,尝试从远程进行加载", id);
              userDto = this.userRemoteClient.load(id);
              if (null ! = userDto) {
                  // 获取后将用户信息进行缓存
                  this.userRedisRepository.saveUser(userDto);
              }
              return userDto;
            }
        }

代码中的UserRemoteClient其实就是一个使用Feign封装用户微服务访问的客户端(第三章),代码如下:

  @FeignClient("USERSERVICE")
        public interface UserRemoteClient {
            @RequestMapping(value = "/users/{id}", method = RequestMethod.GET)
            UserDto load(@PathVariable("id") Long id);
        }

也可以使用RestTemplate直接来获取远程用户信息


/ /注意调用前需要RestTemplate对象
        ResponseEntity<UserDto> restExchange =
            this.restTemplate.exchange(
              "http://userservice/users/{userId}",
              HttpMethod.GET,
              null,
              UserDto.class,
              userId);
        UserDto user = restExchange.getBody();

9.4.2 为用户微服务添加消息发送功能 见stream工程

(1)首先需要构建一个用户变更消息对象,该对象至少需要包含如下内容:变更用户的ID、变更事件类型(更新还是删除)等数据。(2)构建消息发送处理器(3)修改用户管理服务中的保存、删除等功能,当用户信息更新或删除时就构建一个用户信息变更消息,并通过上一步所提供的消息发送处理器发送该消息。\

第一步,包含action,userId,traceId,traceid如果是微服务之间的直接调用,那么该ID就可以通过Sleuth机制进行传递,不需要开发者介入。但是,如果开发者是通过消息中间件进行处理,那么该ID就不会传递下去。复制一份到商品微服务;

第2步,发送器添加依赖spring-cloud-starter-strean-kafka,@EnableBinding(Source.class),启用一个消息代理,同时绑定到Source接口所定义的消息通道(output)中

用户变更消息的发送,见UserMsgSender;

output()返回MessageChannel对象,通过该消息通道就可以将消息发送给消息代理,然后消息代理再将消息发送给具体的消息中间件

第3步,在用户管理服务的代码中增加消息发送代码见UserService

那么,Spring Cloud怎么知道具体发送到哪个消息中间件及哪个主题呢?因此,我们还需要对用户微服务做一些配置,告诉Spring Cloud相应的Kafka地址等配置信息。

    # Kafka及zookeeper配置信息
    spring.cloud.stream.bindings.output.destination=cd826-cloud-usertopic
    spring.cloud.stream.bindings.output.content-type=application/json
    spring.cloud.stream.kafka.binder.brokers=localhost
    spring.cloud.stream.kafka.binder.defaultBrokerPort=9092
    spring.cloud.stream.kafka.binder.zkNodes=localhost

zkNodes属性指定了所要连接到的Zookeeper的节点。


9.4.3 为商品微服务添加消息监听功能

1kafka依赖,@EnableBindling(Sink.class)启动时绑定消息代理及监听处理,kafka配置,

spring.cloud.stream.bindings.input.group是需要设置的分组名称,这里将其设置为productGroup,所建立的缓存是一个分布式缓存,当其中一个微服务实例将用户信息加入到Redis数据中后,其他商品微服务实例都能使用到。自然的,当有用户变更消息时,也只需要处理其中一个商品微服务实例即可。

所以每一个商品微服务的实例都需要将该配置值设置为productGroup。

见UserMsgListener类;

这里为了统一,将消息代理的绑定、监听代码及具体业务处理全部统一到一个类UserMsgListener中。

RDM和Medis:可视化redis客户端工具;

9.4.5 自定义消息通道

首先需要增加一个自定义消息发送或者接收的接口;然后将消息发送或者消息监听者连接到该通道上;最后修改项目配置文件,将该消息通道绑定到消息中间件具体的消息主题上。

下面以商品微服务为例,增加一个名称为inboundUserMsg的消息通道:

1自定义消息通道接收接口见:SpringCloudBookChannels

关键是需要定义一个返回值为SubscribableChannel的方法。该方法的方法名称可以自定义,但返回值必须是SubscribableChannel类型,同时在该方法上增加@Input注解,注解的参数就是自定义的消息通道名称,

2自定义一个消息发送通道见userMsgSender();

3修改商品微服务中UserMsgListener消息监听的处理代码

4最后还需要修改商品微服务中Stream的绑定配置

spring.cloud.stream.bindlings.inboundUserMsg.group=productGroup;


9.5 Spring Cloud Stream高级主题

如何测试,捕捉异常

9.5.1 单元测试

提供了一个TestSupportBinder来支持单元测试,可以让开发者在没有连接到消息中间件的情况下完成测试。通过TestSupportBinder可以模拟访问消息通道,并进行消息的发送与监听。

对于消息发送,TestSupportBinder会注册一个类型为MessageCollector的Bean,通过该Bean可以获取到所发送的消息,这样就可以判断消息是否发送成功。对于消息监听测试,则可以通过直接向入站通道发送消息进行模拟。

@RunWith(SpringJUnit4ClassRunner.class)@SpringApplicationConfiguration(classes = ExampleTest.MyProcessor.class)@IntegrationTest({"server.port=-1"})@DirtiesContextpublic class ExampleTest {@Autowiredprivate Processor processor;@Autowiredprivate BinderFactory<MessageChannel> binderFactory;// 通过注入该Bean,可以判断消息发送是否成功
            @Autowiredprivate MessageCollector messageCollector;

            @Test@SuppressWarnings("unchecked")public void testWiring() {// 这里模拟发送一个hello的消息
              Message<String> message = new GenericMessage<>("hello");
              processor.input().send(message);// 通过messageCollector获取上面所发送的消息,并判断是否发送成功
              Message<String> received =(Message<String>) messageCollector.forChannel(
                      processor.output()).poll();assertThat(received.getPayload(), equalTo("hello world"));}

            // 这里绑定了Processor, Spring Cloud Stream会同时创建消息发送通道和消息监听通道
            // 这样可以同时进行消息发送和监听的测试
            @SpringBootApplication@EnableBinding(Processor.class)public static class MyProcessor {@Autowiredprivate Processor channels;@Transformer(
                  inputChannel = Processor.INPUT,
                  outputChannel = Processor.OUTPUT)public String transform(String in) {return in + " world";}}}

9.5.2 错误处理

一般消息的发送者将消息发送到通道之后就去处理其他事情了,这时候根本没有办法将异常信息发送回给消息发送者。

Spring Cloud Stream提供了一个全局错误消息处理通道,当出现异常时,Spring Cloud Stream就会将该异常包装成ErrorMessage,然后发送到该消息通道中。默认该消息通道的名称为errorChannel,可以通过项目配置文件中的spring.cloud.stream.bindings.error.destination属性来指定通道的名称

9.5.3 消息处理分发

Spring Cloud Stream从1.2版本开始,支持将同一个消息通道中的消息,根据条件分发给不同的方法进行处理。相应的方法除了需要@StreamListener注解外,还需要满足以下条件:

·该方法没有返回值。

·该方法只能处理独立的消息,不能是响应式消息处理器。

消息分发的条件可以通过@StreamListener注解中的condition属性设定,条件可以使用SpEL表达式(关于SpEL表达式,可以参考:https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#expressions)。

在进行消息分发处理时,Spring Cloud Stream会对每一个条件进行求值,所有符合条件的方法都会在同一个线程中执行,但并不保证执行的顺序。

消息分发实例:

    @EnableBinding(Sink.class)@EnableAutoConfigurationpublic static class TestPojoWithAnnotatedArguments {// 当消息头中type参数的值为foo时,使用该方法进行消息处理
        @StreamListener(target = Sink.INPUT, condition = "headers['type']=='foo'")public void receiveFoo(@Payload FooPojo fooPojo) {// 省略具体消息处理
    ……
        }// 当消息头中type参数的值为bar时,使用该方法进行消息处理
        @StreamListener(target = Sink.INPUT, condition = "headers['type']=='bar'")public void receiveBar(@Payload BarPojo barPojo) {  // 省略具体消息处理
        ……
            }}

9.5.4 消费者组与消息分区

发布-订阅模式通过共享主题使应用之间的连接更加容易,但是应用的水平扩展也是非常重要的。通常,对于一个消息只需要一个实例进行处理即可,所以当一个应用存在多个实例时,这些实例之间便会成为同一个消息相互竞争的消费者。(一个应用多个实例,一个实例消费一个消息)

Spring Cloud Stream通过消费者组的概念给这种情况进行建模。既然是一个组,那么组内必然可以有多个消费者或消费者实例(也就是微服务实例),它们之间共享一个相同的ID,即消费者组ID。消费者组内的成员统一在一起消费所订阅消息中的所有消息,而消息中的每个分区只能由同一个消费者组内其中的一个消费者(应用)来消费(多个微服务实例在同一组共同消费所订阅的所有消息(消息按分区分开,一个分区只由组内一个消费者消费))

默认情况下,如果没有为应用指定消费者组,SpringCloud Stream会为该应用创建一个匿名组,并且该组中只有其一个应用。开发者也可以在应用的配置文件中设置spring.cloud. stream.bindings.input.group属性来指定所属消费者组的ID。一般来说,在创建应用时,最好为其指定一个消费者组,这样可以防止当启动多个应用实例时收到重复的消息(除非你的应用需要处理每个应用实例)(不指定组的情况,一个应用就是一个消费者组)

消费者组还有一个概念:再平衡(rebalance),也可以将rebalance理解成一种协议,其用来规定一个消费者组下的所有消费者成员(应用)如何分配所订阅的消息通道中的消息分区。如果一个消息有10个分区,消费者组中有5个消费成员(应用),那么就会为每一个消费成员(应用)分配2个分区的消息。当组内成员发生变更(新应用上线,或应用实例下线),或者消息分区改变时都会引起rebalance。那么rebalance又是如何进行相关处理的呢?(消息按分区,消费成员(应用),分配消息,涉及到组和成员的关系)

以Kafka为例,Kafka 0.8版本以前每一个消费者都会创建一个基于Zookeeper的消费者连接器。当有消费者变更时,都会触发基于Zookeeper的消费组操作,每个消费者都会在客户端执行分区分配算法,然后再从全局的分配结果中获取属于自己的分区。这样做的缺点就是消费者会和Zookeeper产生频繁的交互,给Zookeeper集群造成压力,并且非常容易产生羊群效应和脑裂等问题。在Kafka0.8版本以后重新设计了客户端,并且引入了协调者和消费者组管理协议。由协调者负责消费者组的管理,并将分区分配在消费组的一个主消费者中完成。而每个消费者在加入一个消费者组时都需要完成加入组请求和同步组请求两个动作,从而完成rebalance处理。()

Spring Cloud Stream除了对消费者提供了消费分组的支持外,还对一个给定应用的多个实例之间的消息消费提供了支持——数据分片。在Spring Cloud Stream所提供的数据分片方案中,消息中间件的一个主题可以视为分隔成为多个分片,并确保消息发送者所发送的具有相同特征的消息数据可以被同一消费者实例所处理

此外,SpringCloud Stream对分割的进程实例实现进行了抽象,使得不具备分区功能的消息中间件(如RabbitMQ)也能具有数据分区功能。

9.5.5 消息绑定器

抽象的绑定器作为中间层,实现了与具体消息中间件连接;

通过暴露的统一的消息通道进行消息的发送与监听;

image-20200513095023553

在进行消息发送前,需要调用绑定器的bindProducer()方法,并根据所要绑定的具体消息代理,创建一个消息通道。bindProducer()方法有以下3个参数。

·name:要绑定的消息代理的名称。·outboundBindTarget:本地中用来发送消息的通道。·producerProperties:创建消息通道时的参数,如分区配置等。

对于消息监听也一样,需要调用bindConsumer()方法,创建一个消息监听通道。bindConsumer ()方法也有以下4个参数。

·name:要绑定的消息代理的名称。·group:消费者组名。·inboundBindTarget:本地中用来进行消息监听的通道。·consumerProperties:创建消息通道时的参数。

Spring Cloud Stream的绑定器SPI(Service ProviderInterface)由数个接口、一些开箱即用的工具类及服务发现策略组成,并提供了可插入机制实现了与外部多种消息中间件的连接。对于绑定器SPI最核心的就是上面所说的绑定接口,见Binder

实现一个消息绑定器需要以下3步:(1)实现Binder接口。(2)通过@Configuration注解对上面的实现类及所要连接的消息中间件进行相关配置的处理,并创建一个Bean。(3)在classpath下的META-INF/spring.binders文件中(如果没有可自行添加该文件)按照下面的格式配置该绑定器

kafka:\
        org.springframework.cloud.stream.binder.kafka.config.KafkaBinder
        Configuration

如果项目中包含了多种类型的消息中间件,那么可以通过下面的配置来设置默认绑定器,或者设置某个消息通道所使用的绑定器。

/ 配置默认的绑定器
        spring.cloud.stream.defaultBinder=kafka
        // 配置input通道所使用的绑定器
        spring.cloud.stream.bindings.input.binder=kafka
        // 配置output通道所使用的绑定器
        spring.cloud.stream.bindings.output.binder=rabbit

        // 也可以针对不同通道设置所连接到的中间件
        // input通道连接到192.168.0.1的RabbitMQ
        spring.cloud.stream.bindings.input.destination=foo
        spring.cloud.stream.bindings.input.binder=rabbit1
        spring.cloud.stream.binders.rabbit1.type=rabbit
        spring.cloud.stream.binders.rabbit1.environment.spring.rabbitmq.host=
        192.168.0.1

        // output通道连接到192.168.0.2的RabbitMQ
        spring.cloud.stream.bindings.output.destination=bar
        spring.cloud.stream.bindings.output.binder=rabbit2

        spring.cloud.stream.binders.rabbit2.type=rabbit
        spring.cloud.stream.binders.rabbit2.environment.spring.rabbitmq.host=
        192.168.0.2

注意:如果读者在项目中手动显式地进行了消息绑定器的配置,Spring Cloud Stream就会禁用掉默认的消息绑定器配置,因此此时必须保证项目中所用的每一个消息绑定器都有相应的配置。


9.6 消息总线——Spring Cloud Bus

Spring Cloud Config中,如何通过Spring Cloud Bus自动刷新应用配置。

。Spring Cloud Bus建构在Spring Cloud Stream之上,是一个轻量级的通信组件,可以将分布式系统中的节点与轻量级消息代理连接,从而实现状态更改(如上面说的配置信息更改)广播或其他事件的广播。

在实现上Spring Cloud Bus基于Spring事件驱动模型。Spring事件驱动模型包含以下3个基本概念。·事件:ApplicationEvent;·事件监听者:ApplicationListener;·事件发布者:ApplicationEventPublisher。

Spring中的事件驱动模型其实是观察者模式的典型应用,通过这种处理方式可以解耦目标对象和它的依赖对象

9.6.1 完成配置自动刷新配置

消息驱动——Stream_第3张图片

从图9-22中可以看到,当修改配置数据并提交到版本管理之后,开发者只需要在配置服务器中访问/bus/refresh端点,这样配置服务器就会发布配置刷新事件,商品微服务和用户微服务监听到事件之后就会自动执行配置刷新处理。下面让我们着手修改代码。

1添加依赖spring-cloud-starter-bus-kafka,若是rabbitMQ,加spring-cloud-starter-bus-amqp;

2服务器配置

 关闭管理端点的安全认证,不然执行时会提示没有权限
        management.security.enabled=false

        # 针对Kafka
        spring.cloud.stream.kafka.binder.brokers=localhost
        spring.cloud.stream.kafka.binder.defaultBrokerPort=9092
        spring.cloud.stream.kafka.binder.zkNodes=localhost
        # 针对RabbitMQ
        # spring.rabbitmq.host=localhost
        # spring.rabbitmq.port=5672
        # spring.rabbitmq.username=yourname
        # spring.rabbitmq.password=yourpass

如果不配置,springboot会默认自动配置机制;

3修改商品微服务,加bus依赖,请求返回中更改从应用上下文中获取配置参数的foo值,用户微服务同理;然后测试,更改配置查看;

执行过程?通过下面命令擦看kafka中主题列表:

    > bin/kafka-topics.sh --list --zookeeper localhost:2181
    # 将会获取到如下列表
        __consumer_offsets
        cd826-cloud-usertopic
        inboundUserMsg
        output
        springCloudBus


在列表最后有一个springCloudBus主题,是为配置刷新时所用。

仔细观察配置服务和商品微服务的控制台输出,可以看到输出,说明配置服务器已经连接到Kafka服务器上的springCloudBus主题,商品微服务则订阅了springCloudBus主题的消息;

假如,在更新配置后只想刷新部分微服务,那么此时可在访问/bus/refresh端点时通过destination参数来指定所要刷新的微服务。例如,/bus/refresh? destination=productservice:2200,这样只会刷新服务名称为productservice、端口为2200的微服务。同时在参数中还可以使用通配符,如productservice:**,表示要刷新所有productservice实例。


9.6.2 发布自定义事件

通过Spring Cloud Bus也可以发布自定义事件,所发布的事件需要继承自Remote ApplicationEvent。在发布事件时默认会将事件转换为JSON格式,在反序列化时也需要使用到该事件的类型。因此,事件发布者和监听者都需要访问这个事件类,或者保持这两个类一致。也可以用@JsonTypeName注解来自定义序列化中的类名,但在接收端也要有同样的定义。

1用户微服务定义事件

MyBusEvent,

2商品微服务中复制,添加一个事件监听类MyBusEventListener

在用户微服务中增加触发事件的测试端点UserEventEndpoint

3引导类中加@RemoteApplicationEventScan注解

在Postman中通过Post方式访问http://localhost:2100/bus/events/user-test

dBus主题,商品微服务则订阅了springCloudBus主题的消息;

假如,在更新配置后只想刷新部分微服务,那么此时可在访问/bus/refresh端点时通过destination参数来指定所要刷新的微服务。例如,/bus/refresh? destination=productservice:2200,这样只会刷新服务名称为productservice、端口为2200的微服务。同时在参数中还可以使用通配符,如productservice:**,表示要刷新所有productservice实例。


9.6.2 发布自定义事件

通过Spring Cloud Bus也可以发布自定义事件,所发布的事件需要继承自Remote ApplicationEvent。在发布事件时默认会将事件转换为JSON格式,在反序列化时也需要使用到该事件的类型。因此,事件发布者和监听者都需要访问这个事件类,或者保持这两个类一致。也可以用@JsonTypeName注解来自定义序列化中的类名,但在接收端也要有同样的定义。

1用户微服务定义事件

MyBusEvent,

2商品微服务中复制,添加一个事件监听类MyBusEventListener

在用户微服务中增加触发事件的测试端点UserEventEndpoint

3引导类中加@RemoteApplicationEventScan注解

在Postman中通过Post方式访问http://localhost:2100/bus/events/user-test

你可能感兴趣的:(springcloud)