消息队列
一、 MQ有什么用?有哪些具体的使用场景?
1.Message Queue消息队列,是一种FIFO的数据结构。消息由生产者发送到MQ进行排队,然后由消费者对信息进行处理。QQ,微信。
2.MQ有三个作用:
异步:让消息的发送和接收可以异步进行,生产者不必一直等消费者,此时MQ相当于一个中转站。提高系统响应速度和吞吐量。
解耦:可以减少服务之间的影响,提高系统稳定性和独立性(不同服务分来独立进行)。另外还可以实现数据分发,生产者发送一条消息,可以由多个消费者处理。
削峰:可以应对生产者的流量冲击(类似于流量控制),MQ类似于缓存,可以进行高并发量下的流量削峰。
MQ也具有如下缺点:
系统可用性降低:MQ要保证具有高可用性,否则如果MQ当机,则整个系统都会崩溃用不了。
数据链路复杂:本来A只需要直接发消息给C,这时候加入一个中间站B,让A先发给B再发给C,可能出现消息丢失,消息的转发顺序改变,消息重复调用等等问题。
数据一致性问题:A发送消息需要B,C共同处理,如果B处理成功,而C处理失败,则会造成数据一致性问题。或者是多个消费者要处理修改同一条消息时,也可能会造成一致性问题。
二、MQ进行产品选型?
1.kafka:优点是吞吐量大,性能非常好。缺点是会丢数据,功能单一。
使用场景:日志分析,或者是大数据采集,允许有一小些数据丢失的情况,订单类型的就不适合使用Kafka。
2.RabbitMQ:优点是可靠性高,提供多种功能队列。缺点吞吐量低,消息堆积会影响性能,消息一来立马处理。
使用场景:小规模场景。
3.RocketMQ:优点高吞吐,高可用,功能全面。缺点是开源版功能不如云上版本,客户端支持JAVA(不能解耦)。
三、如何保证消息不丢失?
1.生产者发送消息时会丢失。
解决方法: ①kafka中生产者会注册一个回调函数,当MQ收到消息后,会发送一个请求去调用回调函数,来告诉生产者已经收到消息,如果生产者发现回调函数没被调用,则重发消息。
②RocketMQ当中还提供事务消息机制,保证本地事务和消息发送原子性一致性,同时失败或成功。过程如下:
首先生产者会先发送一个半消息(对于消费者不可见),目的在于确认MQ服务正常,然后MQ会发送半消息响应。生产者收到半消息响应后(说明MQ正常),然后执行本地事务(往数据库插入消息) 。
执行完后生产者就会往MQ发送真正的事务消息,并且附带本地事务的状态(包括成功,失败,未知),成功就会推送给消费者进行下一步事务处理,失败MQ就不发了。
而如果MQ收到的状态是未知的,表明本地事务可能还没有执行完,那么就MQ就会反复轮询本地事务进行回查,看他完成没有, 生产者就会检查本地数据库mysql是否插入成功。然后发送事务状态给MQ重复上述过程。最多回查15次,如果还是未知就说明本地事务超时就丢弃。
消息事务的应用场景很多,比如先完成在支付系统支付后才能发送MQ进行手续费处理或者清空购物车。假如要设计一个5min内付款功能,因为只要生产者第一次发送事务状态是未知的,那么就可以触发整个回查机制,所以我们只要保证5min内回查15次支付系统看是否支付即可。注意,消息事务仅保证了分布式事务一半的原子性。
2.MQ中消息主从同步时会丢失。
RocetkMQ:①普通集群中,Master和Slave主从模式配合进行消息主从复制,达到高可用性。消息同步方式分为同步和异步。
同步表示生产者往MQ发起消息时,Master立即往Slave消息同步存盘,存盘完后才向生产者响应。好处是一定能够保证消息同步保存,但是会产生阻塞。
异步表示生产者发送消息后,Master立即返回响应生产者,然后才向Slave存盘,好处是效率更高,但是可能有丢消息风险。
kafka:通常使用在允许消息少量丢失场景,丢失不可完全避免。
3.MQ时基于内存工作的,因此保存消息到硬盘时可能会丢失。
Rocket MQ:同步刷盘和异步刷盘。同步刷盘当消息写入内存后,通知线程立即刷盘,写入磁盘完成后才向应用返回写成功信息,消息安全性高,效率低;异步则是只有当内存中的消息积累到一定程度,才统一触发写磁盘。
4.MQ发送给消费者时可能会丢失。
队列当中有一个offset偏移量,标记消费者消费到哪个消息。
消费者如果是同步机制,执行完事务后才返回offset,则不会造成消息丢失;而如果进行异步机制,消费者收到消息立马提交offset给MQ,此时MQ中的offset就会往后偏移,消费者再处理事务,那么如果执行本地事务写数据库失败,下一次消息事务处理就是另外一条,先前失败的订单则会丢失。 因此消费者端一定要采用同步机制。
整个消息丢失情况如下:
四、如何保证信息消费幂等性?
由于网络等因素,使得MQ发送重复消息给消费者消费的问题。所有MQ产品并没有提供主动解决幂等性机制,需要消费者自己控制。数据库查询,删除操作是天然的幂等操作。
1.通过乐观锁,给每一个业务数据增加一个版本号属性,每次本地事务完成更新后版本号+1,当消息数据版本号和当前数据库的版本号不一致,则拒绝更新。
2.通过一个全局的带有业务标识的OrderID进行判断, 比如订单编号,消费者获得消息后会先验证该id是否已经被消费,如果没有则进行消息处理后,把id存入redis设置为已消费。
五、如何保证消息的顺序?
1.MQ只需要保证局部有序(对于一个业务而言),不需要全局有序。 如果只是有一个队列的话,队列天生就是可以保证FIFO有序的。但是在分布式场景下,会有多个MQ队列,生产者发送的消息会考虑充分利用MQ资源,并不会都在同一个队列中,消费者取出消息时也是分布式的读取处理消息。因此不能保证单个业务消息有序。
2.RocketMQ在生产者和MQ间有一个Message selector,它保证一组有序消息一定是在同一个队列中的。而对于消费者来说,它一次仅消费整个队列的消息(如果是分布式接收处理消息事务,则按序把消息放在同一个队列并锁住,消费完这个队列再消费下一个 )。RocketMQ中有完整的设计。
六、如何保证消息的高效读写?
传统的文件发送过程首先通过read()调用进行上下文切换(用户态->核心态),把磁盘文件读取到内核空间中缓冲区,因为JAVA程序运行用户空间,因此需要从内核空间缓冲区再拷贝到用户空间内存缓冲区中,并返回read()调用(又引发一次上下文切换,内核态->用户态),这时候应用程序才能操作文件数据;然后调用send()上下文切换,用户空间拷贝到内核空间的Socket 缓冲区(与目标套接字相关联);最后send()返回调用,通过DMA把数据从内存拉到网卡缓冲区。
其中第二次和第三次是比较耗费资源的,因为需要内核态和用户态之间来回切换并且CPU拷贝开销大。而第一次和第四次是通过DMA拷贝,外设直接与系统内存交换数据,不需要通过CPU。传统的文件发送过程如下:
kafka和RocketMQ背后都是通过零拷贝来优化文件读写。
1.Mmap。应用进程系统调用mmap()并返回后,拿到文件的映射,用户空间和内核态缓冲区共享一个缓冲区,然后再依次拷贝到-Socket缓冲区->NIC 缓冲区。好处是可以直接在用户空间对内存中的文件进行操作,缺点在于建立映射开销大。RocketMQ通过Mmap写日志文件。
2.sendfile+DMA 。如果文件不需要操作内容,可以直接从内核空间读写缓冲区拷贝到Socket缓冲区,JAVA通过transferTo()方法可以实现。第二步,读写缓冲区把包含有文件长度和位置的文件描述符发到套接字缓冲区中,给用户区返回send()调用上下文切换。利用网卡DMA控制器可以直接访问内存中的文件描述符,把文件直接拷贝到网卡缓冲区中。总共只需要两次DMA拷贝,以及两次上下文切换。
kafka索引文件使用的是mmap+write,数据文件使用的是sendfile。假设有10个消费者,那么仅需要拷贝1(读写缓冲区)+10(10份文件网卡缓冲区)=11次,相比传统方法需要的40次而言大大减少。
因此实际上的零拷贝指的是在用户空间不需要拷贝,并不是真正的不需要拷贝。
七、使用MQ如何保证分布式事务最终一致性?
分布式事务:就是指业务相关的操作需要同时成功或者同时失败,比如过下单可以分为支付和下物流单两个操作,不可能会有你支付交钱了,人家没有把东西发给你的情况。
最终一致性是指,只要求最终事务之间状态是对齐的,中间允许有不对齐状态,比如A完成了B没完成,但是最后两个事务一定是同时完成或者未完成的。
强一致性要求每时每刻事务之间状态都是对齐相同的。
MQ要保证事务最终一致性需要保证两点:
1.生产者需要保证百分百的消息投递。通过事务消息机制。
2.消费者需要保证幂等性消费。唯一全局业务ID。