redis報protocol error的真正原兇 原 薦

前段時間寫了個文章詳細描述了在什麼場景下會出現redis的protocol error錯誤,但是手抽筋, 不小心點錯給刪了,而且還原不了,沒辦法了,只能重寫一下,但是沒上次那麼詳細了,如果不太明白就看源代碼吧!首先呢,這種錯誤是基於使用了phpredis的的長連接和multi功能纔會出現!這裏有兩個問題

1、當你開了事務,做了N次寫操作,然後又discard之後又做了M次操作(M小於N),這樣請求就會被阻塞住(這個操作無論使用短連接還是長連接,都能復現),具體看代碼:

$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,所以卡住了。


2、開事務,做N次操作,discard之後再做M次操作(但這裏M大於N),多刷幾次就會出現protocol error(這個必須使用長連接纔會復現)!

$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之後,去遍歷這個列表處理返回數據就即可。


由於redis針對multi之後的請求都是隊列並沒有執行,所以客戶端可以使用discard命令來清空這個隊列,同時客戶端也應該將之前綁定的函數列表一併清除,可是phpredis對於discard的處理僅僅是發送了discard命令到redis服務端,卻沒有清空處理函數列表。只是在下一次執行multi的時候,他僅僅是將這個處理函數列表中一個叫current的指針值爲NULL(這個列表是一個單向連表,head表示頭元素,current表示尾元素),可是他忽略了head,因爲函數列表一旦進入處理是從head開始,只有需要新加函數到列表的時候纔會用到current。所以在phpredis裏面,執行一條命令,再discard,再執行兩條命令之後,這個處理函數列表裏只有一個函數(正確應該是兩個,而且還是最開始加的那一個,後面加的兩個,不翼而飛了。。),處理函數與返回數據不配對,協議自然也就亂了,這就是protocol error報錯的由來....


本想着提個bug給phpredis就好了,結果提了也不見他們修改,於是就自己改了,修改的地方:

https://github.com/scgywx/phpredis/commit/3f05eb7acd3b7f64d1f4f857767b5dd74d585cd9


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