• [原创]Laravel 基于redis队列的解析


    Last-Modified: 2019年5月10日11:44:18

    参考链接

    本文环境

    • Laravel 5.5
    • 队列 Redis

    为什么使用队列

    使用队列的目的一般是:

    1. 异步执行
    2. 出错重试

    解释一下:

    异步执行: 部分代码执行很耗时, 为了提高响应速度及避免占用过多连接资源, 可以将这部分代码放到队列中异步执行.

    Eg. 网站新用户注册后, 需要发送欢迎的邮件, 涉及到网络IO无法控制耗时的这一类就很适合放到队列中来执行.

    出错重试: 为了保证一些任务的正常执行, 可以将任务放到队列中执行, 若执行出错则可以延迟一段时间后重试, 直到任务处理成功或出错超过N次后取消执行.

    Eg. 用户需要绑定手机号, 此时发送短信的接口是依赖第三方, 一个是不确定耗时, 一个是不确定调用的成功, 为了保证调用成功, 必然需要在出错后重试

    Laravel 中的队列

    以下分析默认使用的队列及其配置如下

    • 默认队列引擎: redis

    通过在 redis-cli 中使用 monitor 命令查看具体执行的命令语句

    • 默认队列名: default

    分发任务

    此处以分发 异步通知(class XxxNotification implement ShouldQueue)为例.

    在Laravel中发起异步通知时, Laravel 会往redis中的任务队列添加一条新任务

    redis 执行语句

    redis> RPUSH queues:default
    
    {
    	"displayName": "App\Listeners\RebateEventListener",
    	"job": "Illuminate\Queue\CallQueuedHandler@call",
    	"maxTries": null,
    	"timeout": null,
    	"timeoutAt": null,
    	"data": {
    		"commandName": "Illuminate\Events\CallQueuedListener",
    		"command": "O:36:"Illuminate\Events\CallQueuedListener":7:{s:5:"class";s:33:"App\Listeners\RebateEventListener";s:6:"method";s:15:"onRebateCreated";s:4:"data";a:1:{i:0;O:29:"App\Events\RebateCreatedEvent":4:{s:11:"u0000*u0000tbkOrder";O:45:"Illuminate\Contracts\Database\ModelIdentifier":3:{s:5:"class";s:19:"App\Models\TbkOrder";s:2:"id";i:416;s:10:"connection";s:5:"mysql";}s:15:"u0000*u0000notifyAdmins";b:1;s:13:"u0000*u0000manualBind";b:0;s:6:"socket";N;}}s:5:"tries";N;s:9:"timeoutAt";N;s:7:"timeout";N;s:6:"u0000*u0000job";N;}"
    	},
    	"id": "iTqpbeDqqFb3VoED2WP3pgmDbLAUQcMB",
    	"attempts": 0
    }
    

    上面的redis语句是将任务信息(json格式) rpush 到 redis 队列 queues:default 的尾部.

    任务队列 Worker

    Laravel 处理任务队列的进程开启方式: php artisan queue:work, 为了更好的观察, 这里使用 --once 选项来指定队列中的单一任务进行处理, 具体的更多参数请自行参考文档

    php artisan queue:work --once --delay=1 --tries=3
    

    上述执行语句参数含义:

    1. --once 仅执行一次任务, 默认是常驻进程一直执行
    2. --tries=3 任务出错最多重试3次, 默认是无限制重试
    3. --delay=1 任务出错后, 每次延迟1秒后再次执行, 默认是延迟0秒

    当 Worker 启动时, 它依次执行如下步骤:

    此处仍以默认队列 default 为例讲解, 且只讲解redis的相关操作

    1. queues:default:delayed 有序集合中获取可以处理的 "延迟任务", 并 rpushqueue:default队列的尾部

      具体的执行语句:

      redis> eval "Lua脚本" 2 queues:default:delayed queues:default 当前时间戳
      

      Lua 脚本内容如下:

      -- Get all of the jobs with an expired "score"...
      local val = redis.call('zrangebyscore', KEYS[1], '-inf', ARGV[1])
      
      -- If we have values in the array, we will remove them from the first queue
      -- and add them onto thedestination queue in chunks of 100, which moves
      -- all of the appropriate jobs onto the destination queue very safely.
      if(next(val) ~= nil) then
          redis.call('zremrangebyrank', KEYS[1], 0, #val - 1)
      
          for i = 1, #val, 100 do
              redis.call('rpush', KEYS[2], unpack(val, i, math.min(i+99, #val)))
          end
      end
      
      return val 
      
    2. queue:default:reserved有序集合中获取已过期的 "reserved 任务", 并 rpushqueue:default队列的尾部

      具体的执行语句:

      redis> eval "Lua脚本" 2 queues:default:reserved queues:default 当前时间戳
      

      使用的Lua脚本同步骤 1

    3. queue:default 队列中获取(lpop)一个任务, 增加其 attempts 次数, 并将该任务保存到 queu:default:reserved 有序集合中, 该任务的 score 值为 当前时间 + 90(任务执行超时时间)

      具体的执行语句:

      redis> eval “Lua脚本” 2 queues:default queues:default:reserved 任务超时时间戳
      

      Lua脚本

      -- Pop the first job off of the queue...
      local job = redis.call('lpop', KEYS[1])
      local reserved = false
      
      if(job ~= false) then
          -- Increment the attempt count and place job on the reserved queue...
          reserved = cjson.decode(job)
          reserved['attempts'] = reserved['attempts'] + 1
          reserved = cjson.encode(reserved)
          redis.call('zadd', KEYS[2], ARGV[1], reserved)
      end
      
      return {job, reserved}
      

      这里的 90 是根据配置而定: config('queue.connections.redis.retry_after')

      若预计任务耗时过久, 则应增加该数值, 防止任务还在执行时就被重置

    4. 在成功执行上面获取的任务后, 就将该任务从 queues:default:reserved 队列中移除掉

      具体执行语句: ZREM queues:default:reserved "具体任务"

    5. 如果执行任务失败, 此时分为2种情况:

      1. 任务失败次数未达到指定的重试次数阀值

        将该任务从 queues:default:reserved 中移除, 并将该任务添加到 queue:default:delayed 有序集合中, score 为该任务下一次执行的时间戳

        执行语句:

        redis> EVAL "Lua脚本" 2 queues:default:delayed queues:default:reserved "失败的任务" 任务延迟执行的时间戳
        

        Lua脚本

        -- Remove the job from the current queue...
        redis.call('zrem', KEYS[2], ARGV[1])
        
        -- Add the job onto the "delayed" queue...
        redis.call('zadd', KEYS[1], ARGV[2], ARGV[1])
        
        return true
        
      2. 如果任务失败次数超过指定的重试阀值

        将该任务从 queue:default:reserved 中移除

        执行语句:

        redis> ZREM queue:default:reserved
        

    注意, 上述使用 Lua 脚本的目的在于操作的原子性, Redis 是单进程单线程模式, 以Lua脚本形式执行命令时可以确保执行脚本的原子性, 而不会有并发问题.

  • 相关阅读:
    [POJ 1200]Crazy Search
    [来源不详]删数方案数
    noip搜索模拟题 骰子
    [SDOI2010]地精部落
    [HAOI2008]硬币购物
    BZOJ1009: [HNOI2008]GT考试
    BZOJ1830: [AHOI2008]Y型项链 & BZOJ1789: [Ahoi2008]Necklace Y型项链
    BZOJ1251: 序列终结者
    BZOJ3620: 似乎在梦中见过的样子
    BZOJ4477: [Jsoi2015]字符串树
  • 原文地址:https://www.cnblogs.com/youjiaxing/p/10843674.html
Copyright © 2020-2023  润新知