延时消息队列

如果遇到如下场景:

  1. 用户下单后,24小时内未付款则自动取消订单;
  2. 用户设定5分钟后提醒通知用户。

我们就需要延时消息队列,它是这样的一个消息队列:进入队列的消息在延时时间到达后才能出列被消费,时间未到达时对消费者是不可见的。

以下介绍的几种实现方案,可以依据具体业务需求进行选择。

一、简单粗暴的MongoDB

MongoDB吃内存的特性一值备受人诟病,但也是内存的大量使用,使得MongoDB比MySQL性能更好。使用MongoDB的文档原子修改特性,可以快速实现延时消息队列功能,在业务初期,不啻为简单粗暴的选择。

入列:

db.queue.insert({"body":"Hello","effective_at":ISODate("2017-09-17T00:30:00Z"),"ack":0,"read_count":0});

轮询出列(轮询间隔取决于业务需要的时间精度):

db.queue.findAndModify({ 
   query:{read_count:{$eq:0},effective_at:{$lte:ISODate("2017-09-17T00:30:00Z")}}, 
   update:{ 
      $inc:{read_count:1},
   }  
})
{
        "_id" : ObjectId("6049cd622764d56dec638f03"),
        "body" : "Hello",
        "effective_at" : ISODate("2017-09-17T00:30:00Z"),
        "ack" : 0,
        "read_count" : 0
}

消费确认:

db.queue.update(
   {"_id":ObjectId("6049cd622764d56dec638f03")},
   {$set:{"ack":1}}
)

非常适合进行定时或延时的内容推送、通知提醒等场景。

二、重量级、高可用的RabbitMQ

如果你的数据一致性比较重要,例如处理退款等重要业务逻辑,可以考虑适用RabbitMQ,下面介绍RabbitMQ如何实现延时消息队列。

三、高性能的Redis

redis之所以快是因为它是内存数据库,意味着数据可能会丢失,但不用过多担心,简单的主从备份即可满足大部分生产环境对数据可靠性的要求。遗憾的是使用redis的基本数据没法实现异步延时消息队列的,有人会说使用redis的有序集合不就可以了吗?如果每秒只有一个任务,当然没问题。倘若并发场景下,你可以控制每次取出来的消息数量吗?你可以保证消息被消费失败后能重回队列吗?纯粹地使用有序集合是没法满足这样的需求的。
其实我们可以使用redis的lua脚本来实现延时队列。

注意点:在redis集群中注意hashtag的使用。

使用lua脚本时,可以使用lua脚本的hash。

四、自己动手写一个

自己动手,丰衣足食。我们还可以实现到期主动通知的功能。 主要思路是: 消息队列服务端与消费者维持长连接,当有消息到期需要出列时,通知客户端。

下面提供了一个简单的demo,仅供参考。

//