RabbitMQ,RocketMQ,Kafka 事务性,消息丢失和重复发送处理策略

客户 0 20

RabbitMQ,RocketMQ,Kafka 事务性,消息丢失和重复发送处理策略,第1张

RabbitMQ,RocketMQ,Kafka 事务性,消息丢失和重复发送处理策略
导读: 我们的服务器从单机发展到拥有多台机器的分布式系统,各个系统之前需要借助于网络进行通信,原有单机中相对可靠的方法调用以及进程间通信方式已经没有办法使用,同时网络环境也是不稳定的,造成了我们多个机器之间的数据同步问题,这就是典型的分布式事务问

我们的服务器从单机发展到拥有多台机器的分布式系统,各个系统之前需要借助于网络进行通信,原有单机中相对可靠的方法调用以及进程间通信方式已经没有办法使用,同时网络环境也是不稳定的,造成了我们多个机器之间的数据同步问题,这就是典型的分布式事务问题。

在分布式事务中事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。分布式事务就是要保证不同节点之间的数据一致性。

1、2PC(二阶段提交)方案 - 强一致性

2、3PC(三阶段提交)方案

3、TCC (Try-Confirm-Cancel)事务 - 最终一致性

4、Saga事务 - 最终一致性

5、本地消息表 - 最终一致性

6、MQ事务 - 最终一致性

消息的生产方,除了维护自己的业务逻辑之外,同时需要维护一个消息表。这个消息表里面记录的就是需要同步到别的服务的信息,当然这个消息表,每个消息都有一个状态值,来标识这个消息有没有被成功处理。

发送放的业务逻辑以及消息表中数据的插入将在一个事务中完成,这样避免了业务处理成功 + 事务消息发送失败,或业务处理失败 + 事务消息发送成功,这个问题。

举个栗子:

我们假定目前有两个服务,订单服务,购物车服务,用户在购物车中对几个商品进行合并下单,之后需要情况购物车中刚刚已经下单的商品信息。

1、消息的生产方也就是订单服务,完成了自己的逻辑(对商品进行下单操作)然后把这个消息通过 mq 发送到需要进行数据同步的其他服务中,也就是我们栗子中的购物车服务。

2、其他服务(购物车服务)会监听这个队列;

1、如果收到这个消息,并且数据同步执行成功了,当然这也是一个本地事务,就通过 mq 回复消息的生产方(订单服务)消息已经处理了,然后生产方就能标识本次事务已经结束。如果是一个业务上的错误,就回复消息的生产方,需要进行数据回滚了。

2、很久没收到这个消息,这种情况是不会发生的,消息的发送方会有一个定时的任务,会定时重试发送消息表中还没有处理的消息;

3、消息的生产方(订单服务)如果收到消息回执;

1、成功的话就修改本次消息已经处理完,也就是本次分布式事务的同步已经完成;

2、如果消息的结果是执行失败,同时在本地回滚本次事务,标识消息已经处理完成;

3、如果消息丢失,也就是回执消息没有收到,这种情况也不太会发生,消息的发送方(订单服务)会有一个定时的任务,定时重试发送消息表中还没有处理的消息,下游的服务需要做幂等,可能会收到多次重复的消息,如果一个回复消息生产方中的某个回执信息丢失了,后面持续收到生产方的 mq 消息,然后再次回复消息的生产方回执信息,这样总能保证发送者能成功收到回执,消息的生产方在接收回执消息的时候也要做到幂等性。

这里有两个很重要的操作:

1、服务器处理消息需要是幂等的,消息的生产方和接收方都需要做到幂等性;

2、发送放需要添加一个定时器来遍历重推未处理的消息,避免消息丢失,造成的事务执行断裂。

该方案的优缺点

优点:

1、在设计层面上实现了消息数据的可靠性,不依赖消息中间件,弱化了对 mq 特性的依赖。

2、简单,易于实现。

缺点:

主要是需要和业务数据绑定到一起,耦合性比较高,使用相同的数据库,会占用业务数据库的一些资源。

下面分析下几种消息队列对事务的支持

RocketMQ 中的事务,它解决的问题是,确保执行本地事务和发消息这两个操作,要么都成功,要么都失败。并且,RocketMQ 增加了一个事务反查的机制,来尽量提高事务执行的成功率和数据一致性。

主要是两个方面,正常的事务提交和事务消息补偿

正常的事务提交

1、发送消息(half消息),这个 half 消息和普通消息的区别,在事务提交 之前,对于消费者来说,这个消息是不可见的。

2、MQ SERVER写入信息,并且返回响应的结果;

3、根据MQ SERVER响应的结果,决定是否执行本地事务,如果MQ SERVER写入信息成功执行本地事务,否则不执行;

如果MQ SERVER没有收到 Commit 或者 Rollback 的消息,这种情况就需要进行补偿流程了

补偿流程

1、MQ SERVER如果没有收到来自消息发送方的 Commit 或者 Rollback 消息,就会向消息发送端也就是我们的服务器发起一次查询,查询当前消息的状态;

2、消息发送方收到对应的查询请求,查询事务的状态,然后把状态重新推送给MQ SERVER,MQ SERVER就能之后后续的流程了。

相比于本地消息表来处理分布式事务,MQ 事务是把原本应该在本地消息表中处理的逻辑放到了 MQ 中来完成。

Kafka 中的事务解决问题,确保在一个事务中发送的多条信息,要么都成功,要么都失败。也就是保证对多个分区写入操作的原子性。

通过配合 Kafka 的幂等机制来实现 Kafka 的 Exactly Once,满足了读取-处理-写入这种模式的应用程序。当然 Kafka 中的事务主要也是来处理这种模式的。

什么是读取-处理-写入模式呢?

栗如:在流计算中,用 Kafka 作为数据源,并且将计算结果保存到 Kafka 这种场景下,数据从 Kafka 的某个主题中消费,在计算集群中计算,再把计算结果保存在 Kafka 的其他主题中。这个过程中,要保证每条消息只被处理一次,这样才能保证最终结果的成功。Kafka 事务的原子性就保证了,读取和写入的原子性,两者要不一起成功,要不就一起失败回滚。

这里来分析下 Kafka 的事务是如何实现的

它的实现原理和 RocketMQ 的事务是差不多的,都是基于两阶段提交来实现的,在实现上可能更麻烦

先来介绍下事务协调者,为了解决分布式事务问题,Kafka 引入了事务协调者这个角色,负责在服务端协调整个事务。这个协调者并不是一个独立的进程,而是 Broker 进程的一部分,协调者和分区一样通过选举来保证自身的可用性。

Kafka 集群中也有一个特殊的用于记录事务日志的主题,里面记录的都是事务的日志。同时会有多个协调者的存在,每个协调者负责管理和使用事务日志中的几个分区。这样能够并行的执行事务,提高性能。

下面看下具体的流程

事务的提交

1、协调者设置事务的状态为PrepareCommit,写入到事务日志中;

2、协调者在每个分区中写入事务结束的标识,然后客户端就能把之前过滤的未提交的事务消息放行给消费端进行消费了;

事务的回滚

1、协调者设置事务的状态为PrepareAbort,写入到事务日志中;

2、协调者在每个分区中写入事务回滚的标识,然后之前未提交的事务消息就能被丢弃了;

这里引用一下消息队列高手课中的

RabbitMQ 中事务解决的问题是确保生产者的消息到达MQ SERVER,这和其他 MQ 事务还是有点差别的,这里也不展开讨论了。

先来分析下一条消息在 MQ 中流转所经历的阶段。

生产阶段 :生产者产生消息,通过网络发送到 Broker 端。

存储阶段 :Broker 拿到消息,需要进行落盘,如果是集群版的 MQ 还需要同步数据到其他节点。

消费阶段 :消费者在 Broker 端拉数据,通过网络传输到达消费者端。

发生网络丢包、网络故障等这些会导致消息的丢失

在生产者发送消息之前,通过channeltxSelect开启一个事务,接着发送消息, 如果消息投递 server 失败,进行事务回滚channeltxRollback,然后重新发送, 如果 server 收到消息,就提交事务channeltxCommit

不过使用事务性能不好,这是同步操作,一条消息发送之后会使发送端阻塞,以等待RabbitMQ Server的回应,之后才能继续发送下一条消息,生产者生产消息的吞吐量和性能都会大大降低。

使用确认机制,生产者将信道设置成 confirm 确认模式,一旦信道进入 confirm 模式,所有在该信道上面发布的消息都会被指派一个唯一的ID(从1开始),一旦消息被投递到所有匹配的队列之后,RabbitMQ 就会发送一个确认(BasicAck)给生产者(包含消息的唯一 deliveryTag 和 multiple 参数),这就使得生产者知晓消息已经正确到达了目的地了。

multiple 为 true 表示的是批量的消息确认,为 true 的时候,表示小于等于返回的 deliveryTag 的消息 id 都已经确认了,为 false 表示的是消息 id 为返回的 deliveryTag 的消息,已经确认了。

确认机制有三种类型

1、同步确认

2、批量确认

3、异步确认

同步模式的效率很低,因为每一条消息度都需要等待确认好之后,才能处理下一条;

批量确认模式相比同步模式效率是很高,不过有个致命的缺陷,一旦回复确认失败,当前确认批次的消息会全部重新发送,导致消息重复发送;

异步模式就是个很好的选择了,不会有同步模式的阻塞问题,同时效率也很高,是个不错的选择。

Kafaka 中引入了一个 broker。 broker 会对生产者和消费者进行消息的确认,生产者发送消息到 broker,如果没有收到 broker 的确认就可以选择继续发送。

只要 Producer 收到了 Broker 的确认响应,就可以保证消息在生产阶段不会丢失。有些消息队列在长时间没收到发送确认响应后,会自动重试,如果重试再失败,就会以返回值或者异常的方式告知用户。

只要正确处理 Broker 的确认响应,就可以避免消息的丢失。

RocketMQ 提供了3种发送消息方式,分别是:

同步发送:Producer 向 broker 发送消息,阻塞当前线程等待 broker 响应 发送结果。

异步发送:Producer 首先构建一个向 broker 发送消息的任务,把该任务提交给线程池,等执行完该任务时,回调用户自定义的回调函数,执行处理结果。

Oneway发送:Oneway 方式只负责发送请求,不等待应答,Producer 只负责把请求发出去,而不处理响应结果。

在存储阶段正常情况下,只要 Broker 在正常运行,就不会出现丢失消息的问题,但是如果 Broker 出现了故障,比如进程死掉了或者服务器宕机了,还是可能会丢失消息的。

防止在存储阶段消息额丢失,可以做持久化,防止异常情况(重启,关闭,宕机)。。。

RabbitMQ 持久化中有三部分:

消息的持久化,在投递时指定 delivery_mode=2(1是非持久化),消息的持久化,需要配合队列的持久,只设置消息的持久化,重启之后队列消失,继而消息也会丢失。所以如果只设置消息持久化而不设置队列的持久化意义不大。

对于持久化,如果所有的消息都设置持久化,会影响写入的性能,所以可以选择对可靠性要求比较高的消息进行持久化处理。

不过消息持久化并不能百分之百避免消息的丢失

比如数据在落盘的过程中宕机了,消息还没及时同步到内存中,这也是会丢数据的,这种问题可以通过引入镜像队列来解决。

镜像队列的作用:引入镜像队列,可已将队列镜像到集群中的其他 Broker 节点之上,如果集群中的一个节点失效了,队列能够自动切换到镜像中的另一个节点上来保证服务的可用性。(更细节的这里不展开讨论了)

操作系统本身有一层缓存,叫做 Page Cache,当往磁盘文件写入的时候,系统会先将数据流写入缓存中。

Kafka 收到消息后也会先存储在也缓存中(Page Cache)中,之后由操作系统根据自己的策略进行刷盘或者通过 fsync 命令强制刷盘。如果系统挂掉,在 PageCache 中的数据就会丢失。也就是对应的 Broker 中的数据就会丢失了。

处理思路

1、控制竞选分区 leader 的 Broker。如果一个 Broker 落后原先的 Leader 太多,那么它一旦成为新的 Leader,必然会造成消息的丢失。

2、控制消息能够被写入到多个副本中才能提交,这样避免上面的问题1。

1、将刷盘方式改成同步刷盘;

2、对于多个节点的 Broker,需要将 Broker 集群配置成:至少将消息发送到 2 个以上的节点,再给客户端回复发送确认响应。这样当某个 Broker 宕机时,其他的 Broker 可以替代宕机的 Broker,也不会发生消息丢失。

消费阶段就很简单了,如果在网络传输中丢失,这个消息之后还会持续的推送给消费者,在消费阶段我们只需要控制在业务逻辑处理完成之后再去进行消费确认就行了。

总结:对于消息的丢失,也可以借助于本地消息表的思路,消息产生的时候进行消息的落盘,长时间未处理的消息,使用定时重推到队列中。

消息在 MQ 中的传递,大致可以归类为下面三种:

1、At most once: 至多一次。消息在传递时,最多会被送达一次。是不安全的,可能会丢数据。

2、At least once: 至少一次。消息在传递时,至少会被送达一次。也就是说,不允许丢消息,但是允许有少量重复消息出现。

3、Exactly once:恰好一次。消息在传递时,只会被送达一次,不允许丢失也不允许重复,这个是最高的等级。

大部分消息队列满足的都是At least once,也就是可以允许重复的消息出现。

我们消费者需要满足幂等性,通常有下面几种处理方案

1、利用数据库的唯一性

根据业务情况,选定业务中能够判定唯一的值作为数据库的唯一键,新建一个流水表,然后执行业务操作和流水表数据的插入放在同一事务中,如果流水表数据已经存在,那么就执行失败,借此保证幂等性。也可先查询流水表的数据,没有数据然后执行业务,插入流水表数据。不过需要注意,数据库读写延迟的情况。

2、数据库的更新增加前置条件

3、给消息带上唯一ID

每条消息加上唯一ID,利用方法1中通过增加流水表,借助数据库的唯一性来处理重复消息的消费。

生产者将消息投递到交换器,然后交换器再将消息路由到一个或者多个队列中。

RabbitMQ 定义了4种类型的交换器:

在使用交换器之前,需要先创建交换器,RabbitMQ 的 Java 客户端提供了 exchangeDeclare() 方法来声明交换器。

参数说明:

返回值:

方法重载:

有创建就会有删除,RabbitMQ 的 Java 客户端提供了 exchangeDelete() 方法来删除交换器。

参数说明:

返回值:

exchangeDeclarePassive() 方法用来检测交换器是否存在。如果存在,则正常返回;如果不存在,则抛出异常 404 channel exception

队列在 RabbitMQ 中用来存储消息,队列通过 BindingKey 与 交换器相互绑定。

与 exchangeDeclare() 方法相比, queueDeclare() 的重载方法少很多,只有两个重载方法:

参数说明:

返回值:

与交换器一样,队列也可以删除

参数说明:

返回值:

are一个队列,置AMQP_PASSIVE标志位,

就不会影响服务端状态,并返回消息计数。 $conn = new AMQPConnection(); // $queue = new AMQPQueue($conn); $queue->setFlags(AMQP_PASSIVE); $messageCount = $queue->declare($queueName);

1、安装 在Mac下安装RabbitMQ是非常简单的,一般默认RabbitMQ服务器依赖的Erlang已经安装,只需要用下面两个命令就可以完成RabbitMQ的安装(前提是homebrew已经被安装): brew update brew install rabbitmq 安装完成后需要将/usr/local/sbin

下面是RabbitMQ的消息确认机制:“为了确保消息不会丢失,RabbitMQ支持消息确认机制。客户端在接受到消息并处理完后,可以发送一个ack消息给RabbitMQ,告诉它该消息可以安全的删除了。假如客户端在发送ack之前意外死掉了,那么RabbitMQ会将消息投递到下一个consumer客户端。如果有多个consumer客户端,RabbitMQ在投递消息时是轮询的。RabbitMQ如何判断客户端死掉了?唯一根据是客户端连接是否断开。这里没有超时机制,也就是说客户端可以处理一个消息很长时间,只要没断开连接,RabbitMQ就一直等待ack消息。”我现在遇到的问题是这样的:我这边有几条线程去消息队列里取数据,但是会有异常数据导致线程挂掉,就是上边的“客户端在发送ack之前意外死掉了”,RabbitMQ会将消息投递到下一个consumer客户端,这样一条异常数据会把我的所有线程挂掉,我现在想实现这样的功能:如果有异常数据导致进程挂掉,那么我不让RabbitMQ将这条消息投递到下一个consumer客户端,而是放到另一个地方或者另外处理,请问该如何实现呢?

在使用RabbitMQ时,默认预取消息队列中的全部数据缓存在本地,导致只有一个进程能获取资源,而其他进程处于闲置状态。而持有资源的进程消费能力有限时,会导致消息队列积压。

消息的轮询分配机制和尽可能快速推送消息的机制给实际使用带来困难。实际情况下,每个消费者处理消息的能力、每个消息处理所需时间可能都是不同的,若只是机械化地顺次分配,可能造成一个消费者由于处理的消息的业务复杂、处理能力低而积压消息,另一个消费者早早处理完所有的消息,处于空闲状态,造成系统的处理能力的浪费。且无法加入新的消费者以提高系统的处理能力。

希望达到的效果是: 每个消费者都根据自身处理能力合理分配消息处理任务,既无挤压也无空闲,新加入的消费者也能分担消息处理任务,使系统的处理能力能够平行扩展

RabbitMQ 客户端可通过 Channel 类的 basicQos(int prefetchCount) 设置消费者的预取数目,即消费者最大的未确认消息的数目。

假设 prefetchCount=10 ,有两个消费者,两个消费者依次从队列中抓取10条消息缓存本地,若此时有新的消息到达队列,先判断信道中未确认的消息是否大于或等于20条,若是,则不向信道中投递消息,当信道中未确认消息数小于20条后,信道中哪个消费者未确认消息小于10条,就将消息投递给哪个消费者。

channelbasicQos() 中设置的预取数量多少合适,是一个颇有讲究的问题。我们希望充分利用消费者的处理能力,因此不宜设置过小,否则在消费者处理消息后, RabbitMQ 收到确认消息后才会投递新的消息,导致此期间消费者处于空闲状态,浪费消费者的处理能力;但设置过大,又可能使消息积压在消费者的缓存里,我们希望对于来不及处理的消息,应保留在队列中,便于加入新的消费者或空闲出来的消费者分担消息处理任务。

   RabbitMQ 官网的一篇文章详细讨论了预取数量的设置问题:

   https://wwwrabbitmqcom/blog/2012/05/11/some-queuing-theory-throughput-latency-and-bandwidth/

假设从 RabbitMQ 服务端队列取消息、传输消息到消费者耗时为50ms,消费者消费消息耗时4ms,消费者传输确认消息到服务端耗时为50ms。若网络状况、消费者处理速度稳定,则预取数量的最优数值为:(50 + 4 + 50)/4=26个。

最初服务端将向客户端发送 26 条消息,并缓存在客户端本地,当消费者处理好第一个消息后,向服务端发送确认消息并取本地缓存的第二个消息,确认消息由客户端传送到服务端耗时 50ms ,服务端收到确认后发送新的消息经过 50ms 又到达了客户端,而余下的 25 个消息被消费耗时为 25×4=100ms ,所以当新的消息达到时,第一轮的 26 个消息恰好全部处理完。依次类推,之后,每当处理完一个旧有的消息时,恰好会到达一个新的消息。既不会发生消息积压,消费者也不会空闲。

  但实际情况是,网络的传输状况、消费者处理消息的速度都不会是恒定的,会时快时慢,造成消息积压或消费者空闲,这就要求预取数量要与网络和消费者的状况实时改变。

说白了,预取数据就是为了控制消费者在获取/发送数据与业务逻辑之间能够更好的衔接,避免某个消息积压或过于空闲的情况出现。

向/etc/yumreposd/下添加rabbitmqrepo文件

yum update -y

yum install -y make gcc gcc-c++ m4 openssl openssl-devel ncurses-devel unixODBC unixODBC-devel java java-devel

yum install socat logrotate -y

yum install erlang rabbitmq-server -y

systemctl start rabbitmq-server

RabbitMQ启动出错:- unable to connect to epmd on xxxx: timeout (timed out)

因为本机主机名到IP地址的解析对应不起来;

192168100251 test-web2

保存退出,再次启动

设置RabbitMQ开机自启动:

systemctl enable rabbitmq-server

RabbitMQ其他操作:

rabbitmqctl status

rabbitmqctl stop

systemctl restart rabbitmq-server

/etc/rabbitmq/rabbitmqconf

rabbitmqctl : rabbitmq的客户端,用来连接管理rabbitmq;

rabbitmq-env :配置环境变量的管理者;

rabbitmq-plugins:rabbitmq后台插件管理;

rabbitmq-server: rabbitmq守护进程;

查看状态信息

1启用网页版后台管理插件:

rabbitmq-plugins enable rabbitmq_management

2新建一个用户名为admin,密码为admin的管理员,并授予管理员(administrator)权限。

rabbitmqctl add_user admin admin

rabbitmqctl set_user_tags admin administrator

3设置admin可以使用的虚机权限。

添加 admin 虚拟机

rabbitmqctl add_vhost admin

设置admin可以使用的虚机admin权限

rabbitmqctl set_permissions -p admin admin " " " " " "

31用户管理

查看所有用户

rabbitmqctl list_users

添加一个用户

rabbitmqctl add_user zhaobl 123456

配置权限

rabbitmqctl set_permissions -p "/" zhaobl " " " " " "

查看用户权限

rabbitmqctl list_user_permissions zhaobl

设置tag

rabbitmqctl set_user_tags zhaobl administrator

删除用户(安全起见,删除默认用户)

rabbitmqctl delete_user guest

4插件管理:

插件列表:

rabbitmq-plugins list

启动插件:

rabbitmq-plugins enable XXX (XXX为插件名)

停用插件:

rabbitmq-plugins disable XXX

5打开后台web管理界面: http://localhost:15672/ ,刚才我们设置了admin用户,所以可以使用admin登录,也可以使用默认账号和密码都是guest登录。进入管理界面后可以看到:

7其他配置

我们可以到官网地址: https://wwwrabbitmqcom/configurehtml ,了解RabbitMQ的性能优化方面的配置。

使用Docker安装RabbitMQ

首先,我们得安装docker环境,docker环境的安装本站后面会有文章介绍,本文假设你的机器上已经安装好了docker环境。

先拉取RabbitMQ镜像:

docker pull rabbitmq:381-management

然后查看镜像:

docker images

REPOSITORY TAG IMAGE ID CREATED SIZE

rabbitmq 381-management 36ed80b6a1b1 5 weeks ago 180MB

然后运行容器:

docker run --name rabbitmq -d -p 5672:5672 -p 15672:15672 -v /data:/var/lib/rabbitmq rabbitmq:381-management

最后,使用docker ps查看运行的容器。

这样,一个RabbitMQ的docker环境就装好了。

RabbitMQ技术入门与实战

https://blogcsdnnet/super_rd/category_9268807html