内容

面试官: 你项目中用 RocketMQ 解决协同编辑冲突和保证最终一致性的设计非常关键。

消息模型选择

Q1. 你说采用了集群/广播模式适配不同场景。能分别举个具体场景的例子说明为什么选集群模式,为什么选广播模式吗?

A1. 先说一下广播消费场景

  • 广播消费不支持顺序消息,不支持重置消费位点,消费进度由客户端维护。那么我们的场景就限定在了容忍无顺序消费,且所有客户端都需要该消息的情况。

    根据已有业务,我们在媒体流同步方面使用了广播消费模式,因为用户连接到的服务器实例是不可预知的,所以对于每一个实例,我们都需要同步直播间所有媒体流信息。而且媒体流信息和顺序无关,只需最终一致性即可。

  • 集群消费支持顺序消息,支持重置消费位点,消费进度由服务端维护。那么我们的场景就限定在了需要顺序消费,且消息只需被一个消费者消费,适用于服务解耦的情况。

    根据已有业务,我们在录播与知识图谱关联业务上使用了集群消费模式,录播服务所在服务器接收媒体流服务器录播回调后,保存录播信息到本地。

    服务器实例发送录播上传完成的事务半消息,本地使用 TUS 协议进行录播大文件分片上传与断点续传,完成后半消息成为普通消息放入相应队列。

    集群中消费者服务器实例通过查询消息携带的上传地址,完成后续录播和知识图谱的关联。

事务消息保证原子性:

Q1. 请详细解释一下 RocketMQ 事务消息的工作流程(Half Message, 本地事务执行,Commit/Rollback,事务状态回查)。

A1. RocketMQ 事务消息工作流程如下:

  1. 首先是生产者向 Broker 发送半消息,即被标记了“暂不可投递”状态的消息。此时 Broker 会向生产者发送接收消息确认。
  2. 若没有收到接收消息确认,生产者则不进行本地事务操作,Broker 将会在后续回查中查询到事务执行失败,删除半消息。
  3. 随后生产者开始执行本地事务。在此过程中 Broker 若未接收到生产者发送的消息,则回查生产者的事务执行状态。
  4. 若事务执行失败,则本地事务回滚,且 Broker 回滚,其中的半消息标记清除;
  5. 若事务执行成功,消费者向 Broker 发送提交消息,则 Broker 正常提交消息。

Q2. 在你的“本地操作与消息发送原子性”场景中,“本地操作”具体指什么?如果本地事务成功但消息 Commit 失败(或超时)了,系统会怎样?如何处理的?

A2. 本地操作指生产者在接收到半消息接收确认后,开始进行的本地事务操作。

  • 若本地事务成功,但消息发送失败,Broker 还是会回查生产者事务执行状态,业务中需要实现消息状态回查接口,选择重新 Commit 或者回滚。

Q3. 事务状态回查是如何实现的?需要注意什么?

A3. Broker 设置了一个定时任务,定时遍历未提交/回滚的半消息,向消息发送方发送回查请求以获取本地事务状态。如果回查次数大于 maxCheckTime,那么默认回滚。

  • 所有的 half 消息都会写到同一个 topic 中,且每个半消息都会在整个过程中被写多次,如果刚好并发的都是事务消息,会影响可靠性。

  • 对于大文件上传的事务情况,我们会考虑配置 BrokertransactionCheckInterval,根据分片和网速大小来定回查间隔。

最终一致性保障

Q1. 消息重试机制是如何配置的?最大重试次数、重试间隔?

A1. 消息重试机制设计生产者和消费者两端,而消费者重试则关注死信队列。

  • 对于生产者,可以使用 setRetryTimesWhenSendFailed 来设定发送重试次数
  • 对于消费者,只有在集群模式下,Broker 才会进行重试,广播模式下不会重试。可以在 broker.conf 文件中配置消费者端的重试次数和重试时间间隔:messageDelayLevel=1s 5s 10s 30s...,这种递增的延迟队列,保证了可以多次重试,并且通过延长延迟时间使任务尽可能成功。(默认 16 次,第 17 次就进入死信队列)

Q2. 消息最终进入死信队列意味着什么?进入死信队列后,你们是如何处理的?(人工干预?有补偿 Job?)

A2. 消息会因为如下三种情况而进入死信队列:

  1. 消费者在处理消息时发生了异常,并且没有进行正确的重试机制
  2. 消费者处理消息的时间过长,导致消息超时被认为处理失败
  3. 消息被拒绝(例如,消费者明确拒绝该消息)
  • 通常来说可以通过 RocketMQ Dashboard 及时发现问题,并进行故障排查,开发人员进行人工干预,找出消息失败的原因,修复后重新处理这些消息。
  • 或者放入死信队列后等待其他节点处理死信队列中的数据,通过独立消费者解析死信队列中信息失败原因,对于可重试的消息,投递回业务 topic;对于需要修复的就告警+入库,通过人工处理。

Q3. 你说文档冲突修改丢失率降至<0.1%,除了 MQ 的可靠性保障,在消费者端处理协同冲突时,采用了什么算法或策略?(比如 OT - Operational Transformation, CRDT - Conflict-free Replicated Data Type?还是基于版本号/时间戳的简单合并?)请简述其思想。

A3. 除了 MQ 之外,我们还使用了 OT 算法,该算法起初使用于在线多人文档协作解决冲突的算法,现在可以应用于 json 数据等多种分布式协作数据场景。该算法的思想是使用插入和删除两个元操作,并设计了转换操作来保证最终一致性:

  • 插入和插入:如果两个插入位置相同,则一个操作的位置要向后移动
  • 删除和删除:如果两个删除位置相同,则一个操作需要忽略
  • 插入和删除:如果插入在删除操作之前,则删除位置需要向后移动;如果插入在删除之后,则插入位置要向前移动。

对于操作排序:

  • 通过版本向量(Version Vector) 确定操作时序
  • 每个客户端维护自己的版本计数器

记录所有转换操作,顺序应用到文档上。