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


你可能感兴趣的:(redis报protocol error的真正原凶)