1.起源很多时候,业务需要“在一段时间后完成一个工作任务”。例如:滴滴打车订单完成后,如果用户没有评论,48小时后会自动评价为5星。一般来说,如何实现这种“48小时后自动评价为5星”的需求呢?1、常用解决方案:启动一个cron定时任务,每小时运行一次,取出完成时间超过48小时的订单,设置为5星,审核状态设置为已审核。假设订单表的结构是:t_order(oid,finish_time,stars,status,…),更具体地说,计划任务将每小时执行一次:selectoidfromt_orderwherefinish_time>48hoursandstatus=0;updatet_ordersetstars=5andstatus=1whereoidin[…];如果数据量很大,需要进行分页查询和分页更新,这会是一个for循环。2.方案的不足:(1)轮询效率比较低(2)每次扫描库时,已经记录了,仍然会扫描(但不会出现在结果集中),并且有有重复计算嫌疑(3)时效性不够好。如果每小时轮询一次,最坏的情况下,时间误差会达到1小时。(4)如果通过提高cron轮询频率来减少(3)中的时间误差,则(1)中的轮询低效和(2)中重复计算的问题将进一步凸显如何使用“延迟消息”来触发每个任务只执行一次,在保证实时性的同时保证效率。这就是今天要讨论的问题。2.高效延时消息的设计与实现高效延时消息包含两个重要的数据结构:环形队列,例如可以创建一个环形队列(本质上是一个数组),其中包含3600个slots的任务集合,每个slot就是一个Set。同时启动定时器。该定时器每1s在上述循环队列中移动一格,并有一个CurrentIndex指针来标识被检测的slot。Task结构中有两个很重要的属性:Cycle-Num:当当前索引扫描到这个slot时,执行任务Task-Function:指向待执行任务的指针假设当前CurrentIndex指向***cell,当有延迟消息到达后,比如你想在3610秒后触发一个延迟消息任务,你只需要:计算这个Task应该放在哪个slot,现在指向1。3610后seconds,应该是第11个slot,所以这个Task应该放在第11个slot的Set里面,计算这个Task的Cycle-Num。由于循环队列是3600格(每秒移动一格,正好1小时),这个任务会在3610秒后执行,所以应该绕3610/0=1个循环再执行,所以Cycle-Num=1CurrentIndex不停移动,每秒移动到一个新的slot,这个slot中对应的Set,每个Task检查Cycle-Num是否为0:如果不为0,说明还需要移动几个laps,将Cycle-Num减1,如果为0,表示Task即将执行,取出Task-Funciton执行(可以使用单独线程执行Task),将Task放入使用“延时消息”方案后,Set中去掉了“48小时后关闭评价”的要求,只需要在关闭订单的48小时后触发一条延时消息:无需轮询所有订单,效率高一个命令,任务只执行一次,时效性好,精确到秒(控制定时器走动的频率可以控制时间)eaccuracy)3.综上所述,环形队列是实现“延迟消息”的好方法。开源的MQ好像不支持延迟消息,你还不如自己实现一个简单的“延迟消息队列”,可以解决很多业务问题,减少很多扫描数据库的低效cron任务。【本文为专栏作者《58神剑》原创稿件,转载请联系原作者】点此阅读更多该作者好文
