python redis 模块有小小的bug
今天用python写robot framework扩展库时候才发现,原来安装的python redis模块存在一个小小的bug。
首先说一下,我的redis模块是怎么安装的:
通过 pip install redis 安装依赖库
python 脚本:
import redis
rds = redis.Redis('127.0.0.1',22555)
rds.set(None,'Test')
rds.set('None','tet')
rds.get(None)
rds.get('None')
结果你会发现一个奇怪的现象是:
rds.get('None') 返回 tet ; rds.get(None) 也是返回tet 也就是说,key为None和'None'是 没区别的。
但按道理来说不应该这样,我们知道None 类型是NoneType, 'None'类型是str (类型 具体可以通过print type(None) 和print type('None')查看)
因此这应该是两个key 所以很明显该模块有个小小问题:
我们怎么区分开这两个key呢?
首先看看python redis模块是怎么实现的。首先我们找出我们安装的redis python 模块,我的是在$HOME/app/python-2.7.3/lib/python-2.7/site-packages/redis 这个目录。
这个目录有好几个文件 client.py 、 connection.py、 exceptions.py 等好几个文件 我们只关注client.py 和 connection.py 这两个文件
client 这个文件是我们用到的客户端主要实现的文件,而connection 是把已经封装好的redis 命令发送到服务器,并把结果集返回。
client 里面有好几个类,我们常用的redis命令封装在 StrictRedis 和 Redis 这两个类中,Redis 又是StrictRedis 的子类。因此我们应该把关注点重点放在StrictRedis 和 Redis 两个类上。我们要一步步追踪,究竟是如何把命令封装成redis 命令的。
我们看一个很简单的命令--set。set 命令封装在 client 模块的 StrictRedis类中。看源码:
def set(self, name, value, ex=None, px=None, nx=False, xx=False):
pieces = [name, value]
if ex:
pieces.append('EX')
if isinstance(ex, datetime.timedelta):
ex = ex.seconds + ex.days * 24 * 3600
pieces.append(ex)
if px:
pieces.append('PX')
if isinstance(px, datetime.timedelta):
ms = int(px.microseconds / 1000)
px = (px.seconds + px.days * 24 * 3600) * 1000 + ms
pieces.append(px)
if nx:
pieces.append('NX')
if xx:
pieces.append('XX')
return self.execute_command('SET', *pieces)
__setitem__ = set
从这里面还看不出什么 ,我们注意到这一方法重点是处理后面的默认参数再前面调用究竟有没有传递,其实可以忽略,这不是我们关注点。我们注意到这个方法最后是交给execute_command去处理的,因此我们找出 execute_command的源码:
def execute_command(self, *args, **options):
"Execute a command and return a parsed response"
pool = self.connection_pool
command_name = args[0]
connection = pool.get_connection(command_name, **options)
try:
connection.send_command(*args)
return self.parse_response(connection, command_name, **options)
except ConnectionError:
connection.disconnect()
connection.send_command(*args)
return self.parse_response(connection, command_name, **options)
finally:
pool.release(connection)
重点在try 里面 connection.send_command(*args)。还是没找到我们想要的,继续找到send_command 的源代码:
def send_command(self, *args):
"Pack and send a command to the Redis server"
self.send_packed_command(self.pack_command(*args))
不多说 继续找 pack_command 源代码(在 connection 模块):
def pack_command(self, *args):
"Pack a series of arguments into a value Redis command"
output = BytesIO()
output.write(SYM_STAR)
output.write(b(str(len(args))))
output.write(SYM_CRLF)
for enc_value in imap(self.encode, args):
output.write(SYM_DOLLAR)
output.write(b(str(len(enc_value))))
output.write(SYM_CRLF)
output.write(enc_value)
output.write(SYM_CRLF)
return output.getvalue()
感觉是不是迷糊了? 哈哈
其实 就是这里我们找对了!!!!!
好吧,看这段代码之前,我们首先了解一下 redis协议的基本知识。
See the following example:
*3
$3
SET
$5
mykey
$7
myvalue
This is how the above command looks as a quoted string, so that it is possible to see the exact value of every byte in the query, including newlines.
"*3\r\n$3\r\nSET\r\n$5\r\nmykey\r\n$7\r\nmyvalue\r\n"
看上面 *3 表示 这个redis 命令有三个字段 command_name key val(set key1 4),$3 z指的是command_name的长度,strlen('SET') = 3 ,$5表示key 长度 strlen('mykey') = 5, 同理 $7 表示 strlen('myvalue') = 7
再回头看看我们pack_command 的源代码是不是一目了然了?
output.write(b(str(len(args))))
我们封装的redis指令是通过参数传递进来的 *args ,例如:print args[0] # SET , print args[1] # mykey, print args[2] # myvalue
对比我们上面介绍的redis 协议知道吧, str(len(args)) 就是上面的*3 ;
for enc_value in imap(self.encode, args):
这里就是对args里面的参数作一定转换,同样我们找到self.encode源代码:
def encode(self, value):
"Return a bytestring representation of the value"
if isinstance(value, bytes):
return value
if isinstance(value, float):
value = repr(value)
if not isinstance(value, basestring):
value = str(value)
if isinstance(value, unicode):
value = value.encode(self.encoding, self.encoding_errors)
return value
注意到没有,if not isinstance(value, basestring):
value = str(value)
这里意思就是说,如果不是str类型,会把该值强制转换为str类型。原来这样,怪不得我们说set(None,'Test') 和set('None','Test') 是一回事,其实,不应该这样。
既然这样我们稍作修改:
if type(value) == 'None':
value = value
else:
value = str(value)
这样修改就可以了吗?。。。当然不是啦!!。。。我们接下来看pack_command的代码,
for enc_value in imap(self.encode, args):
output.write(SYM_DOLLAR)
output.write(b(str(len(enc_value))))
output.write(SYM_CRLF)
output.write(enc_value)
output.write(SYM_CRLF)
return output.getvalue()
我们上面的value不再强制转换为str类型后,那么output.write(b(str(len(enc_value)))) 就有可能报异常,当enc_value 为None时len 函数报异常,因此我们要避免这种异常,可以这样改:
if enc_value == None:
output.write(b(str(len(enc_value))))
else:
output.write(b(str(0)))
同样下面的语句我们也稍微修改一下:
output.write(enc_value)
修改为:
if enc_value != None:
output.write(enc_value)
好 大工告成!!!!!!
我们测试一下 是不是这样呢? 同样使用之前的脚本:
import redis
rds = redis.Redis('127.0.0.1',22555)
rds.set(None,'Test')
rds.set('None','tet')
rds.get(None)
rds.get('None')
结果返回 rds.get(None) # "Test" rds.get('None') # "tet"
如果,你自己开发一个能兼容redis协议的并且还有属于自己扩展命令的存储引擎,我们其实也是可以通过修改redis 模块使之成为我们的客户端的,最方便的是继承Redis 类,当然这是后话。