系列往期回顾:
学习微服务系列(三):springboot+页面前后端分离与RESTFUL风格接口编写
学习微服务系列(四):springboot服务gateway网关
学习微服务系列(五):springboot微服务使用nacos作为注册中心
学习微服务系列(六):springboot微服务使用nacos作为配置中心
学习微服务系列(八):springboot服务分布式事务及解决方案
学习微服务系列(九):springboot服务接口安全认证设计
在分布式服务系统中我们经常会出现有些需要进行异步通信的场景,举个例子来讲:在电商业务中订单服务调用商品服务时候,我们采用的是同步的方式。订单服务调用商品服务,商品服务库存进行操作。如果订单服务这里需要同时调用支付服务、商品服务多个服务时,等待各个服务响应完,整个订单请求才算执行完毕,这对用户的使用体验大打折扣。用户就会一直在网页端进行等待,等待程序的处理。而异步时,客户端请求不会阻塞进程,服务端的响应可以是非即时的。这样用户的体验就会很好。我们之前的文章中对于分布式事务的处理也用到了MQ消息中间件。
消息队列产品
我们现在互联网主流的异步中间件就是MQ,MQ主要使用以下几种:
- rabbitMQ
- kafka
- rocketMQ
整个异步消息中间件的核心作用是应用解耦,流量削峰填谷及消息通讯,下面我们整体介绍一下MQ在系统中的使用。之后在单独介绍一下rocketMQ。
MQ的异步处理能力
我们用用户注册一个系统成功后需要发送邮件和短信举例,当用户注册成功系统需要给注册成功的用户发送邮件和短信告知,在这个场景中传统的做饭有2种,串行和并行方式。如下图所示:
- 同步进行:将注册信息写入数据库成功后,发送注册邮件,再发送注册短信。以上三个任务全部完成后,返回给客户端。假设三个业务节点每个使用50毫秒钟,不考虑网络等其他开销,则串行方式的时间是150毫秒。
- 并行方式:将注册信息写入数据库成功后,发送注册成功消息到MQ中,之后发送邮件服务监听读取MQ进行发送邮件,同时监听读取发送短息服务进行短信的发送。按照以上约定,用户的响应时间相当于是注册信息写入数据库的时间+发送到消息队列的时间,也就是55毫秒。
按照上面的对比,写入消息队列的速度很快,基本可以忽略,因此用户的响应时间可能是50毫秒。
MQ的应用解耦能力
针对于MQ的解耦能力我们也同样举个例子来说明:在电商业务场景中以用户下单购买业务为例。用户下单后,订单系统需要通知库存系统。传统的做法是,订单系统调用库存系统的接口。如下图:
传统模式的缺点:
- 假如库存系统无法访问,则订单减库存将失败,从而导致订单失败。
- 订单系统与库存系统耦合。 如何解决以上问题呢?引入应用消息队列后的方案,如下图:
如上图进行应用解耦后:
- 订单系统:用户下单后,订单系统完成持久化处理,将消息写入消息队列,返回用户订单下单成功。
- 库存系统:订阅下单的消息,采用拉/推的方式,获取下单信息,库存系统根据下单信息,进行库存操作。 基于修改后解耦的服务在下单时库存系统不能正常使用。也不影响正常下单,因为下单后,订单系统写入消息队列就不再关心其他的后续操作了。实现订单系统与库存系统的应用解耦。
流量削峰填谷能力
流量削峰填谷也是消息队列中的常用场景,一般在秒杀或团抢活动中使用广泛。 秒杀活动,一般会因为流量过大,导致流量暴增,应用挂掉。为解决这个问题,需要在应用前端加入消息队列。
- 可以控制活动的人数。
- 可以缓解短时间内高流量压垮应用。
- 用户的请求,服务器接收后,首先写入消息队列。
- 秒杀业务根据消息队列中的请求信息,再做后续处理。
消息队列中间件的对比(借用网上一个图)
RocketMQ
我们了解了消息中间件的作用和特点之后我们重点介绍一下其中一个---rocketMQ。RocketMQ是阿里开源的消息中间件,目前在Apache孵化,使用纯Java开发,具有高吞吐量、高可用性、适合大规模分布式系统应用的特点。RocketMQ思路起源于Kafka,但并不是简单的复制,它对消息的可靠传输及事务性做了优化,目前在阿里集团被广泛应用于交易、充值、流计算、消息推送、日志流式处理、binglog分发等场景,支撑了阿里多次双十一活动。
如上图所示,整体可以分成4个角色,分别是:Producer,Consumer,Broker以及NameServer;
- NameServer 可以理解为是消息队列的注册中心,所有的Broker都注册到nameServer上。
- Broker Broker是RocketMQ的核心,提供了消息的接收,存储,拉取等功能,一般都需要保证Broker的高可用,所以会配置Broker Slave,当Master挂掉之后,Consumer然后可以消费Slave; Broker分为Master和Slave,一个Master可以对应多个Slave,Master与Slave的对应关系通过指定相同的BrokerName,不同的BrokerId来定义,BrokerId为0表示Master,非0表示Slave;
- Producer 消息队列的生产者,需要与NameServer建立连接,从NameServer获取Topic路由信息,并向提供Topic服务的Broker Master建立连接;
- Consumer 消息队列的消费者,同样与NameServer建立连接,从NameServer获取Topic路由信息,并向提供Topic服务的Broker Master,Slave建立连接;
- Topic和Message Group 在介绍完以上4个角色以后,还需要重点介绍一下上面提到的Topic和Message Group;字面意思就是主题,用来区分不同类型的消息,发送和接收消息前都需要先创建Topic,针对Topic来发送和接收消息,为了提高性能和吞吐量,引入了Message Group,一个Topic可以设置一个或多个Message Group;
代码测试
- 生产者
public class DemoProducer {
public static void main(String[] args) throws Exception {
// 构造Producer
DefaultMQProducer producer = new DefaultMQProducer("GroupName");
producer.setNamesrvAddr("192.168.0.12:9876");
// 初始化Producer,整个应用生命周期内,只需要初始化1次
producer.start();
for (int i = 0; i < 10; i++) {
Message msg = new Message("Topic1", "Tag1",
("Hello World" + i).getBytes(RemotingHelper.DEFAULT_CHARSET));
SendResult sendResult = producer.send(msg);
System.out.println(sendResult);
}
producer.shutdown();
}
}
- 消费者
public class DemoConsumer {
public static void main(String[] args) throws MQClientException {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("");
consumer.setNamesrvAddr("192.168.0.12:9876");
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
consumer.subscribe("Topic1", "*");
consumer.registerMessageListener(new MessageListenerConcurrently() {
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
ConsumeConcurrentlyContext context) {
System.out.printf(Thread.currentThread().getName() + "Messages :" + msgs + "%n");
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
consumer.start();
}
}
消息顺序消费
在上面的技术架构介绍中,我们已经知道了 RocketMQ 在topic上是无序的、那么我们怎么能保证它有序的呢。其实很简单,我们需要处理的仅仅是将同一语义下的消息放入同一个队列(比如这里是同一个订单),那我们就可以使用 Hash 取模法来保证同一个订单在同一个队列中就行了。我们需要定义一个固定的key值,那么key值相同则会发送到相同的队列中。
public Boolean aliSent(Object body, String shardingKey) {
String topic = TOPIC;
Message msg = new Message(topic,TAG,JSON.toJSONString(body).getBytes());
SendResult sendResult = orderProducer.send(msg, shardingKey);
log.info("AliRocketMQ发送消息Message Id:{},||shardingKey:{},||消息体:{}", sendResult.getMessageId(), shardingKey,JSON.toJSONString(body));
return true;
}
消息重复消费
根本解决重复消费问题就是两个字-幂等。所以我们需要给我们的消费者实现幂等,也就是对同一个消息的处理结果,执行多少次都不变。可以使用写入 Redis 来保证,因为 Redis 的 key 和 value 就是天然支持幂等的。当然还有使用数据库插入法,基于数据库的唯一键来保证重复数据不会被插入多条。
更多内容请关注: IT技术小栈