欢迎光临
我们一直在努力

延时消息的几种实现方式及优缺点

方案①:定时任务轮询

核心原理

写一个定时任务(比如用 Spring Task),每隔固定时间(比如 1 分钟)去数据库里查一遍「到期 / 超时」的数据,查到后执行业务逻辑。

举个例子:订单关单场景,就是定时任务查 expire_time <= now() AND status = '待支付' 的订单,然后批量关单。

特点说明
✅ 优点:「准」 数据存在数据库里,只要定时任务执行了,就一定会查到并处理,不会丢数据,可靠性拉满。
❌ 缺点:「时效差」 延迟精度由轮询间隔决定:比如 1 分钟轮询一次,订单最多会晚 1 分钟才被关闭,做不到秒级精准延时。

隐藏坑点

  • 资源浪费:不管有没有到期数据,都要频繁查库,高并发场景下会给数据库带来不必要的压力。
  • 重复执行:分布式部署多个定时任务实例时,会同时查库导致重复处理,必须加分布式锁(比如 Redis 锁)控制。
  • 扩展性差:如果业务需要不同的延时时间(比如有的订单 10 分钟超时,有的 30 分钟),轮询间隔不好设置,设小了浪费,设大了不满足需求。
  • 适用场景

    对延时精度要求低(分钟级以上)、数据量不大的非核心场景,比如:

    • 每日凌晨对账、用户每周签到提醒
    • 非紧急的订单超时兜底(和其他方案搭配使用)

    三、方案②:Redis Key 过期监听

    核心原理

    利用 Redis 的Key 过期事件通知机制:给需要延时处理的业务数据(比如订单 ID)设置一个带过期时间的 Key,比如 setex order:123 1800 1(30 分钟过期)。

    当 Key 过期时,Redis 会发布一个 expired 事件,客户端监听这个事件,收到后就去执行关单逻辑。

    优缺点 & 对应你笔记里的点

    特点说明
    ✅ 优点:实现简单 不用额外组件,只要 Redis,配置一下就能用,延时精度比定时任务好(能到秒级)。
    ❌ 缺点:「丢失」 这是这个方案的致命问题!Redis 的过期事件通知是不可靠的发布订阅模式,如果消费者掉线、Redis 主从切换、网络波动,事件就会直接丢失,而且 Redis 不会补发。

    隐藏坑点

  • 配置门槛:Redis 默认关闭过期事件通知,必须修改配置 notify-keyspace-events Ex 才能收到事件,很多新手会踩这个坑。
  • 时间不准:Redis 的 Key 过期是「惰性删除 + 定期删除」,不是到点就删,可能会出现 Key 过期了但 Redis 没扫到,导致事件延迟触发。
  • 无法持久化:如果 Redis 宕机,Key 的过期信息直接丢失,重启后也不会补发事件。
  • 主从限制:主节点的过期事件不会同步到从节点,只能监听主节点,主从切换后事件会断流。
  • 适用场景

    只适合对可靠性要求极低、允许少量消息丢失的非核心场景,比如:

    • 用户登录态过期提醒、非核心缓存清理
    • 不建议用在订单关单、支付回调这类核心业务,丢事件会导致资损。

    四、方案③:MQ 延时队列(RabbitMQ / RocketMQ)

    一、什么是 MQ 延时队列

    普通 MQ:

    生产者发送消息 -> Broker -> 消费者立刻消费

    延时队列:

    生产者发送消息 -> Broker 暂存一段时间 -> 到期后投递给消费者

    注意:延时消息一般不是“到时间必然执行成功”,而是“到时间后触发一次检查”。业务上还要查数据库状态,不能只相信消息。


    二、RabbitMQ 延时队列

    RabbitMQ 本身原生没有特别完善的延时消息能力,常见有两种实现方式。

    方式一:TTL + 死信队列

    这是最经典的 RabbitMQ 延时队列方案。

    核心组件:

    TTL:消息过期时间
    DLX:Dead Letter Exchange,死信交换机
    死信队列:接收过期消息的队列

    流程:

    生产者发送消息到延时队列
    消息在延时队列中等待 TTL 时间
    消息过期后变成死信
    RabbitMQ 把死信转发到死信交换机
    死信交换机路由到真正的消费队列
    消费者消费消息

    订单超时示例:

    order.delay.queue:延时队列,设置 TTL = 30 分钟
    order.close.exchange:死信交换机
    order.close.queue:真正消费关闭订单消息的队列

    流程:

    创建订单 -> 发消息到 order.delay.queue
    30 分钟后消息过期 -> 进入 order.close.queue
    消费者消费 -> 查询订单是否未支付 -> 关闭订单

    优点:

    实现简单
    不需要额外插件
    适合固定延时时间

    缺点:

    不适合大量不同延时时间
    可能有队头阻塞问题
    延迟精度一般
    管理起来相对麻烦

    队头阻塞是什么意思?

    假设队列里第一条消息延迟 30 分钟,第二条消息延迟 5 分钟。
    如果 RabbitMQ 按队列顺序检查过期,第二条消息可能被第一条挡住,不能准时投递。

    所以 TTL + 死信队列更适合:

    固定延时,比如统一 30 分钟关闭订单
    统一 10 分钟重试
    统一 24 小时提醒


    三、RocketMQ 延时队列

    RocketMQ 对延时消息支持更直接。

    早期 RocketMQ 版本主要支持 固定延时等级。

    例如:

    1s
    5s
    10s
    30s
    1m
    2m
    3m
    4m
    5m
    6m
    7m
    8m
    9m
    10m
    20m
    30m
    1h
    2h

    发送消息时指定延时等级:

    message.setDelayTimeLevel(16);

    比如 level 16 可能代表 30 分钟,具体看 broker 配置。

    流程:

    生产者发送延时消息
    RocketMQ 根据延时等级暂存消息
    到时间后投递到真实 Topic
    消费者消费消息

    订单超时关闭:

    创建订单
    发送延时等级为 30 分钟的消息
    30 分钟后消费者收到消息
    查询订单状态
    未支付则关闭订单

    RocketMQ 的优点:

    原生支持延时消息
    使用简单
    适合订单超时、支付超时、定时触发类业务
    吞吐能力较强

    缺点:

    早期版本延时时间不够灵活,只能选固定等级
    不适合特别精确的定时任务
    大量长时间延时消息也要关注 Broker 存储压力

    现在一些新版本 RocketMQ 对定时/延时消息支持更灵活,但面试时你可以先讲经典的延时等级机制。


    四、RabbitMQ 和 RocketMQ 对比

    RabbitMQ:
    更常见方案是 TTL + 死信队列,或者延时插件。
    适合固定延时、业务量中等、已有 RabbitMQ 技术栈的系统。

    RocketMQ:
    原生支持延时等级消息。
    适合订单、支付、交易类场景,尤其是电商、金融、履约系统。

    简单对比:

    实现复杂度:
    RocketMQ 更简单,RabbitMQ TTL + DLX 更绕。

    延时灵活性:
    RabbitMQ 插件比较灵活;RabbitMQ TTL + DLX 不太适合动态延时。
    RocketMQ 早期固定等级,灵活性有限。

    业务适配:
    RocketMQ 更适合交易链路里的延时消息。
    RabbitMQ 也能做,但要设计好死信队列和路由。


    关键注意点

  • 消费者必须查数据库状态
    不能消息到了就直接改状态。

  • 消费要幂等
    MQ 可能重复投递。

  • 要有失败重试
    消费失败不能直接丢。

  • 最好有定时任务兜底
    延时队列负责实时触发,定时任务负责补偿。

  • 延时精度不要要求太高
    MQ 延时队列适合“分钟级/秒级业务触发”,不是高精度定时器。

  • 大量延时消息会占 Broker 资源
    高并发场景要评估消息堆积和存储压力。

  • 赞(0)
    未经允许不得转载:171主机测评 » 延时消息的几种实现方式及优缺点
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址