1. 数据结构的使用
1.1 时间复杂度
谈到数据结构,一定会谈到 “时间复杂度”。
在计算机科学中,算法的时间复杂度是一个函数,它定性描述该算法的运行时间。
时间复杂度常用大O符号表述。
时间复杂度可被称为是渐近的,即考察输入值大小趋近无穷时的情况。
在 Redis 中,用它来表示,基于我们处理的数据的数量,命令执行的速度将会如何。
O(1)
最快的应该是 O(1) ,一个常量。sismember 命令,用于查询一个值是否属于一个集合,就是 O(1)。
sismember 是个强力的命令,很大一个原因就是快。Redis 中的大多数命令都是 O(1)。
O(log(N))
O(log(N)), 是第二快的,因为它需要扫描的区间范围越来越小。通过使用这种类型的切分和处理方法,一个非常大的集合仅需要做几次迭代就会被迅速的分解。
zadd 是一个 O(log(N)) 命令,N 表示在有序集合中的元素个数。
O(N)
O(N) 在表中查找没有做索引的列就是一个 O(N) 操作。就像用 ltrim 命令一样。但是,在 ltrim 中,N 不是列表的元素个数,而是要移除的元素的个数。
O(log(N)+M)
zremrangebyscore 用来从有序列表中删除那些权重在最小值和最高值之间的元素,拥有复杂度 O(log(N)+M)。
O(N+M*log(M))
sort 命令,的复杂度 O(N+M*log(M))
其他
还有两个比较常用的是 O(N^2) 和 O(C^N)。N 越大,性能越差。Redis 没有这种复杂度的命令
1.2 多 Key 查询
假如说,一个用户叫 users:leto 的用户,他的 基本信息 有 '{"id": 9001, "email": "leto@dune.gov", ...}'
如果,想通过用户 id, 和 email 都能快速检索到要怎么做?
方案1:使用 string 结构,存两份
即用 id ,和 email 作为键,存储两次 。示例:
set users:leto@dune.gov '{"id": 9001, "email": "leto@dune.gov", ...}'
set users:9001 '{"id": 9001, "email": "leto@dune.gov", ...}'
当用户很多时,这样的方式绝对是个噩梦,并且它们会占用你两倍内存空间。
方案2:使用哈希结构,用字段作为伪二阶索引
先用 string 结构存储基本信息,再用哈希结构 将 email 作为字段,id 作为值
set users:9001 '{"id": 9001, "email": "leto@dune.gov", ...}'
hset users:lookup:email leto@dune.gov 9001
那么,通过 id 获取 用户,我们可以用普通的 get:
get users:9001
想要通过 email 来获取用户,我们用 hget 配合 get (Ruby):
id = redis.hget('users:lookup:email', 'leto@dune.gov')
user = redis.get("users:#{id}")
这样的方式很节约 key 的数量。使用 伪二阶索引 建立了 映射。
1.3 引用和索引(References and Indexes)
上面的查询优化的例子,其实是 手工维护你的 value 之间的索引和
引用。这样的方式很常见。
在集合中维护索引:
sadd friends:leto ghanima paul chani jessica
上面标示了 leto 有 ghanima ,paul ,chani等几个 朋友。而 ghanima 这样的又可以指向具体的 用户信息。
再跟踪一下反向关系:
sadd friends_of:chani leto paul
上面标示了 chani 是 leto, paul的 朋友。
这些额外的索引值的处理和内存开销会让人吓到,我们通过使用额外的查询次数降低性能开销。其实关系型数据库也有一样的开销。
1.4 Round Trips and Pipelining
添加一个或多个记录
sadd 命令向集合中添加一个或多个记录:
sadd friends:vladimir piter
sadd friends:paul jessica leto "leto II" chani
接收多个参数
许多命令都可以接收一个或者多个参数,或者有一个带有多个参数的子查询
ids = redis.lrange('newusers', 0, 9)
redis.mget(*ids.map {|u| "users:#{u}"})
Redis 也支持管道
通常,一个客户端向 Redis 发送一个请求,然后在下次请求之前会一直等待返回。而用管道你可以发送一堆请求却不用等待它们的响应。这不单降低了网络开销,也在性能上有显著提高。
···
redis.pipelined do
9001.times do
redis.incr('powerlevel')
end
end
···
批处理会被管道加速。
1.5 事务(Transactions)
Redis 所有的命令都是原子性的,包括那些一次可以执行多项操作的命令也一样。此外,在使用多命令的时候,Redis 支持事务。
Redis 确实是单线程的,这就是为什么每个命令都是原子性的原因。
一次只能执行一个命令
- incr 实际上是一个 get 后面跟个 set
- getset 设置一个新值之后返回原值
- setnx 首先检查 key 是否存在,当它不存在的时候设值
事务的使用:
- 首先你需要 multi 命令, 作为开始
- 然后接下来是你希望作为一组事务执行的所有命令
- 最后用 exec 来实际执行命令,或者用 discard 来放弃取消执行所有的命令
示例:
multi
hincrby groups:1percent balance -9000000000
hincrby groups:99percent balance 9000000000
exec
** watch**
redis.watch('powerlevel')
current = redis.get('powerlevel')
redis.multi()
redis.set('powerlevel', current + 1)
redis.exec()
如果另一个客户端在调用 watch 之后,改变了 powerlevel 的话,我们的事务将会失败。
1.6 Keys Anti-Pattern
keys 命令。该命令通过指定模式返回所有匹配的 key。这个命令看起来在某些情况下很适用,但是它绝对不应当用在产品代码中。因为它为了查找匹配的 key 会对所有的 key 做一个线性扫描,它很慢。
于是,考虑用哈希结构。就像我们可以用哈希来暴露二级索引那样,所以我们也可以用它来组织我们的数据:
hset bugs:1233 1 '{"id":1, "account": 1233, "subject": "..."}'
hset bugs:1233 2 '{"id":2, "account": 1233, "subject": "..."}'
为了取得一个账户下的所有 bug 标识符,我们只需要调用 hkeys bugs:1233。要删除指定 bug 我们可以 hdel bugs:1233 2,要删除账户的话我们可以通过 del bugs:1233 来删除 key。
END
网友评论