虽然幂等的相关文章非常多,不过我们还是从 rabbitmq 的场景来谈谈幂等。
但是幂等在任何新增场景下都可能会被要求实现。比如 mysql 中,一条插入请求的实现,要求插入10次该记录和插入1次的效果相同,那么典型的就是使用唯一索引来约束。那么对于 update 请求来说,相同的参数多次 update ,结果肯定是一样的。但是以不同的参数多次去 update 同一条记录,就可能造成最终结果的不一致。
所以 新增 的语义,更偏向于 去重,最终实现幂等;而 更新 的语义,更偏向于 并发,或者说 排序,可以从之前的 偏序与全序 来理解,主要是通过决定 n 个并发操作的先后顺序,从而实现多次更新结果的一致性。
那么,回到消息队列中,我们常说的幂等性主要指的就是消息从生产到最终消费的幂等。为了维持 Broker 以及 生产过程的简单性,幂等进一步指的是消费者端的幂等实现。
消费者端的幂等实现,逻辑的起点是:如何判断重复消息或者重复数据?
- 如果消息承载的业务信息本身就有能够唯一标识的字段,并且正好适用于消费场景,那么就可以直接用该字段来区分重复消息;
- 否则,自然要对每个消息创建能够唯一标识的字段,比如通过 snowflake 算法来生成唯一ID
当能够区分消息是否重复之后,就要利用工具来去重。比如利用数据库的唯一索引约束来去重。考虑到性能问题,也可以使用 redis 来做集中式缓存,利用其自身提供的能力去实现消息是否已经处理过的判断。
现在,我们不禁要问,为什么消费者会收到重复的消息呢?
- 生产者重发:队列开启生产确认机制,生产者未收到ack,则进行重发
- 消息队列重新投递:消费者收到消息后,返回的ack丢失,则消息队列认为该消息没有执行成功。
其中,一个队列中的消息没有投递成功,则仍然会保存在队列中,所以会阻塞队列中的其他消息。但是,每条消息都有一个 TTL 值,表示该条消息在队列中的最大存活时间。
如果消息超过TTL值,则会被丢弃。除非为该队列配置了 死信队列
。死信队列,顾名思义,就是那些超过 TTL 值的消息,将会去往的队列,如下图:
那么消息进入死信队列之后怎么办呢?当然是和普通队列一样,需要由下游的消费者来处理。比如消费者收到死信队列中的消息后,就可以进入短信告警等补偿流程了。
所以,rabbitmq 通过死信队列,就解决了业务队列中消息被阻塞的情况。
现在,rabbitmq 中队列可以正常处理了,那么决定整体生产-消费过程的性能是什么呢?
首先来看生产端,性能的源头当然是取决于生产者的发送速度。生产者想要提速,最简单的就是多线程并行发送。考虑到生产者是无状态服务,而不是我们之前研究的有状态服务,所以可以进一步地增加生产者数量。也可以根据消息实时性要求,让生产者进行批量发送。
然后再来看 Broker 端,Broker 端实际上不会非常影响整体性能,因为对性能影响最大的自然是消费者端。但是 Broker 端的一些配置是会影响消息的写入速度。比如我们已经学过的可以控制消息是否持久化到磁盘后,再返回生产确认;再比如对于 Quorum 队列,成功写入到大多数队列中,才会返回生产确认。
关于 Broker 端自身高性能的实现方案,我们会在 kafka 中进行讲述,因为 kafka 是性能优化做到极致的,但是也带了其他的影响。
不过,rabbitmq 可以动态的根据 Broker 的负载情况,来决定消息在磁盘和内存之间的存储转换,以平衡性能和负载。
非持久化的消息也有可能会被置换到磁盘中,这就类似 Linux 系统中的 swap 分区的工作机制一样。每个 Broker 都有一个专门管理消息存储的组件,Broker 上所有的队列共享此组件。当 Broker 所在机器内存不足时,该组件就会进行置换。置换过程中就可能将内存中的非持久化消息存储到磁盘。
对于持久化消息来说,也可能会存在于内存中。这非常类似 innodb 的 buffer pool
即BP,磁盘中的数据会被加载到BP中,这样更快读取。而且就算只读取一条记录,由于 innodb 存储的管理单位是Page,那么也会将该 Page 读入 BP,所以当再次访问相邻数据时,会直接从 BP 中返回。所以 BP 是 innodb 实现高性能的非常重要的组件。
那么回过头来,消息在持久化到磁盘后,假如内存充足,可以预先加载到内存中。因为队列消息和数据行记录是非常相似的,都是顺序存储,所以从磁盘中也不是读取某一条消息,而是一次读取出来一整个数据页到内存,这样可以充分利用内存的速度优势。
说到缓存,不得不提一嘴 redis,现在我们学习缓存,上手就是 redis 原理。但是缓存无处不在,从最底层的 CPU cache,到操作系统 cache,再到我们学过的 BP、消息队列等,都是缓存。学习过程中一定要注意对比与联系。
当我们的linux服务器内存快要爆炸时,我们会发现根本无法远程登录上去,过一会登录上去,可能会发现有的软件被kill了。那么,当 Broker 所接受的压力太大时,导致内存占用飙升,则 Broker 很可能被系统kill,引发不可用。如同微服务中常听说的熔断限流一样,rabbitmq 同样提供了限流机制,会限制生产者的写入速度。
以上关于 rabbitmq 分析就先到这一步,作为引子引出下面要研究的 rocketmq 和 kafka。对他们的学习,我们同样是结合之前的知识点去做对比联系,理解对应的设计思路,也就是理清相关的 底层逻辑。