使用pipeline来提高性能
应该使用pipeline来将多个请求组合在一起,一次性在发送给服务器,并返回结果。
import redis
from redis.client import Pipeline
from typing import List
connection = redis.StrictRedis(port=16379, decode_responses=True)
pipe: Pipeline = connection.pipeline()
pipe.set(...) #1
pipe.get(...) #2
pipe.sadd(...) #3
result:List = pipe.execute()
上述代码执行后, #1, #2, #3的结果依次保存在数组result中。如果不使用pipeline,上述#1, #2, #3将分为三次网络传输完成,占用3个RTT的时间,使用pipeline将使得这一时间减少到1个RTT时间。
注意pipeline只是将多个命令组合在一起发送给服务器,且结果也一次性返回,但并不意味着这些命令的执行是原子性的。要保证其原子性,应该使用事务。
pipeline也有美中不足的地方,就是你没有办法利用中间结果进行运算。比如如果#2命令依赖于#1的输出结果,那么这两个操作是无法pipeline在一起的。这时应该使用客户端脚本。
使用迭代和batch操作来提高性能
如果你要一次性地操作比如说1B的数据,那么很有可能你不会有这么多内存来存储返回的keys,这种情况下,应该使用scan操作。比如我们要删除redis中的所有以user:开头的key:
import redis
r = redis.StrictRedis(host='localhost')
for key in r.scan_iter("user:*"):
r.delete(key)
这样内存问题解决了,但是有点慢。
如果要一次操作很多数据,比如说100k以上,应该使用批量操作:
import redis
from itertools import izip_longest
r = redis.StrictRedis(host='localhost', port=6379, db=0)
# iterate a list in batches of size n
def batcher(iterable, n):
args = [iter(iterable)] * n
return izip_longest(*args)
# in batches of 500 delete keys matching user:*
for keybatch in batcher(r.scan_iter('user:*'),500):
r.delete(*keybatch)
这段代码来自于Get all keys in Redis database with python,根据作者的测试,使用批量操作(且batch size为500),最高将提高5倍的速度。
需要使用asyncio吗?
由于python的执行是单线程的,所以在python中,一旦涉及到IO操作,我们都尽可能地使用asyncio来完成。但在访问redis时,是否要使用asyncio,要具体分析。一般来说,我们会把redis服务器部署在离客户端很近的地方,甚至可能就在本机,由于RTT很小,而redis本身性能很高,延时很低,因此完全可以不用异步编程模型。毕竟对redis的使用会很频繁,python的asyncio模型中加入这么多请求后,对其调度性能也是一种考验。两相折冲,使用asyncio的性能并不一定高。也许这也是python下没有特别好的asyncio模型的redis客户端的原因。但是,上述结论是基于我们使用redis的场景是高频,小数据量的情况。如果要与redis交换大量数据,显然还是要使用异步,以免因与redis的通信阻塞程序运行。关于这一点,我并没有做测试,也没有找到合适的benchmark文章。不过看到知乎文章aredis —— 一款高效的异步 redis 客户端也有类似的观点。
如何存储复杂的数据结构?
redis通过set/get, hset/hmset/hmget等命令提供了嵌套级别为0~1级的各种操作。但有时候我们需要读写更复杂的数据结构,我们应该如何拓展信息嵌套的层次呢?
首先,可以使用多个数据库来提供第一层的嵌套(或者第一级的名字空间)。这是通过建立连接时指定db索引来实现的:
import redis
r = redis.StrictRedis(host='localhost', port=6379, db=0)
# 通过r发生的操作都写在db=0这个数据库中。
redis数据库通过整数索引,而不是更易理解的字符串名字来标识。因此,在正式的工程中,你应该定义一些常量,通过常量名来区分各个数据库的作用。不同的数据库是允许存在相同的key的。现在,假设我们有一个CRM系统,则相应的用户数据,我们可以分别保存在vendor(供货商)和customer(客户)数据库下。
redis提供了hash, list, set这样的容器,从而提供了另一个级别的嵌套。比如我们的客户数据可能组成如下:
id -> {
name: 'john',
mobile: 13400001234,
orders: [oid1, oid2, ...]
}
这样我们可以直接通过hmset来更改用户的名字、电话等数据。如果要操作他的订单数据呢?当然我们可以这样:
import redis
r = StrictRedis(...)
id = 123456 # id of user 'john'
orders = r.hget(id, orders)
# modify orders
orders.append('1111-1121')
r.hset(id, orders)
但这样操作的效率不高,因为我们只希望给orders增加一笔订单,没有必要把之前的订单数据都取回来(特别是如果用户订单数据很长的话,那就更是浪费)。但是redis并不提供对这一嵌套级别数据的直接操作。
有两个办法来解决这一问题(如何更高效地修改订单数据)。一是使用服务器脚本。或者,我们在key上做文章。比如对每一级嵌套的容器类型(hash, list, set),我们通过给它一个多级的key来将其从内部嵌套中提取出来。以上面的用户订单数据为例,它是一个两层的嵌套,超过了redis命令可以直接运算的最大深度,因此我们可以这样改造存储在redis中的数据:
id -> {
name: 'john',
mobile: 13400001234
...
}
id:orders -> [oid1, oid2, oid3, ...]
# 还可以通过类似的方法将更深层的数据“扁平化”