前段時間寫了個文章詳細描述了在什麼場景下會出現redis的protocol error錯誤,可是手抽筋, 不當心點錯給刪了,並且還原不了,沒辦法了,只能重寫一下,可是沒上次那麼詳細了,若是不太明白就看源代碼吧!首先呢,這種錯誤是基於使用了phpredis的的長鏈接和multi功能纔會出現!這裏有兩個問題php
一、當你開了事務,作了N次寫操做,而後又discard以後又作了M次操做(M小於N),這樣請求就會被阻塞住(這個操做不管使用短鏈接仍是長鏈接,都能復現),具體看代碼:git
$redis = new Redis(); $redis->connect('localhost', 6379); $redis->multi(); $redis->set('test', 10); $redis->zIncrBy('test2', 1, 'bbb'); $redis->discard(); $redis->multi(); $redis->zIncrBy('test2', 2, 'bbb'); $redis->exec();//操做會阻塞在這裏
由於phpredis在discard成功後,沒有清理callback list,因此卡住了。github
二、開事務,作N次操做,discard以後再作M次操做(但這裏M大於N),多刷幾回就會出現protocol error(這個必須使用長鏈接纔會復現)!redis
$redis = new Redis(); $redis->pconnect('localhost', 6379); $redis->multi(); $redis->set('test', 10); $redis->discard(); $redis->multi(); $redis->zIncrBy('test2', 2, 'bbb'); $redis->zIncrBy('test2', 1, 'bbb'); $redis->exec();
跟上面緣由同樣,discard沒的清理callback list, 就會出現stream裏面的數據沒讀完!協議就徹底亂掉了,函數
那麼上面說的callback list又是什麼東西呢?在redis裏面,當你使用了multi,在執行exec以前的請求基本都是返回+QUEUED(若是須要了解更詳細redis協議,請見redis.io),而真正返回數據是等exec執行以後,纔去解析返回數據。因此phpredis針對不一樣的請求處理方式是不同的,因此在開啓了multi以後,phpredis會維護一個處理函數列表,好比set(k,v)這須要綁定一個bool值處理函數,而zincrBy須要綁定一個double值處理函數,執行exec以後,去遍歷這個列表處理返回數據就便可。spa
因爲redis針對multi以後的請求都是隊列並無執行,因此客戶端能夠使用discard命令來清空這個隊列,同時客戶端也應該將以前綁定的函數列表一併清除,但是phpredis對於discard的處理僅僅是發送了discard命令到redis服務端,卻沒有清空處理函數列表。只是在下一次執行multi的時候,他僅僅是將這個處理函數列表中一個叫current的指針值爲NULL(這個列表是一個單向連表,head表示頭元素,current表示尾元素),但是他忽略了head,由於函數列表一旦進入處理是從head開始,只有須要新加函數到列表的時候纔會用到current。因此在phpredis裏面,執行一條命令,再discard,再執行兩條命令以後,這個處理函數列表裏只有一個函數(正確應該是兩個,並且仍是最開始加的那一個,後面加的兩個,不知去向了。。),處理函數與返回數據不配對,協議天然也就亂了,這就是protocol error報錯的由來....指針
本想着提個bug給phpredis就行了,結果提了也不見他們修改,因而就本身改了,修改的地方:code
https://github.com/scgywx/phpredis/commit/3f05eb7acd3b7f64d1f4f857767b5dd74d585cd9隊列