1、请详细描述一条判题消息从生产到消费的完整生命周期。你在生产者端、Broker端和消费者端分别做了哪些具体配置和编码保证?
这是一个考察消息队列端到端可靠性的经典问题。我的设计目标是:消息绝不能丢。
- 生产者端 -> RabbitMQ:确保消息成功送达Broker
1.1 机制:启用 Publisher Confirm 机制。
1.2 实现
1) 在生产者代码中,将信道(Channel)设置为 confirm 模式。
2)发送消息后,异步监听确认回调。
3)在回调中,如果收到 ack,表示消息已持久化到磁盘(如果是持久化消息)。如果收到 nack 或超时未收到确认,则进行重试。 - Broker端:确保消息在RabbitMQ重启后不丢失
2.1 机制:队列和消息都持久化。
2.2 实现:
1)队列持久化:在声明队列时,设置 durable = true。
2)消息持久化:在发送消息时,将 delivery_mode 属性设置为 2 (MessageProperties.PERSISTENT_TEXT_PLAIN)。
这样即使RabbitMQ服务器宕机重启,队列和消息也能恢复。 - 消费者端 -> 业务处理:确保消息被成功消费
3.1 机制:启用 手动确认模式,并保证业务处理与确认的原子性。
3.2 实现:
1)关闭自动确认 autoAck = false。
2)只有在判题业务完全成功(包括代码执行、结果写入数据库等所有步骤完成后),才调用 channel.basicAck(deliveryTag, false) 进行手动确认。
3) 如果处理过程中抛出异常,则调用 channel.basicNack(deliveryTag, false, true),让消息重新回到队列,等待再次被投递。
2、为什么需要幂等性?在什么场景下会导致RabbitMQ向消费者投递重复的消息? - 为什么需要:因为网络不确定性和消费者故障可能导致同一条消息被多次投递和处理。如果判题操作不是幂等的,用户提交一次代码,可能会在排行榜上增加多次积分,或者产生多条一模一样的判题记录,这绝对是业务灾难。
- 重复投递场景:
1)消费者超时:消费者处理时间过长,超过了RabbitMQ的 consumer_timeout,Broker会认为消费者死亡,从而将消息重新投递给其他消费者。
2)消费者崩溃:消费者在处理消息后、发送ACK之前突然崩溃,消息会重新入队。
3)网络抖动:ACK确认消息在网络传输中丢失。
3、请深入讲解你实现幂等校验的完整技术和业务流程。 - 生产者(接收提交请求的服务)在创建判题任务时,生成一个全局唯一的标识作为幂等Token。这个标识通常是 SubmissionId(业务主键)或者一个雪花算法生成的UUID,随消息体一同发送。
- 消费者在开始处理消息前,先执行幂等校验。
- 校验通过,开始判题;校验不通过,直接丢弃消息并ACK。
4、如果消费者从MQ拉取消息后,在判题完成之前,应用崩溃了,会发生什么?
这是一个检验消息可靠性机制是否健全的完美问题。
- 会发生什么:因为消费者开启了手动确认模式且尚未发送ACK,当消费者通道关闭时,RabbitMQ会检测到这一点,从而判定该消息交付失败。
- 如何确保不丢失:RabbitMQ会自动将这条消息重新入队,并将其准备投递给下一个可用的消费者(或者等当前消费者重启后再次投递给它)。
- 与幂等性的关系:这正是幂等性设计至关重要的原因!当消息被重新投递给新的消费者时,会再次触发幂等校验。由于之前崩溃的消费者已经在Redis中设置了 PROCESSING 状态键,新消费者会认为这是一个重复消息,从而安全地跳过处理或等待。这完美地避免了因为消费者崩溃而导致的任务重复执行。
5、如何保证同一个用户的多个连续提交,其判题结果的返回顺序与提交顺序一致?
这是一个消息顺序性问题。在分布式环境下,保证全局顺序极其困难且代价高昂。我的设计权衡是:不保证全局顺序,但保证用户级顺序。
我的解决方案:
- 用户级消息分区:不再使用一个全局的 judge.queue。而是为每个用户创建一个逻辑上的“消息通道”。在RabbitMQ中,可以通过在路由键中加入用户ID哈希来实现,例如 judge.queue.user.%userId%,或者更实际一点,使用一致性哈希将同一用户的请求路由到同一个队列。
- 单消费者串行化:确保每个用户队列只有一个消费者。这样,同一个用户的消息在队列里是顺序的,并被同一个消费者串行处理,自然就保证了顺序。