前段时间写了个文章详细描述了在什么场景下会出现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队列