消息中间件

用途

异步处理

img

  • 比如某个调用链的业务逻辑可以分为关键和非关键, 那么关键步骤执行完就能立即返回响应, 非关键步骤可以放入MQ异步地消费
  • 在业务高峰期, 可以用k8s动态增加关键服务的节点, 用MQ将非关键操作暂存
  • 在业务低谷期, 再动态减少关键服务的节点, 消费掉非关键服务
业务场景 关键步骤 非关键步骤
秒杀 网关, 鉴权, 并发控制 订单生成, 短信回馈, 更新页面

削峰填谷

  • 侵入式
    img

  • 本质上是在不同速率的服务之间用MQ做缓冲, 比如高性能网关和相对较慢的后端服务之间

  • 非侵入式(令牌桶做sidecar)
    img

分布式事务

  • 半消息 + 2PC
    img

产品对比

MQ 特点
Kafka 适合大数据集中处理, broker采用"先攒后传"模型, 吞吐量高但延时也高
RocketMQ 适合实时场景, 直接发送, 延时低, 吞吐量低

模型

img

  • 生产者 -> Broker -> 消费者
  • 为了提高吞吐量, 主题下有多个分区(1:N), 分区的并集是完整的主题
    • 分区可以部署到多MQ实例上, 类似redis切片或水平分表的概念
    • 分区可以冗余复制到多MQ集群上, 类似备库的概念
    • 有序性只在分区层面上保证, 主题层面可能无序
  • 多个消费者组成消费者组, 多个消费者实例并行地消费多个队列(1:1)
  • 具体消费进度由offset标识

消息丢失

  • 如何侦测丢失? 发送时用interceptor附加递增编号和Producer特征, 消费时查看每个分区内的消息编号是否连续
  • 生产阶段: 请求-响应机制确保可靠性
    • 需要正确处理return正常catch重试逻辑
    • 假如响应延迟/丢失, 生产者就认为发送失败, 会造成重复发送
      img
// 同步式
try {
    RecordMetadata metadata = producer.send(record).get();
    log.info(" 消息发送成功!");
} catch (Throwable e) {
    log.warn(" 消息发送失败!尝试重试");
    try {
        retry10Times();
    } catch (Throwable e) {
        log.error("重试失败!");
        return 
    }
}
// 异步式
producer.send(record, (metadata, exceptionHandler()) -> {
    if (metadata != null) {
        log.info(" 消息发送成功。");
    } else {
        log.warn(" 消息发送失败!, 尝试重试");
        () -> exceptionHandler();
    }
});
  • broker中转阶段

    • 单点broker -> 先刷盘后转发
    • 集群broker -> raft, 发送到一半以上broker实例再转发
  • 消费阶段: 消费-反馈机制

    • 假如消费成功, 但反馈信息延迟/丢失, broker就认为消费失败, 会造成重复消费
def callback(ch, method, properties, body):
    print(" [x] 收到消息 %r" % body)
    # 在这儿处理收到的消息
    database.save(body)
    print(" [x] 消费完成 ")
    # 完成消费业务逻辑后发送消费确认响应
    ch.basic_ack(delivery_tag = method.delivery_tag)
 
channel.basic_consume(queue='hello', on_message_callback=callback)

消息重复

  • MQ可靠性等级
    • 至多一次 -> 允许消息丢失, 但不会重复, 消息丢失由业务层保证
    • 至少一次 -> 允许消息重复, 但不会丢失, 消息重复由业务层保证
    • 恰好一次 -> 不重复不丢失
  • 大多数MQ保证至少一次, 因为业务层去重更简单(接口幂等性)
    • 读请求天然幂等
    • 写请求
      • 实现if absent逻辑, 比如建全局去重表和唯一索引/NX/xxx if not exists, 让消息先走去重表再发到消费者
      • 乐观锁版本号机制, 确保一次写请求后版本号+1, 而执行写请求的前提条件是保持原版本号
      • 分布式锁 + 消息全局唯一id, 对所有相同的写操作, 执行前先检查状态, 如果是未执行就执行并更新状态, 否则丢弃, 这个过程的原子性由分布式锁保证

消息积压

消费速率可以 > 生产, 但绝不能倒挂

  • 生产阶段慢
    • stub并发化 -> 低时延
    • 批量化 -> 高吞吐
  • 消费阶段慢
    • stub并发化, 多reactor
    • 弹性消费, 根据监控模块判断
      • 如果是流量洪峰导致生产海啸 -> 消费者动态扩容
      • 如果是消费者集群宕机 -> 容备灾/降级/快速恢复消费能力
      • 如果是消费阻塞 -> 熔断/人工介入
posted @ 2022-09-07 14:25  Blazer96  阅读(27)  评论(0)    收藏  举报