1、使用同步的通讯方式来解决多个服务之间的通讯
2、使用异步的通讯方式
Ø 消息(Message)是指应用于应用之间传送的数据,消息的类型包括文本字符串、JSON、XML、内嵌对象等等...
Ø 所谓消息中间件/消息队列(Message Queue Middleware,简称MQ)是利用高效可靠的消息传递机制进行数据交流,同时可以基于数据通信来进行分布式系统的继承,消息中间件一般有两种传递模式:点对点(Point-to-Point)模式和发布/订阅(Pub/Sub)模式,点对点模式是基于队列的,消息生产者发送消息到队列,消息消费者从队列中接收消息,队列的存在使得消息的异步传输成为了可能,发布订阅模式定义了如何向一个内容节点发布和订阅内容,这个内容节点叫topic,这种模式可以满足消费者发布一个消息,多个消费者同时消费同一信息的需求。
Ø 一般来说,消息队列是一种异步的服务间通信方式,是分布式系统中重要的组件,主要解决应用耦合,异步消息,流量削锋等问题,实现高性能,高可用,可伸缩和最终一致性架构。使用较多的消息队列有ActiveMQ、RocketMQ、RabbitMQ、Kafka等。
一、多线程和消息队列的区别?
① 多线程是防止系统的阻塞(优先响应用户,后台任务执行)
② 消息队列是提高系统处理业务的效率(异步处理加快程序执行速度)
二、消息队列和多线程的选择
可靠性要求高时选择消息队列:消息队列和多线程两者并不冲突,多线程可以作为队列的生产者和消费者。使用外部的消息队列时,第一是可以提高应用的稳定性,当程序fail后,已经写入外部消息队列的数据依旧是保存的,如果使用两步commit的队列的话,可以更加提高这个项目。
不着急知道结果,尽量使用消息队列,保证服务器的压力减小,因为多线程对cpu的消耗大一点:用线程的话,会占用主服务器资源, 消息队列的话, 可以放到其他机器上运行, 让主服务器尽量多的服务其他请求。我个人认为, 如果用户不急着知道结果的操作, 用消息队列, 否则再考虑用不用线程。
需要解耦的时候用消息队列:解耦更充分,架构更合理。多线程是在编程语言层面解决问题,消息队列是在架构层面解决问题。我认为架构层面解决问题是“觉悟比较高的方式“,理想情况下应该限制语言层面滥用多线程,能不用就不用。
如果容易出现线程安全问题的业务或者批量操作时,也尽量使用消息队列:批量发送邮件时,数据量庞大,如果使用多线程对系统不安全。
三、消息队列和线程池的比较
1) 两者内部都使用了队列,如阻塞队列、优先级队列;
2) 使用线程池时应用服务器既充当生产者又充当消费者,也是消息队列中间件的实现者,使用消息队列时中间件、生产者、消费者可以部署在不同的应用机器上(当然也可以部署在一台服务器上但很少有人这么用);
3) 出于第2点线程池更适合非分布式的系统,但在分布式架构下消息队列明显是更突出优势;
4) 使用消息队列会带来额外的网络开销;
5) 消息队列的耦合性更低,可扩展性更好,适用于弱一致性的场景,如对log日志的解耦;
6) 消息队列自动实现消息的持久化,中间已经实现了大量功能,如消息转发、消息拒绝、消息重试,以及对消息的一些监控,例如消息的消费状态、消息的消费速率等,使用线程池如果需要很多功能还要自己去实现,例如需要执行状态需要打印队列数量、计算消息消费速度;
7) 在不同系统间的服务调用(调用协议也可能不一致)线程池很难实现或开销很大,这时候消息队列可以屏蔽不同机器或不同协议的问题;
8) 使用消息队列会提升系统的复杂度,网络抖动怎么办?最大队列长度怎么设置?超时时间又设置多少?Qos又设置为多少?消费者多少个比较合适?Channel cache size又该设置为多少?业务线可能都是用同一个Mq,你占资源太多,或者设计不当可能会导致整个Mq故障
四、消息队列的主要作用
① 解耦
如果采用推送的方式,A 系统通过接口调用发送数据到 B、C、D 三个系统,A 系统的维护成本就非常的高,而且 A 系统要时时刻刻考虑B、C、D 四个系统如果出现故障该怎么办?使用消息队列就可以解决这个问题。A 系统只负责生产数据,不需要考虑消息被哪个系统来消费。
② 异步
A 系统需要发送个请求给 B 系统处理,由于 B 系统需要查询数据库花费时间较长,以至于 A 系统要等待 B 系统处理完毕后再发送下个请求,造成 A 系统资源浪费。使用消息队列后,A 系统生产完消息后直接丢进消息队列,不用等待 B 系统的结果,直接继续去干自己的事情了。
③ 削峰
A 系统调用 B 系统处理数据,每天 0 点到 12 点,A 系统风平浪静,每秒并发请求数量就 100 个。结果每次一到 12 点 ~ 13 点,每秒并发请求数量突然会暴增到 1 万条。但是 B 系统最大的处理能力就只能是每秒钟处理 1000 个请求,这样系统很容易就会崩掉。这种情况可以引入消息队列,把请求数据先存入消息队列中,消费系统再根据自己的消费能力拉取消费。
五、消息队列的局限性
首先,消息队列会降低系统的可用性,因为引入消息队列,就等于引入多一种外部依赖,多一种外部依赖,挂掉的可能性就多一点;
其次,消息队列会使得系统复杂度提高,比如:需要保证消息没有被重复消费、处理消息丢失的情况、保证消息传递的顺序性等等问题;
最后,使用消息队列,要考虑一致性问题:A 系统处理完了直接返回成功了,但问题是:要是 B、C、D 三个系统那里,B 和 D 两个系统写库成功了,结果 C 系统写库失败了,就造成数据不一致了。
六、kafka消息队列的高可用
Kafka 使用的是 partition和 replica 模式来保证高可用
① Partition:
partition(分区)是kafka的一个核心概念,kafka将1个topic分成了一个或多个分区,每个分区在物理上对应一个目录,分区目录下存储的是该分区的日志段(segment),包括日志的数据文件和两个索引文件。然后每个分区又对应一个或多个副本,由一个ISR列表来维护。 注意:分区数可以大于节点数,但是副本数不能大于节点数,因为副本需要分不到不同的节点上,才能达到备份的目的。
② Replica:
Kafka 是有主题概念的,而每个主题又进一步划分成若干个分区。副本的概念实际上是在分区层级下定义的,每个分区配置有若干个副本。
所谓副本(Replica),本质就是一个只能追加写消息的提交日志。根据 Kafka 副本机制的定义,同一个分区下的所有副本保存有相同的消息序列,这些副本分散保存在不同的 Broker 上,从而能够对抗部分 Broker 宕机带来的数据不可用。
在实际生产环境中,每台 Broker 都可能保存有各个主题下不同分区的不同副本,因此,单个 Broker 上存有成百上千个副本的现象是非常正常的。接下来我们来看一张图,它展示的是一个有 3 台 Broker 的 Kafka 集群上的副本分布情况。
从这张图中,我们可以看到,主题 1 分区 0 的 3 个副本分散在 3 台 Broker 上,其他主题分区的副本也都散落在不同的 Broker 上,从而实现数据冗余。
七、如何保证消息不被重复消费、幂等性
可以在写数据时,先根据主键查一下这条数据是否存在,如果已经存在则 update;
数据库的唯一键约束也可以保证不会重复插入多条,因为重复插入多条只会报错,不会导致数据库中出现脏数据;如果是写 Redis,就没有问题,因为 set 操作是天然幂等性的
八、保证消息不丢失
在生产阶段,你需要捕获消息发送的错误,并重发消息。在存储阶段,你可以通过配置刷盘和复制相关的参数,让消息写入到多个副本的磁盘上,来确保消息不会因为某个 Broker 宕机或者磁盘损坏而丢失。在消费阶段,你需要在处理完全部消费业务逻辑之后,再发送消费确认。
在 Producer 端,我们给每个发出的消息附加一个连续递增的序号,然后在 Consumer 端来检查这个序号的连续性。如果没有消息丢失,Consumer 收到消息的序号必然是连续递增的,或者说收到的消息,其中的序号必然是上一条消息的序号 +1。如果检测到序号不连续,那就是丢消息了。还可以通过缺失的序号来确定丢失的是哪条消息,方便进一步排查原因。大多数消息队列的客户端都支持拦截器机制,你可以利用这个拦截器机制,在 Producer 发送消息之前的拦截器中将序号注入到消息中,在 Consumer 收到消息的拦截器中检测序号的连续性,这样实现的好处是消息检测的代码不会侵入到你的业务代码中,待你的系统稳定后,也方便将这部分检测的逻辑关闭或者删除。
九、处理消息积压
能导致积压突然增加,最粗粒度的原因,只有两种:要么是发送变快了,要么是消费变慢了。大部分消息队列都内置了监控的功能,只要通过监控数据,很容易确定是哪种原因。如果是单位时间发送的消息增多,比如说是赶上大促或者抢购,短时间内不太可能优化消费端的代码来提升消费性能,唯一的方法是通过扩容消费端的实例数来提升总体的消费能力。如果短时间内没有足够的服务器资源进行扩容,没办法的办法是,将系统降级,通过关闭一些不重要的业务,减少发送方发送的数据量,最低限度让系统还能正常运转,服务一些重要业务。还有一种不太常见的情况,你通过监控发现,无论是发送消息的速度还是消费消息的速度和原来都没什么变化,这时候你需要检查一下你的消费端,是不是消费失败导致的一条消息反复消费这种情况比较多,这种情况也会拖慢整个系统的消费速度。如果监控到消费变慢了,你需要检查你的消费实例,分析一下是什么原因导致消费变慢。优先检查一下日志是否有大量的消费错误,如果没有错误的话,可以通过打印堆栈信息,看一下你的消费线程是不是卡在什么地方不动了,比如触发了死锁或者卡在等待某些资源上了。
十、消息过期
我们可以采取一个方案,就是批量重导。就是大量积压的时候,直接丢弃数据了,然后等过了高峰期以后开始写程序,将丢失的那批数据一点一点的查出来,然后重新灌入 MQ 里面去,把丢的数据给补回来。