PHP框架Laravel基於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 有序集合中獲取可以處理的 "延遲任務", 並 rpush  queue:default隊列的尾部
    具體的執行語句:
redis> eval "Lua腳本" 2 queues:default:delayed queues:default 當前時間戳

Lua 腳本內容如下:

-- Get all of the jobs with an expired \"score\"...localval = 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)thenredis.call('zremrangebyrank', KEYS[1],0, #val -1)fori =1, #val,100doredis.call('rpush', KEYS[2],unpack(val, i,math.min(i+99, #val)))endendreturnval

 queue:default:reserved有序集合中獲取已過期的 "reserved 任務", 並 rpush  queue:default隊列的尾部

具體的執行語句:

redis> eval "Lua腳本" 2 queues:default:reserved queues:default 當前時間戳

使用的Lua腳本同步驟 1

 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}
  1. 這裏的 90 是根據配置而定: config('queue.connections.redis.retry_after')
    若預計任務耗時過久, 則應增加該數值, 防止任務還在執行時就被重置
  2. 在成功執行上面獲取的任務後, 就將該任務從 queues:default:reserved 隊列中移除掉
    具體執行語句: ZREM queues:default:reserved "具體任務"
  3. 如果執行任務失敗, 此時分爲2種情況:

任務失敗次數未達到指定的重試次數閥值
將該任務從 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

如果任務失敗次數超過指定的重試閥值
將該任務從 queue:default:reserved 中移除
執行語句:

redis> ZREM queue:default:reserved

注意, 上述使用 Lua 腳本的目的在於操作的原子性, Redis 是單進程單線程模式, 以Lua腳本形式執行命令時可以確保執行腳本的原子性, 而不會有併發問題.

以上內容希望幫助到大家, 很多PHPer在進階的時候總會遇到一些問題和瓶頸,業務代碼寫多了沒有方向感,不知道該從那裏入手去提升,對此我整理了一些資料,包括但不限於:分佈式架構、高可擴展、高性能、高併發、服務器性能調優、TP6,laravel,Redis,Swoole、Swoft、Kafka、Mysql優化、shell腳本、Docker、微服務、Nginx等多個知識點高級進階乾貨需要的可以免費分享給大家 ,需要戳這裏     PHP進階架構師>>>實戰視頻、大廠面試文檔免費獲取     

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章