微服务通讯之间有同步和异步两种方式
同步通讯:就像打电话,需要实时响应
异步通讯:发邮箱一样,不需要马上回复
同步通讯
就像串行一样,优点:时效性强,可以立即得到结果
问题:
耦合度高:每次加入新的需求,都要修改原来的代码
性能下降:调用者需要等待服务提供者响应,如果调用链较长则响应事件等于每次调用的事件之和
资源浪费:调用链中每个服务在等待响应过程中,不能释放请求占用的资源,高并发场景下会极度浪费系统资源
级联失败:如果服务提供者中一项出现问题,所有调用方都会出现问题,导致微服务故障
异步通讯
异步调用常见的实现就是事件驱动模式
事件发布方和订阅者之间有一个中间人Broker, 发布者发布事件到Broker,不关心谁来订阅事件。订阅者从Broker订阅事件,不关心谁发来的消息。
好处:吞吐量提升:无需等待订阅者处理完成,响应更迅速
故障隔离:服务没有之间调用,不存在级联失败的问题
调用者之间没有阻塞,不会造成无效的资源占用。耦合度低,每个服务之间都可以灵活插拨,可替换。流量削峰:不过发布事件的流量波动多大,都由Broker接收,订阅者按照自己的速度处理事件
缺点:架构复杂了,业务没有明显的流程线,不好处理。需要依赖于Broker的可靠,安全,性能
MQ技术,分布式消息队列,也就是事件驱动架构中的Broker
消息中间件对比
RabbitMQ
部署指南
安装镜像:
方式一:在线下载 docker pull rabbitmq:management
方式二:将rabbitmq.tar上传到虚拟机,执行命令安装: docker load -i rabbitmq.tar
运行MQ容器
docker run -id --name rabbitmq -e RABBITMQ_DEFAULT_USER=itcast -e RABBITMQ_DEFAULT_PASS=123321 -p 5672:5672 -p 15672:15672 rabbitmq:management
访问: http://ip:15672/ ip为你的虚拟机ip地址
15672 http端口 控制台 5672 编程端口
MQ的基本结构
publisher:生产者
consumer:消费者
exchange:交换机,负责消息路由
queue:队列,缓存信息,默认在内存中
virtualHost:虚拟主机 ,隔离不同租户的exchange、queue、消息的隔离
Connection:通道 Channel:虚拟信道
RabbitMQ消息模型
基本消息队列BasicQueue
工作消息队列WorkQueue
发布订阅(publish,Subscribe),根据交换机类型不同分为三种:
Fanout Exchange广播
DirectExchange路由
TopicExchange主题
SpringAMQP
基于RabbitMQ封装的模板,利用啦SpringBoot对其实现啦自动装配
SpringAMQP提供的三个功能:自动声明队列,交换机及其绑定关系。基于注解的监听器模式,异步接收消息。封装RabbitTemplate工具,用于发送消息。
publisher:消息发送者
consumer:消息消费者
1.BasicQueue简单队列模型
在父工程导入依赖
<!--AMQP依赖,包含RabbitMQ-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
消息发送:配置MQ地址,在publisher服务的application.yml中添加配置:
spring:
rabbitmq:
host: 192.168.150.101 # 主机名
port: 5672 # 端口
virtual-host: / # 虚拟主机
username: itcast # 用户名
password: 123321 # 密码
在publisher服务中编写测试类SpringAmqpTest,并利用RabbitTemplate实现消息发送
@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringAmqpTest {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void testSimpleQueue(){
// 队列名称
String queueName = "simple.queue";
// 消息
String message = "hello, spring amqp!";
// 发送消息
rabbitTemplate.convertAndSend(queueName, message);
}
}
消息接收:配置MQ地址,在consumer服务的application.yml中添加配置:
spring:
rabbitmq:
host: 192.168.150.101 # 主机名
port: 5672 # 端口
virtual-host: / # 虚拟主机
username: itcast # 用户名
password: 123321 # 密码
在consumer服务的RabbitConfig类中添加初始化代码用于创建队列
/**简单队列模型-队列名*/
public static final String SIMPLE_QUEUE = "simple.queue";
//简单队列模型下,创建一个队列
@Bean
public Queue simpleQueue()
{
return new Queue(SIMPLE_QUEUE);
}
然后在consumer服务的cn.itcast.mq.listener
包中新建一个类SpringRabbitListener
@Component
public class SpringRabbitListener {
@RabbitListener(queues = RabbitConfig.SIMPLE_QUEUE)
public void simpleQueue(String message){
System.out.println("简单模型消费者-消费到消息:" +message );
}
}
启动consumer服务,然后在publisher服务中运行测试代码,发送MQ消息
使用默认的交换机,根据与队列同名的路由key找到队列
WorkQueue
任务模型,多个消费者绑定到一个队列,共同消费队列中的消息(默认消息平均分配的),用于提升效率,有资源竞争的场景
消息发送:循环发送,模拟大量消息堆积现象。在publisher服务中的SpringAmqpTest类中添加一个测试方法
/**
* workQueue
* 向队列中不停发送消息,模拟消息堆积。
*/
@Test
public void testWorkQueue(){
for (int i = 1; i <= 110; i++) {
rabbitTemplate.convertAndSend(RabbitConfig.WORK_QUEUE,"work.message"+i);
}
}
消息接收,在consumer服务的RabbitConfig类中添加初始化代码用于创建队列
/**工作队列模型-队列名称*/
public static final String WORK_QUEUE = "work.queue";
@Bean
public Queue workQueue()
{
return new Queue(WORK_QUEUE,true);
}
拟多个消费者绑定同一个队列,我们在consumer服务的SpringRabbitListener中添加2个新的方法
@RabbitListener(queues = RabbitConfig.WORK_QUEUE)
public void workQueue1(String message){
System.out.println("工作队列模型消费者1-消费到消息:" +message );
try {
TimeUnit.MILLISECONDS.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@RabbitListener(queues = RabbitConfig.WORK_QUEUE)
public void workQueue2(String message){
System.out.println("工作队列模型消费者2-消费到消息:" +message );
try {
TimeUnit.MILLISECONDS.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
启动ConsumerApplication后,在执行publisher服务中刚刚编写的发送测试方法testWorkQueue。这个时候发现,消息是平均分配给每个消费者的
spring中有一个简单的配置,可以解决这个问题,实现能者多劳,设置prefetch来控制消费者预取的消息数量
spring:
rabbitmq:
listener:
simple:
prefetch: 1 # 每次只能获取一条消息,处理完成才能获取下一个消息
发布/订阅模型
Exchange交换机只负责转发消息,不具备存储消息的能力,如果没有队列和它绑定,或者没有符合路由规则的队列,消息丢失。
交换机的作用:接收publisher发送的消息,将消息按照规则路由到与之绑定的队列,能缓存消息,路由失败,消息丢失。FanoutExchange的会将消息路由到每个绑定的队列
Spring提供了一个接口Exchange,来表示所有不同类型的交换机:
针对不同的类型使用不同的Exchange,主要是使用最底下的三个
Fanout广播
广播模式,消息发送流程:可以有多个队列。每个队列都要绑定到Exchange(交换机)。生产者发送的消息,只能发送到交换机,交换机来决定要发给哪个队列,生产者无法决定。交换机把消息发送给绑定过的所有队列。订阅队列的消费者都能拿到消息
他的效率是最高的,场景:群发消息
消息接收:在consumer的RabbitConfig类中声明队列和交换机及绑定关系
/**广播模型-交换机名*/
public static final String FANOUT_EXCHANGE = "fantout.exchange";
/**广播模型-队列1名*/
public static final String FANOUT_QUEUE1 = "fanout.queue1";
/**广播模型-队列2名*/
public static final String FANOUT_QUEUE2 = "fanout.queue2";
//声明队列1
@Bean
public Queue fanoutQueue1(){
return new Queue(FANOUT_QUEUE1);
}
//声明队列2
@Bean
public Queue fanoutQueue2(){
return new Queue(FANOUT_QUEUE2);
}
//声明交换机
@Bean
public FanoutExchange fanoutExchange(){
return new FanoutExchange(FANOUT_EXCHANGE);
}
//绑定队列一和交换机
@Bean
public Binding bindingQueue1(Queue fanoutQueue1,FanoutExchange fanoutExchange){
return BindingBuilder.bind(fanoutQueue1).to(fanoutExchange);
}
//绑定队列二和交换机
@Bean
public Binding bindingQueue2(Queue fanoutQueue2,FanoutExchange fanoutExchange){
return BindingBuilder.bind(fanoutQueue2).to(fanoutExchange);
}
在consumer服务的SpringRabbitListener中添加两个方法,作为消费者
@RabbitListener(queues = RabbitConfig.FANOUT_QUEUE1)
public void fanout1(String message){
System.out.println("广播模型消费者1-消费到消息:" +message );
}
@RabbitListener(queues = RabbitConfig.FANOUT_QUEUE2)
public void fanout2(String message){
System.out.println("广播模型消费者2-消费到消息:" +message );
}
消息发送:在publisher服务的SpringAmqpTest类中添加测试方法
@Test
public void testFanout(){
rabbitTemplate.convertAndSend(RabbitConfig.FANOUT_EXCHANGE,null,"fanout message");
}
Direct 路由模式
在Direct模型下:
-
队列与交换机的绑定,不能是任意绑定了,而是要指定一个
RoutingKey
(路由key) -
消息的发送方在 向 Exchange发送消息时,也必须指定消息的
RoutingKey
。 -
Exchange不再把消息交给每一个绑定的队列,而是根据消息的
Routing Key
进行判断,只有队列的Routingkey
与消息的Routing key
完全一致,才会接收到消息
消息接收:在consumer的RabbitConfig类中声明队列和交换机及绑定关系
/**路由模型-交换机名*/
public static final String DIRECT_EXCHANGE = "direct.exchange";
/**路由模型-路由KEY1名称*/
public static final String DIRECT_ROUTEKEY_RED = "direct.red";
/**路由模型-队列1名称*/
public static final String DIRECT_QUEUE1 = "direct.queue1";
/**路由模型-路由KEY2名称*/
public static final String DIRECT_ROUTEKEY_BLUE = "direct.blue";
/**路由模型-队列2名称*/
public static final String DIRECT_QUEUE2 = "direct.queue2";
@Bean
public DirectExchange directExchange(){
return new DirectExchange(DIRECT_EXCHANGE);
}
@Bean //队列一
public Queue directQueue1(){
return new Queue(DIRECT_QUEUE1);
}
/**路由模型-创建对列2*/
@Bean
public Queue directQueue2(){
return new Queue(DIRECT_QUEUE2);
}
/**路由模型-绑定交换机和队列1*/
@Bean
public Binding bindDirectQueue1(DirectExchange directExchange,Queue directQueue1){
return BindingBuilder.bind(directQueue1).to(directExchange).with(DIRECT_ROUTEKEY_BLUE);
}
/**路由模型-绑定交换机和队列2*/
@Bean
public Binding bindDirectQueue2(DirectExchange directExchange,Queue directQueue2){
return BindingBuilder.bind(directQueue2).to(directExchange).with(DIRECT_ROUTEKEY_RED);
}
在consumer服务的SpringRabbitListener中添加两个方法,作为消费者:
@RabbitListener(queues = RabbitConfig.DIRECT_QUEUE1)
public void direct1(String message){
System.out.println("路由模型消费者1-消费到消息:" +message );
}
@RabbitListener(queues = RabbitConfig.DIRECT_QUEUE2)
public void direct2(String message){
System.out.println("路由模型消费者2-消费到消息:" +message );
}
消息发送:在publisher服务的SpringAmqpTest类中添加测试方法
@Test
public void testDirect(){
rabbitTemplate.convertAndSend(RabbitConfig.DIRECT_EXCHANGE, RabbitConfig.DIRECT_ROUTEKEY_RED,"red color");
rabbitTemplate.convertAndSend(RabbitConfig.DIRECT_EXCHANGE,RabbitConfig.DIRECT_ROUTEKEY_BLUE,"blue color");
}
描述下Direct交换机与Fanout交换机的差异?
-
Fanout交换机将消息路由给每一个与之绑定的队列
-
Direct交换机根据RoutingKey判断路由给哪个队列
-
如果多个队列具有相同的RoutingKey,则与Fanout功能类似
Topic主题模型
Topic
类型的Exchange
与Direct
相比,都是可以根据RoutingKey
把消息路由到不同的队列。只不过Topic
类型Exchange
可以让队列在绑定Routing key
的时候使用通配符!
Routingkey
一般都是有一个或多个单词组成,多个单词之间以”.”分割,例如: item.insert
通配符规则:
#
:匹配一个或多个词
*
:匹配不多不少恰好1个词
消息接收: 在consumer的RabbitConfig类中声明队列和交换机及绑定关系
/**主题模型-交换机名*/
public static final String TOPIC_EXCHANGE = "topic.exchange";
/**主题模型-路由KEY1名称*/
public static final String TOPIC_ROUTEKEY1 = "china.*";
/**主题模型-队列1名称*/
public static final String TOPIC_QUEUE1 = "topic.queue1";
/**主题模型-路由KEY2名称*/
public static final String TOPIC_ROUTEKEY2 = "#.news";
/**主题模型-队列2名称*/
public static final String TOPIC_QUEUE2 = "topic.queue2";
/**主题模型-创建交换机*/
@Bean
public TopicExchange topicExchange(){
return new TopicExchange(TOPIC_EXCHANGE);
}
/**主题模型-创建队列1*/
@Bean
public Queue topicQueue1(){
return new Queue(TOPIC_QUEUE1);
}
/**主题模型-创建对列2*/
@Bean
public Queue topicQueue2(){
return new Queue(TOPIC_QUEUE2);
}
/**主题模型-绑定交换机和队列1*/
@Bean
public Binding bindTopicQueue1( TopicExchange topicExchange,Queue topicQueue1){
return BindingBuilder.bind(topicQueue1).to(topicExchange).with(TOPIC_ROUTEKEY1);
}
/**主题模型-绑定交换机和队列2*/
@Bean
public Binding bindTopicQueue2( TopicExchange topicExchange, Queue topicQueue2){
return BindingBuilder.bind(topicQueue2).to(topicExchange).with(TOPIC_ROUTEKEY2);
}
在consumer服务的SpringRabbitListener中添加两个方法,作为消费者
@RabbitListener(queues = RabbitConfig.TOPIC_QUEUE1)
public void topic1(String message){
System.out.println("主题模型消费者1-消费到消息:" +message );
}
@RabbitListener(queues = RabbitConfig.TOPIC_QUEUE2)
public void topic2(String message){
System.out.println("主题模型消费者2-消费到消息:" +message );
}
消息发送:在publisher服务的SpringAmqpTest类中添加测试方法:
@Test
public void testTopic(){
rabbitTemplate.convertAndSend(RabbitConfig.TOPIC_EXCHANGE, "china.news","中国加油");
rabbitTemplate.convertAndSend(RabbitConfig.TOPIC_EXCHANGE, "china.today.news","中国今日新闻");
rabbitTemplate.convertAndSend(RabbitConfig.TOPIC_EXCHANGE, "japan.news","日本新闻");
}
消息转换器:
Spring会把发送的消息序列化为字节发送给MQ,接收消息的时候,还会把字节反序列化为Java对象。Spring默认的序列化方法是JDK序列化,数据体积过大。
配置Json转换器,使用JSON方式做序列化和发序列化
在publisher和consumer两个服务中都引入依赖,实际上将以下依赖添加到父工程pom.xml即可
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
<version>2.9.10</version>
</dependency>
配置消息转换器,在启动类中添加一个Bean即可 使用amqp协议下的
@Bean
public MessageConverter jsonMessageConverter(){
return new Jackson2JsonMessageConverter();
}
Kafka
kafka是一个分布式流媒体平台,类似于消息队列或企业消息传递系统。kafka官网:Apache Kafka
名词解释:producer:发布消息的对象称为主题生产者
topic:kafka将消息分门别类,每一类消息称之为一个主题
consumer:订阅消息并处理发布的对象称之为主题消费者
broker:已发布的消息保存在一组服务器中称之为kafka集群。集群中每个服务器都是一个代理Broker。消费者可以订阅一个或多个主题topic,并从Broker拉数据,从而消费这些已发布的消息。
topic: 主题
partition 分区,每个主题至少一个分区(集群:分区数>=broker数量)
分区的好处:高吞吐,高性能,高可用
kafka的安装配置
Kafka对于zookeeper是强依赖,保存kafka相关的节点数据,所以安装Kafka之前必须先安装zookeeper
Docker安装zookeeper 默认2181
#下载镜像
docker pull zookeeper:3.4.14
#创建容器
docker run -d --name zookeeper -p 2181:2181 zookeeper:3.4.14
Docker安装kafka kafka默认9092
#下载镜像
docker pull wurstmeister/kafka:2.12-2.3.1
#创建容器,记着将ip替换
docker run -d --name kafka \
--env KAFKA_ADVERTISED_HOST_NAME=192.168.200.130 \
--env KAFKA_ZOOKEEPER_CONNECT=192.168.200.130:2181 \
--env KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://192.168.200.130:9092 \
--env KAFKA_LISTENERS=PLAINTEXT://0.0.0.0:9092 \
--env KAFKA_HEAP_OPTS="-Xmx256M -Xms256M" \
--net=host wurstmeister/kafka:2.12-2.3.1
先启动zookeeper
docker start zookeeper
再启动kafka
docker start kafka
kafka快速入门
-
生产者发送消息,多个消费者只能有一个消费者接收到消息
-
生产者发送消息,多个消费者都可以接收到消息
创建kafka-demo项目,导入依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- kafkfa -->
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
<exclusions>
<exclusion>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
</dependency>
</dependencies>
在resources下创建文件application.yml
server:
port: 9991
spring:
application:
name: kafka-demo
kafka:
bootstrap-servers: 192.168.200.130:9092
producer:
key-serializer: org.apache.kafka.common.serialization.StringSerializer
value-serializer: org.apache.kafka.common.serialization.StringSerializer
consumer:
group-id: ${spring.application.name}-test
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
生产者代码
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
/**
* 生产者controller
*/
@RestController
public class ProducerController {
@Autowired
private KafkaTemplate kafkaTemplate;
/**
* 生产消息的方法
*/
@GetMapping("/send/{key}/{value}")
public String send(@PathVariable("key") String key,@PathVariable("value") String value){
kafkaTemplate.send("testTopic",key, value);
return "ok";
}
}
消费者代码
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Component;
import java.util.Optional;
/**
* kafka消费者监听器
*/
@Component
public class ConsumerListener {
/**
* 消费消息的方法
* @param consumerRecord
*/
@KafkaListener(topics = "testTopic")
public void receiveMsg(ConsumerRecord<String,String> consumerRecord){
// if(consumerRecord!=null){
// System.out.println(consumerRecord.key() + "==>" + consumerRecord.value());
// }
Optional<ConsumerRecord<String, String>> optional = Optional.ofNullable(consumerRecord);
optional.ifPresent(x->{
System.out.println(x.key() +"==>" + x.value());
});
}
}
测试:
同一个消费组下消费者,只能有一个消费者收到消息(一对一)
不同消费组下消费者,每个组内起码一个消费者能收到消息(一对多,广播效果)
kafka高可用设计
Kafka 的服务器端由被称为 Broker 的服务进程构成,即一个 Kafka 集群由多个 Broker 组成。如果集群中某台机器宕机,其他机器上的Broker仍然能对外提供服务,这就是kafka提供高可用的手段之一。
备份机制Replication
Kafka 中消息的备份又叫做 副本(Replica)
Kafka 定义了两类副本:
-
领导者副本(Leader Replica)
-
追随者副本(Follower Replica)
同步方式
ISR(in-sync replica)需要同步复制保存的follower
如果leader失效后,需要选出新的leader,选举的原则如下:
第一:选举时优先从ISR中选定,因为这个列表中follower的数据是与leader同步的
第二:如果ISR列表中的follower都不行了,就只能从其他follower中选取
极端情况,就是所有副本都失效了,这时有两种方案
第一:等待ISR中的一个活过来,选为Leader,数据可靠,但活过来的时间不确定
第二:选择第一个活过来的Replication,不一定是ISR中的,选为leader,以最快速度恢复可用性,但数据不一定完整
kafka生产者详解
发送类型:
同步发送:使用send()方法发送,它会返回一个Future对象,调用get()方法进行等待,就可以知道消息是否发送成功
RecordMetadata recordMetadata = producer.send(kvProducerRecord).get();
System.out.println(recordMetadata.offset());
异步发送:调用send()方法,并指定一个回调函数,服务器在返回响应时调用函数
//异步消息发送
producer.send(kvProducerRecord, new Callback() {
@Override
public void onCompletion(RecordMetadata recordMetadata, Exception e) {
if(e != null){
System.out.println("记录异常信息到日志表中");
}
System.out.println(recordMetadata.offset());
}
});
参数详解
-
ack
代码的配置方式:
//ack配置 消息确认机制
prop.put(ProducerConfig.ACKS_CONFIG,"all");
retries 重发消息次数
生产者从服务器收到的错误有可能是临时性错误,在这种情况下,retries参数的值决定了生产者可以重发消息的次数,如果达到这个次数,生产者会放弃重试返回错误,默认情况下,生产者会在每次重试之间等待100ms
代码中配置方式:
//重试次数
prop.put(ProducerConfig.RETRIES_CONFIG,10);
消息压缩:默认情况下, 消息发送时不会被压缩。
//数据压缩
prop.put(ProducerConfig.COMPRESSION_TYPE_CONFIG,"lz4");
使用压缩可以降低网络传输开销和存储开销,这往往就是kafka发送消息的瓶颈
kafka消费者详解
消费者组
消费者组(Consumer Group)一个或多个 消费者组成的群体
一个发布在Topic上消息被分发给此消费者组中的一个消费者。如果所有消费者都在一个组中,那么就变成queue模型。所有消费者都在不同组中,就变成了发布-订阅模型
消息的有序性
即时消息中的单对单聊天和群聊,保证发送方消息发送顺序与接收方的顺序一致
充值转账两个渠道在同一个时间进行余额变更,短信通知必须要有顺序
topic分区中消息只能由消费者组中的唯一一个消费者处理,所以消息肯定是按照先后顺序进行处理的。但是它也仅仅是保证Topic的一个分区顺序处理,不能保证跨分区的消息先后处理顺序。 所以,如果你想要顺序的处理Topic的所有消息,那就只提供一个分区。
kafka顺序问题:单机有序,集群默认无序,集群指定分区有序
提交和偏移量
kafka不会像其他JMS队列那样需要得到消费者的确认,消费者可以使用kafka来追踪消息在分区的位置(偏移量offset)
消费者会往一个叫做topic_consumer_offset的特殊主题发送消息,消息里包含了每个分区的偏移量。如果消费者发生崩溃或有新的消费者加入群组,就会触发重均衡
offset segement文件(简称offset文件):追加方式,每行一条消息,offset从0自增1
topic_consumer_offset文件:记录 消费群组消费情况 (groupName,topicName,partition,offset,total )
默认偏移量每5秒提交一次
消息删除
kafka定期检查进行删除