复杂性问题
MQ的加入大大增加了系统的复杂度,以前系统是同步的远程调用,现在是通过MQ的异步调用。
一致性问题
A系统处理完业务,通过MQ给B,C,D三个系统发送消息数据,如果B系统,C系统处理成功,D系统处理失败。
可用性问题
如果MQ挂掉,系统中依赖MQ的服务都将不可用。比如:用户订单下单付款成功,会发送消息积分奖励消息到MQ。监听队列的消费者收到消息后给用户下发积分。如果MQ挂掉,用户收不到短信通知也收不到积分奖励。
复杂性问题
消息重复消费
解决: 幂等设计的方案
约束控制:数据库的唯一索引或者redis的setnx
前置条件:数据库的查询条件+分布式锁
全局id:生成消息的唯一id
消息丢失问题
解决:三个阶段保证。第一阶段:消息的生产阶段;第二阶段:消息的存储阶段;第三阶段:消息的消费阶段。
生产阶段:ack机制。消息发送到broker后,会返回给生产端消息已收到。若长时间未确认,生产端会重发。
存储阶段:一般消息不会丢失,除非服务器宕机。可以做持久化操作,消息发送到broker会做持久化到硬盘。成功后再返回给生产方确认机制。
消费阶段:与生产阶段类似。ack请求-确认机制。消费端成功处理后消息后,给broker发送确认消息。
有序性消费
比如商城下订单,订单付款成功,需要给用户发放积分,短信通知。发送积分和短信通知需要顺序消费。积分奖励发送成功之后才给用户发送短信。可以根据订单号哈希取模。保证同一个订单号的积分奖励和短信通知写入同一个队列。一个队列保证一个消费者,这样就可以保证消息顺序消费(局部顺序消费)。这样效率不高,我们同样可以开启多线程消费一个队列,但也要保证同一个消息在同一个线程里被消费。
一致性问题
产生消息的业务动作与发送的一致,就是说,如果操作成功了,那么这个操作产生的消息一定要发送出去,否则就丢失消息了。另一方面,如果业务行为没有发生或者失败则不能把消息发送出去。
事务消息:实现了消息生成者本地事务与消息发送的原子性,保证消息生成者本地事务处理成功与消息发送成功的最终一致性
问题。
- 事务消息与普通消息的区别就在于消息生产环节,生产者首先预发送一条消息到MQ(这也被称为发送half消息)
- MQ接受到消息后,先进行持久化,则存储中会新增一条状态为待发送的消息
- 然后返回ACK给消息生产者,此时MQ不会触发消息推送事件
- 生产者预发送消息成功后,执行本地事务
- 执行本地事务,执行完成后,发送执行结果给MQ
- MQ会根据结果删除或者更新消息状态为可发送
- 如果消息状态更新为可发送,则MQ会push消息给消费者,后面消息的消费和普通消息是一样的
注意点:由于MQ通常都会保证消息能够投递成功,因此,如果业务没有及时返回ACK结果,那么就有可能造成MQ的重复消息投递问题。因此,对于消息最终一致性的方案,消息的消费者必须要对消息的消费支持幂等,不能造成同一条消息的重复消费的情况。
如果consumer消费失败,是否需要producer做回滚?
答:事务消息,producer不会因为consumer消费失败而做回滚,采用事务消息的应用,其所追求的是高可用和最终一致性,消息消费失败的话,MQ自己会负责重推消息,直到消费成功。因此,事务消息是针对生产端而言的,而消费端,消费端的一致性是通过MQ的重试机制来完成的。
基于本地消息的最终一致性
基于本地消息的最终一致性
方案的最核心做法就是在执行业务操作的时候,记录一条消息数据到DB,并且消息数据的记录与业务数据的记录必须在同一个事务内完成,这是该方案的前提核心保障。在记录完成后消息数据后,后面我们就可以通过一个定时任务到DB中去轮训状态为待发送
的消息,然后将消息投递给MQ。这个过程中可能存在消息投递失败的可能,此时就依靠重试机制
来保证,直到成功收到MQ的ACK确认之后,再将消息状态更新或者消息清除;而后面消息的消费失败的话,则依赖MQ本身的重试来完成,其最后做到两边系统数据的最终一致性。
问题:每个业务系统在使用该方案时,都需要在对应的业务库创建一张消息表来存储消息,可以将该功能单独提取出来,做成一个消息服务来统一处理。
独立消息服务的最终一致性
过程其实就是模拟了事务消息的消息预发送过程,如果预发送消息失败,生产者业务就不会去执行,对于生产者的业务而言,它是强依赖于该消息服务的。不过好在独立消息服务支持水平扩容,只要部署多台,做成HA的集群模式,就能够保证其可靠性。在消息服务中,还有一个单独地定时任务,会定期轮训长时间处于
待发送
状态的消息,通过一个check补偿机制来确认该消息对应的业务是否成功,如果对应的业务处理成功,则将消息修改为可发送
,然后将其投递给MQ;如果业务处理失败,则将对应的消息更新或者删除即可。因此在使用该方案时,消息生产者必须同时实现一个check服务,来供消息服务做消息的确认。对于消息的消费,该方案与上面的处理是一样,都是通过MQ自身的重发机制来保证消息被消费。
可用性问题
搭建集群方式
搭建步骤
- 修改 3 台机器的主机名称 :vim /etc/hostname
- 配置各个节点的 hosts 文件,让各个节点都能互相识别对方
vim /etc/hosts10.211.55.74 node110.211.55.75 node210.211.55.76 node3
- 以确保各个节点的 cookie 文件使用的是同一个值
在 node1 上执行远程操作命令scp /var/lib/rabbitmq/.erlang.cookie root@node2:/var/lib/rabbitmq/.erlang.cookiescp /var/lib/rabbitmq/.erlang.cookie root@node3:/var/lib/rabbitmq/.erlang.cookie
- 启动 RabbitMQ 服务,顺带启动 Erlang 虚拟机和 RbbitMQ 应用服务(在三台节点上分别执行 以下命令)
rabbitmq-server -detached
- 在节点 2 执行
rabbitmqctl stop_app(rabbitmqctl stop 会将 Erlang 虚拟机关闭, rabbitmqctl stop_app 只关闭 RabbitMQ 服务 )rabbitmqctl resetrabbitmqctl join_cluster rabbit@node1rabbitmqctl start_app(只启动应用服务 )
- 在节点 3 执行rabbitmqctl stop_app
rabbitmqctl resetrabbitmqctl join_cluster rabbit@node2rabbitmqctl start_app
- 集群状态
rabbitmqctl cluster_status
- 需要重新设置用户
创建账号rabbitmqctl add_user admin 123设置用户角色rabbitmqctl set_user_tags admin administrator设置用户权限rabbitmqctl set_permissions -p "/" admin ".*" ".*" ".*"
- 解除集群节点(node2 和 node3 机器分别执行)
rabbitmqctl stop_apprabbitmqctl resetrabbitmqctl start_apprabbitmqctl cluster_statusrabbitmqctl forget_cluster_node rabbit@node2(node1 机器上执行 )