文章目录
- 1 初识Redis
- 2 Redis基本命令
1 初识Redis
1.1 Redis简介
Redis 是一个速度非常快的非关系型数据库(non-relational database),它可以存储键(key)和五种不同类型的值(value)之间的映射(mapping),可基于内存存储亦可持久化到硬盘的日志型,Key-Value
数据库。
1.1.1 Redis与其他数据库的对比
如果你使用过关系型数据库,例如:Mysql,那么你肯定写过关联两张表数据的查询语句。而 Redis 属于 NoSQL,它不使用表,也不会预定义数据模式或强制用户对 Redis 的各种数据进行关联。
NoSQL(Not Only SQL)
意指“不仅仅是SQL”,其泛指非关系型数据库,主要分为四类:键值(Key-Value)存储数据库,列存储数据库,文档型数据库,**图形(Graph)**数据库。
Redis 也经常与高性能键值缓存服务器 memcached 做比较:两者均可用于存储键值映射,性能相差也甚少,但 Redis 能存储除普通字符串值之外的四种数据结构,而 memcached 只能存储普通的字符串值。这些不同使得 Redis 能够解决更为广泛的问题,而且既能作为主数据库使用,也可以作为辅助数据库使用。
我们通过一张表来对比常用的数据库与缓存服务器:
名称 | 类型 | 数据存储选项 | 查询类型 | 附加功能 |
---|---|---|---|---|
Redis | 基于内存的非关系型数据库 | 字符串、列表、集合、哈希、有序集合 | 针对数据类型有专属命令,另有批量操作和不完全的事务支持 | 发布与订阅、复制、持久化、脚本扩展 |
memcached | 基于内存的键值缓存 | 键值映射 | 创建、读取、更新、删除等 | 多线程支持 |
MySQL | 关系型数据库 | 数据表、视图等 | 查询、插入、更新、删除、内置函数、自定义存储过程等 | 支持 ACID 性质、复制等 |
MongoDB | 基于硬盘的非关系型文档存储数据库 | 无 schema 的 BSON 文档 | 创建、读取、更新、删除、条件查询等 | 复制、分片、空间索引等 |
1.1.2 Redis的特性
由于 Redis 是内存型数据库,在使用之前就要考虑当服务器被关闭时,服务器存储的数据是否能保留。Redis 拥有两种不同形式的持久化方法,都可以用紧凑的格式将数据写入硬盘:
RDB
持久化- 在指定的时间间隔内生成数据集的时间点快照
AOF
持久化- 记录服务器执行的所有写操作命令
- 新命令会被追加到文件的末尾
- 在服务器启动时,通过重新执行这些命令还原数据集
除此之外,为了扩展 Redis 的读性能,并为 Redis 提供故障转移支持,Redis 实现了主从复制特性:
- 执行复制的从服务器连接主服务器
- 接收主服务器发送的初始副本
- 接收主服务器执行的所有写命令
- 在从服务器上执行所有写命令,实时更新数据库
- 读命令可以向任意一个从服务器发送
1.2 快速安装 Redis 与 Python
为了避免安装到旧版 Redis 的问题,我们直接使用源码编译安装 Redis,首先你需要获取并安装 make 等一系列构建工具:
$ sudo apt-get update
$ sudo apt-get install make gcc python-dev
构建工具安装完毕后,你需要执行以下操作:
- 从 https://redis.io/download 下载最新的稳定版本 Redis 源码
- 解压源码,编译、安装并启动 Redis
- 下载并安装 Python 语言的 Redis 客户端库
其中,安装 Redis 的过程如下:
~:$ wget -q http://download.redis.io/releases/redis-5.0.0.tar.gz
~:$ tar -xzf redis-5.0.0.tar.gz
~:$ cd redis-5.0.0
# 注意观察编译消息,最后不应该产生任何错误(`Error`)
~/redis-5.0.0:$ make
# 注意观察安装消息,最后不应该产生任何错误(`Error`)
~/redis-5.0.0:$ sudo make install
# 启动 Redis 服务器,注意通过日志确认 Redis 顺利启动
~/redis-5.0.0:$ redis-server redis.conf
除了上述的启动 Redis 服务器方式,你还可以通过 Redis 默认的配置在后台启动它(常用启动方式):
$ redis-server &
因为近几年发布的 Ubuntu 和 Debian 都预装了 Python 2.6 或 Python 2.7,所以你不再需要花时间去安装 Python。你可以通过一个名为 setuptools
的辅助包更方便的下载和安装 Redis 客户端:
~:$ sudo python -m easy_install redis hiredis
这里的 redis
包为 Python 提供了连接 Redis 的接口,hiredis
包则是可选的,它是一个使用 C 语言编写的高性能 Redis 客户端。
1.3 Redis数据结构简介
Redis 的五种数据结构分别是:
- 字符串(
STRING
) - 列表(
LIST
) - 集合(
SET
) - 哈希(
HASH
) - 有序集合(
ZSET
)
ZSET
可以说是 Redis 特有的数据结构,我们会在之后的实训中详细介绍它,在本实训中,我们只简要介绍他们的功能和小部分命令。
他们的存储的值如下:
结构类型 | 存储的值 |
---|---|
STRING | 字符串、整数或浮点数 |
LIST | 一个链表,上面的每个节点都是一个字符串 |
SET | 包含若干个字符串的无序集合,且集合中的元素都是唯一的 |
HASH | 包含键值对的无序散列表 |
ZSET | 成员中的字符串与分值的有序映射,其排序由分值决定 |
在安装完 Redis 并启动了 redis-server
后,我们可以使用 redis-cli
控制台与 Redis 进行交互,其启动方式是在终端中输入:
$ redis-cli
其会默认连接本机 6379
端口启动的 Redis 服务器,接下俩你可以使用它来体验 Redis 各种数据结构和其命令的使用。
1.3.1 Redis中的字符串
STRING
拥有一些和其他键值存储相似的命令,比如 GET
(获取值),SET
(设置值),**DEL
(删除值)**等,例如:
$ redis-cli
redis-cli 127.0.0.1:6379> set hello redis
OK
redis-cli 127.0.0.1:6379> get hello
"redis"
redis-cli 127.0.0.1:6379> del hello
(integer) 1
redis-cli 127.0.0.1:6379> get hello
(nil)
其中:
SET
命令的第一个参数是键(Key)
,第二个参数是值(Value)
- 尝试获取不存在的键时会得到一个
nil
1.3.2 Redis中的列表
就像前面所说的,Redis 中的列表是一个“链表”,这和大多数编程语言相似。所以他们的操作也十分相似:
LPUSH
命令可用于将元素推入列表的左侧RPUSH
命令可将元素推入列表的右侧LPOP
和RPOP
就分别从列表的左侧和右侧弹出元素LINDEX
可以获取指定位置上的元素LRANGE
可以获取指定范围的全部元素
我们通过 redis-cli
来亲自体验:
redis 127.0.0.1:6379> rpush testlist item
(integer) 1
redis 127.0.0.1:6379> rpush testlist item2
(integer) 2
redis 127.0.0.1:6379> rpush testlist item
(integer) 3
redis 127.0.0.1:6379> lrange testlist 0 -1
1) "item"
2) "item2"
3) "item"
redis 127.0.0.1:6379> lindex testlist 1
"item2"
redis 127.0.0.1:6379> lpop testlist
"item"
redis 127.0.0.1:6379> lrange testlist 0 -1
1) "item2"
2) "item"
testlist
为列表的键
我们可以看出,在列表中,元素可以重复出现。
1.3.3 Redis中的集合
集合和列表的区别就在于:列表可以存储多个相同的字符串,而集合通过散列表来保证存储的字符串都是各不相同的(这些散列表只有键,而没有对应的值)。
由于集合是无序的,所以我们只能通过统一的 SADD
命令将元素添加到集合中,SREM
命令将元素从集合中移除。你还可以通过:
SMEMBERS
命令获取到集合中的所有元素SISMEMBER
命令来判断一个元素是否已存在在集合中
redis 127.0.0.1:6379> sadd testset item
(integer) 1
redis 127.0.0.1:6379> sadd testset item2
(integer) 1
redis 127.0.0.1:6379> sadd testset item
(integer) 0
redis 127.0.0.1:6379> smembers testset
1) "item"
2) "item2"
redis 127.0.0.1:6379> sismember testset item3
(integer) 0
redis 127.0.0.1:6379> sismember testset item
(integer) 1
redis 127.0.0.1:6379> srem testset item2
(integer) 1
redis 127.0.0.1:6379> srem testset item2
(integer) 0
redis 127.0.0.1:6379> smembers testset
1) "item"
上面示例的集合中包含的元素少,所以执行 SMEMBERS
命令没有问题,一旦集合中包含的元素非常多时,SMEMBERS
命令的执行速度会很慢,所以要谨慎的使用这个命令。
1.3.4 Redis中的哈希
哈希可以存储多个键值对之间的映射。和字符串一样,哈希存储的值既可以是字符串又可以是数字值,并且可以对数字值进行自增/自减操作。
哈希就像是一个缩小版的 Redis,有一系列命令对哈希进行插入、获取、删除:
redis 127.0.0.1:6379> hset testhash key1 value1
(integer) 1
redis 127.0.0.1:6379> hset testhash key2 value2
(integer) 1
redis 127.0.0.1:6379> hset testhash key1 newvalue
(integer) 0
redis 127.0.0.1:6379> hgetall testhash
1) "key1"
2) "newvalue"
3) "key2"
4) "value2"
redis 127.0.0.1:6379> hdel testhash key2
(integer) 1
redis 127.0.0.1:6379> hget testhash key1
"newvalue"
redis 127.0.0.1:6379> hgetall testhash
1) "key1"
2) "newvalue"
hset
用于插入元素
- 第一个参数为该哈希的键名,如果该哈希不存在,则创建一个
- 第二个参数为哈希中的域名
- 如果不存在,则创建该域,并与第三个参数的值进行映射
- 如果存在,则使用第三个参数更新该域的值
- 第三个参数为哈希中的值
hgetall
会获取到该哈希的所有域-值对hget
用于获取哈希中的某一个域hdel
用户删除哈希中的某一个域
1.3.5 Redis中的有序集合
有序集合和哈希一样,也是存储键值对。
只是有序集合的键被称为成员(member),每个成员都是唯一的,有序集合的值则被称为分值(score),这个分值必须为浮点数。所以有序集合既可以通过成员访问元素,也可以通过分值来排序元素。
我们可以通过:
ZADD
命令将带有指定分值的成员添加到有序集合中ZRANGE
命令根据分值有序排列后的集合获取到指定范围的元素ZRANGEBYSCORE
命令获取指定分值范围内的元素ZREM
命令从有序集合中删除指定成员
我们也可以在 redis-cli
中验证上述命令的功能:
redis 127.0.0.1:6379> zadd testzset 100 member1
(integer) 1
redis 127.0.0.1:6379> zadd testzset 200 member0
(integer) 1
redis 127.0.0.1:6379> zrange testzset 0 -1 withscores
1) "member1"
2) "100"
3) "member0"
4) "200"
redis 127.0.0.1:6379> zrangebyscore testzset 0 150 withscores
1) "member1"
2) "100"
redis 127.0.0.1:6379> zrem testzset member1
(integer) 1
redis 127.0.0.1:6379> zrange testzset 0 -1 withscores
1) "member0"
2) "200"
1.4 使用 Python 与 Redis 交互
1.4.1 如何使用 Python 连接 Redis
使用 easy_install
包安装了 redis
包后,可以使用以下两种方法连接 Redis :
-
方法1:
# 导入 redis 模块 import redis # 创建 redis 客户端 conn = redis.Redis() ... # 使用完资源之后删除客户端 conn del conn
-
方法2:
# 导入 redis 模块 import redis # 创建连接池 pool = redis.ConnectionPool(host='127.0.0.1', port=6379, decode_responses=True) # 创建客户端并连接到 Redis r = redis.Redis(connection_pool=pool)
两种方法的对比如下:
- 方法 1:需要在使用完该客户端后手动删除客户端,以避免创建多个连接
- 方法 2:
- 使用了连接池总揽多个客户端与服务端的连接
- 不需要手动删除客户端
- 同时有效的减少多个客户端连接的损耗
所以我们在实际开发中使用第二种方法较多。
在创建了客户端之后,你就可以使用 coon
或 r
这个客户端来进行 Redis 操作了。
1.4.2 通过客户端对 Redis 的数据进行操作
通过客户端对 Redis 的数据进行操作和直接在 Redis 中的操作命令基本相同。只是在客户端中操作如下,要在命令前加上客户端的名字和.(假设使用方法2创建客户端r):
# 使用 SET 命令设置一个字符串键
r.set("test", "hello")
# 显示字符串键 test 的值
print(r.get("test"))
1.5 使用Python+Redis实现文章投票网站后端功能
1.5.1 实现投票功能
实现投票功能,要注重文章的时效性与投票的公平性,所以需要给投票功能加上一些约束条件:
- 文章发布满一个星期后,不再允许用户对该文章投票
- 一个用户对一篇文章只能投一次票
所以我们需要使用:
- 一个有序集合
time
,存储文章的发布时间 - 一个集合
voted:*
,存储已投票用户名单- 其中 * 是被投票文章的 ID
- 一个有序集合
score
,存储文章的得票数
ONE_WEEK_IN_SECONDS = 7 * 24 * 60 * 60
def article_vote(r, user_id, article_id):
# 使用 time.time() 获取当前时间
# 减去一周的秒数,从而获取一周前的Unix时间
cutoff = time.time() - ONE_WEEK_IN_SECONDS
if r.zscore('time', article_id) < cutoff:
return
if r.sadd('voted:' + article_id, user_id):
r.zincrby('score', article_id, 1)
当用户尝试投票时,使用 ZSCORE
命令读取 time
有序集合,得到这篇文章的发布时间,再判断文章的发布时间是否超过一周。
ZSCORE
命令的语法如下:
r.zscore(key, member)
key
:是有序集合的键名member
:是有序集合中的某个成员
若未超过,则使用 SADD
命令尝试将用户追加到这篇文章的已投票用户名单中,如果添加成功,则说明该用户未投过票。
SADD
命令的语法是:
r.sadd(key, member)
key
:是集合的键名member
:是要添加进集合的元素
由于集合中的元素是唯一的,所以 sadd
函数会根据 member
是否存在在集合中做出不同返回:
- 若该元素不存在在集合中,返回
True
- 若该元素已存在在集合中,返回
False
所以返回为 True
时使用 ZINCRBY
命令来为文章的投票数加 1。
zincrby
函数语法如下:
r.zincrby(key, member, increment)
key
:是有序集合的键名member
:是有序集合中要增加分值的成员increment
:是要增加的分值
1.5.2 创建文章数据
现在系统中还缺少文章数据,所以我们要提供一个创建文章的函数,并把文章数据存储到 Redis 中。创建文章的步骤如下:
- 创建新的文章 ID
- 将文章作者加入到这篇文章的已投票用户名单中
- 存储文章详细信息到 Redis 中
- 将文章的发布时间和初始投票数加入到
time
和score
两个有序集合中
def post_article(r, user, title, link):
# 创建新的文章ID,使用一个整数计数器对 article 键执行自增
# 如果该键不存在,article 的值会先被初始化为 0
# 然后再执行自增命令
article_id = str(r.incr('article'))
voted = 'voted:' + article_id
r.sadd(voted, user)
r.expire(voted, ONE_WEEK_IN_SECONDS)
now = time.time()
article = 'article:' + article_id
r.hmset(article, {
'title': title,
'link': link,
'poster': user,
})
r.zadd('score', article_id, 1)
r.zadd('time', article_id, now)
return article_id
将文章作者加入已投票用户名单中和之前一样,这里不再赘述,但在这里我们需要为这个已投票用户名单设置一个过期时间,让它在一周后**(到期后)自动删除**,减少 Redis 的内存消耗。
为键设置过期时间的命令是:
r.expire(key, seconds)
key
:要设置过期时间的键名seconds
:过期时间的长度(单位:秒)
这里我们要设置的时间是一周,所以我们可以使用上面定义好的全局变量 ONE_WEEK_IN_SECONDS
。
接下来要存储文章详细信息了,前面介绍过 hset
可以执行单个字段(域)的设置,这里我们使用 **hmset**
一次性设置多个字段(域),其语法如下:
r.hmset(key, {field: value, [field: value ...]})
我们可以使用 Python 的散列来一次性存储多个字段(域)到 Redis,只需要将整个散列当作 key 对应的值通过 hmset 函数设置进去就行。
最后,将初始投票数和创建时间设置到 score
和 time
中都可以通过 ZADD
命令来实现:
r.zadd(key, member, score)
key
:有序集合的键名member
:要加入有序集合的成员score
:该成员的分值
这里需要注意的是,因为该篇文章的作者已经被加入到该文章已投票用户名单中,为了保持数据一致性,我们需要将文章的初始投票数设为 1。
1.5.3 对文章进行排序
实现了文章投票和创建文章功能,接下来我们就需要将评分最高的文章和最新发布的文章从 Redis 中取出了。
- 首先我们要根据排序方式的不同:
- 按评分排序,则从
score
有序集合中取出一定量的文章 ID(score
有序集合存放文章ID和对应的投票数) - 按时间排序,则从
time
有序集合中取出一定量的文章 ID(time
有序集合存放文章ID和对应的发布时间)
- 按评分排序,则从
- 构成一个有序文章信息列表,每个元素都:
- 使用
HGETALL
命令,取出每篇文章的全部信息
- 使用
def get_articles(r, start, end, order='score'):
ids = r.zrevrange(order, start, end)
articles = []
for id in ids:
article_data = r.hgetall(id)
article_data['id'] = id
articles.append(article_data)
return articles
这里因为需要对有序集合进行排序,所以我们在取出文章 ID 时需要使用到 ZREVRANGE 命令,以分值从大到小的排序方式取出文章 ID。
ZREVRANGE
命令的语法是:
r.zrevrange(key, start, stop)
key
:有序集合的键名start
:开始的数组下标stop
:结束的数组下标
得到多个文章 ID 后,我们还需要根据每一个文章 ID 获取文章的全部信息,这时就需要使用到 HGETALL
命令,它的语法如下:
r.hgetall(key)
key :哈希的键名
我们取出文章的全部信息后,还为文章信息添加了一个字段 id。这是因为文章 ID 在 Redis 中是作为键名存储的,不在值当中,所以我们需要附加这个字段到文章信息中。
实现这些方法后,我们大体实现了一个文章投票的后端处理逻辑,能够为文章投票并能根据投票结果改变文章的排序情况。
2 Redis基本命令
2.1 常用字符串命令
Redis 的字符串可以存储三种类型的值:
- 整数
- 浮点数
- 字节串
取值范围说明
Redis 中整型数据的长度与系统字长一致(例如:32位系统,整型数据为32位有符号整数)
Redis 中浮点数的取值范围与精度都与双精度浮点数(double)一致
2.1.1 数值操作
所以针对存储整型和浮点型的字符串就有自增和自减操作。在需要的时候(例如下表的 INCRBYFLOAT
命令),Redis 还会将整数转换为浮点数。
对 Redis 字符串执行自增和自减的命令列表如下:
命令 | 用法 | 说明 |
---|---|---|
INCR | INCR key | 将 key 存储的值加上 1 |
DECR | DECR key | 将 key 存储的值减去 1 |
INCRBY | INCRBY key increment | 将 key 存储的值加上 increment |
DECRBY | DECRBY key decrement | 将 key 存储的值减去 decrement |
INCRBYFLOAT | INCRBYFLOAT key increment | 将 key 存储的值加上浮点数 increment |
注意:
INCRBYFLOAT
只能在 Redis 版本 >= 2.6 时可用
当用户将一个值存储到 Redis 字符串中,Redis 会检测这个值是否可以被解释(interpret)为十进制整数或者浮点数。如果可以,则允许用户对该字符串进行自增和自减操作。
在前面也提到过,如果用户对一个不存在的键或者一个保存了空串的键执行了自增或自减操作,Redis 都会:
- 先将该键的值置为 0
- 再对该键的值进行自增或自减操作
需要额外提到的是,Python 的 Redis 库在 incr(key, increment=1)
方法中同时实现了 INCR
和 INCRBY
命令,该方法的第二个参数 increment
是可选的,如果用户没有设置该值,就会使用其默认值 1。例如:
>>> conn = redis.Redis()
>>> conn.set('key', '1')
True
>>> conn.incr('key', 10)
11
>>> conn.decr('key', 5)
6
>>> conn.incr('key')
7
2.1.2 字节串操作
Redis 还可以对字节串的一部分内容进行读取/写入:
命令 | 用法 | 说明 |
---|---|---|
APPEND | APPEND key value | 将 value 追加到 key 键存储的值的末尾 |
GETRANGE | GETRANGE key start end | 获取 start 到 end 间的子串 |
SETRANGE | SETRANGE key offset value | 从 start 偏移量开始,将与 value 长度一致的子串设置为 value |
在使用
GETRANGE
读取字符串时,超出字符串末尾的数据会被视为空串;而在使用
SETRANGE
对字符串进行写入时,如果字符串当前长度不能满足写入要求,Redis 则会自动使用空字节将字符串扩展至所需的长度,然后再执行写入/更新操作。
值得一提的是,Redis 现在的 GETRANGE
命令是以前的 SUBSTR
命令改名而来的,所以,Python 客户端仍然可以使用 substr()
方法获取子串,例如:
>>> conn.set('string', 'hello')
True
>>> conn.append('string', ' educoder')
14L
>>> conn.substr('string', 0, 4)
'hello'
>>> conn.setrange('string', 0, 'ByeBye')
14
>>> conn.get('string')
'ByeByeeducoder'
>>> conn.getrange('string', 6, -1)
'educoder'
我们推荐使用 getrange()
方法来获取子串。在上述示例中,我们还将 end
下标传入了 -1
的值,这时 Redis 将会从起始偏移量读取到该字符串的末尾。
2.2 常用列表命令
Redis 提供了丰富的列表操作命令,从而使得列表的应用场景非常广泛,例如:存储任务队列,记录最近的操作/数据变化,作为日志收集器等。
首先我们介绍一些常用的列表命令:
命令 | 用法 | 说明 |
---|---|---|
LPUSH | LPUSH key value [value ...] | 将一个或多个 value 推入到列表的左侧 |
RPUSH | RPUSH key value [value ...] | 将一个或多个 value 推入到列表的右侧 |
LLEN | LLEN key | 返回列表 key 的长度 |
LREM | LREM key count value | 根据参数 count 的值,移除列表中与参数 value 相等的元素 |
加上我们在上一节中已经介绍过的弹出、获取元素等命令,就构成了最为常用的列表命令。使用 Python 交互的示例如下:
>>> conn.lpush('list', 'a', 'b', 'c', 'd')
4L
>>> conn.llen('list')
4
>>> conn.rpush('list', 'a', 'b', 'c', 'd')
8L
>>> conn.lrange('list', 0, -1)
['d', 'c', 'b', 'a', 'a', 'b', 'c', 'd']
>>> conn.lrem('list', 'b', 2)
>>> conn.lrange('list', 0, -1)
['d', 'c', 'a', 'a', 'c', 'd']
我们发现 lrem()
方法与 LREM
命令在参数的顺序上不完全一致,lrem()
方法将 count 参数放至最后,在 Python 的 Redis 客户端中,大多数命令中的数值型参数都被放到了最后,如果弄不清某个方法的参数,你可以到 redis客户端主页查看。
我们还可以在两个列表之间移动元素:
RPOPLPUSH source destination
RPOPLPUSH
命令在一个原子时间内,执行以下两个动作:
- 将列表 source 中的最右侧元素弹出,并返回给客户端。
- 将 source 弹出的元素推入到列表 destination 的最左侧
例如:
>>> conn.lpush('list2', '1', '2', '3')
>>> conn.rpoplpush('list', 'list2')
'd'
>>> conn.lrange('list', 0, -1)
['d', 'c', 'a', 'a', 'c']
>>> conn.lrange('list2', 0, -1)
['d', '3', '2', '1']
原子时间
不可再拆分的时间段
意指该操作执行时,不可被其他操作打断,也就是包含在一个原子时间内的若干操作要么都成功要么都失败
2.3 常用集合命令
与列表有序不同,Redis 中的集合以无序的方式存储多个互不相同的元素,用户可以快速的添加、删除和查找元素。Redis 提供了针对单个集合以及多集合间处理的命令:
命令 | 用法 | 说明 |
---|---|---|
SCARD | SCARD key | 返回集合 key 中元素的数量 |
SRANDMEMBER | SRANDMEMBER key [count] | 返回集合中的 1 或 count 个随机元素 |
SPOP | SPOP key | 移除并返回集合中的一个随机元素 |
SMOVE | SMOVE source destination member | 将 member 元素从 source 集合移动到 destination 集合 |
我们通过一些示例来展示上述命令的用法:
>>> conn.sadd('set', 'a', 'b', 'c', 'a')
>>> conn.scard('set')
3
>>> conn.srandmember('set')
'a'
>>> conn.spop('set')
'b'
>>> conn.smembers('set')
set(['a', 'c'])
>>> conn.smove('set', 'set2', 'a')
>>> conn.smembers('set2')
set(['a'])
Redis 中的许多命令都有着实际的应用场景,例如 SRANDMEMBER
命令从集合中随机选择一个元素并输出,在数据库层面就实现了随机数功能,避免用户将集合的全部成员取出后再随机选择,加快了效率,减少了开发人员的工作量。所以我们一直称 Redis 是基于实用主义的。
在 SMOVE
命令的示例中你也发现了,如果目的集合是不存在的,我们会先创建目的集合,再将成员从源集合中取出并放入目的集合。但如果指定的成员不存在于源集合中,则该命令不会继续执行。
Redis 集合还有更为强大的功能 —— 组合和关联多个集合:
命令 | 用法 | 说明 |
---|---|---|
SDIFF | SDIFF key [key ...] | 返回所有给定集合之间的差集 |
SINTER | SINTER key [key ...] | 返回所有给定集合的交集 |
SUNION | SUNION key [key ...] | 返回所有给定集合的并集 |
上述三个命令是差集,交集,并集运算的“返回结果”版本,同时 Redis 还提供了“存储结果”版本,你可以参考 Redis 命令参考 中的 SDIFFSTORE
,SINTERSTORE
和 SUNIONSTORE
命令。
2.4 常用哈希命令
Redis 的哈希允许用户将多个键值存储到一个 Redis 键中,使得哈希十分适合将一些相关的数据存储在一起。我们可以把这种数据看作是关系型数据库中的行。
常用的哈希命令包括之前介绍过的添加和删除域-值对命令、获取所有域-值对命令以及对域-值对的值进行自增/自减操作的命令:
命令 | 用法 | 说明 |
---|---|---|
HMSET | HMSET key field value [field value ...] | 同时将多个 field-value (域-值)对设置到哈希表 key 中 |
HMGET | HMGET key field [field ...] | 返回哈希表 key 中,一或多个给定域的值 |
HDEL | HDEL key field [field ...] | 删除哈希表 key 中的一或多个指定域 |
HLEN | HLEN key | 返回哈希表 key 中域的数量 |
在上一节中,我们使用过 HMSET
命令来批量的存储域-值对信息,实际上 HMSET
和 HMGET
命令既可以通过批量处理给用户带来便利,又减少了命令的调用次数,提升了客户端与 Redis 之间的通信次数,提高了 Redis 的性能:
>>> conn.hmset('hash', {'a': '1', 'b': '2', 'c': '3'})
True
>>> conn.hmget('hash', ['a', 'b'])
['1', '2']
>>> conn.hdel('hash', 'b', 'c')
2
>>> conn.hlen('hash')
1
在使用 HMGET
命令时,我们可以使用类似于上面数组形式传入参数,也可以类似于 HDEL
命令的多参数形式传入参数。而之前介绍的 HGET
和 HSET
命令则分别是 HMGET
和 HMSET
命令的单参数版本,每次执行时只能处理一个键值对。
Redis 哈希还支持一些更高级的批量操作:
命令 | 用法 | 说明 |
---|---|---|
HEXISTS | HEXISTS key field | 查看哈希表 key 中,给定域 field 是否存在 |
HKEYS | HKEYS key | 返回哈希表 key 中所有域 |
HVALS | HVALS key | 返回哈希表 key 中所有域的值 |
HINCRBY | HINCRBY key field increment | 为哈希表 key 中的域 field 的值加上 increment |
在哈希包含的值的体积都十分大时,我们应该使用 HKEYS
命令获取所有的域,再使用 HGET
一个个的从哈希中取出域的值,从而避免 Redis 因为一次性获取多个大体积的值而导致服务器阻塞。甚至,我们可以只获取必要的值来减少传输的数据量。
2.5 常用有序集合命令
有序集合与哈希类似,也存储着成员(member)和分值(score)之间的映射关系。Redis 为有序集合提供了分值处理命令,并能根据分值大小有序的排列成员:
命令 | 用法 | 说明 |
---|---|---|
ZCARD | ZCARD key | 返回有序集合 key 的成员总数 |
ZCOUNT | ZCOUNT key min max | 返回有序集合 key 中, score 值在 min 和 max 之间的成员数量 |
ZRANK | ZRANK key member | 返回有序集合 key 中成员 member 的排名 |
ZSCORE | ZSCORE key member | 返回有序集合 key 中,成员 member 的分值 |
值得一提的是,之前提过的 ZADD
命令在 Redis 中的语法是:
- 先输入分值,后输入成员。
- 例如:ZADD sorted-set 100 member
而在 Python 客户端中执行 ZADD
命令组需要:
- 先输入成员,后输入分值
- 例如:conn.zadd(‘sorted-set’, ‘member’, 100)
类似于集合,有序集合也有交集(ZINTERSTORE
)和并集(ZUNIONSTORE
)命令。我们通过一个示例来理解有序集合的交集和并集命令:
>>> conn.zadd('zset-1', 'a', 1, 'b', 2, 'c', 3)
>>> conn.zadd('zset-2', 'b', 4, 'c', 1, 'd', 0)
>>> conn.zinterstore('zset-i', ['zset-1', 'zset-2'])
2L
>>> conn.zrange('zset-i', 0, -1, withscores=True)
[('c', 4.0), ('b', 6.0)]
>>> conn.zunionstore('zset-u', ['zset-1', 'zset-2'], aggregate='min')
4L
>>> conn.zrange('zset-u', 0, -1, withscores=True)
[('d', 0.0), ('a', 1.0), ('c', 1.0), ('b', 2.0)]
>>> conn.sadd('set-1', 'a', 'd')
2
>>> conn.zunionstore('zset-u2', ['zset-1', 'zset-2', 'set-1'])
4L
>>> conn.zrange('zset-u2', 0, -1, withscores=True)
[('d', 1.0), ('a', 2.0), ('c', 4.0), ('b', 6.0)]
在执行交集和并集运算时,可以传入不同的聚合函数:
sum
,对相同成员的分值求和作为新分值。min
,取相同成员中最低的分值作为新分值。max
,取相同成员中最高的分值作为新分值。
在交集运算时,使用了默认的聚合函数sum
,所以其交集运算过程如下:
而并集运算则不同,只要某个成员存在于一个输入有序集合中,那么这个成员就会包括在输出有序集合中。在执行并集运算时,我们使用 aggregate
参数指定了聚合函数是 min
,所以其并集运算过程如下:
并集运算还可以在有序集合和集合之间进行,上面的示例中,我们将两个有序集合和一个集合组合成了一个新的有序集合:
在上述过程中,集合中的每个成员的分值都先被当作 0,然后再进行并集运算。
2.6 如何实现带优先级的队列系统
上文中,我们实现了任务分配的后端处理逻辑,在学习了哈希和有序集合的知识后,我们为每个任务带上优先级,使得高优先级的任务优先分配,更加符合实际情况。
首先我们使用哈希存储任务状态,方便我们后续查询任务状态。任务与任务状态构成域-值对,存放在 task_status
键中:
conn.hset("task_status", task_id, "init")
接下来我们要开始构建任务队列了,由于任务具有优先级,所以可以使用有序集合来存储队列信息,其成员是任务 ID,分值是优先级。例如:任务 1 的优先级为 2 时:
conn.zadd('task_queue', '1', 2)
通过上述方法将任务放进任务队列,而在取任务时,则需要使用到有序集合的排序功能,找出优先级(分值)最高的成员:
task_list_by_priority = conn.zrevrange('task_queue', 0, -1)
current_task = task_list_by_priority[0]
conn.zrem('task_queue', current_task)
ZREVRANGE
命令有三个参数,依次为 key,start,stop,其返回有序集合根据排名范围 start 到 stop 中的成员,并按分值从大到小排列。
所以我们可以使用这个命令获取到整个有序集合按照分值从大到小顺序排列的结果,从当中取出第一个成员,就是我们所需要的优先级(分值)最高的成员(current_task)了。最后,别忘了将这个成员从有序集合中移除(使用 ZREM
命令)。
因为我们使用了 task_status 哈希存储了任务状态,所以需要在任务从队列中取出,开始处理时更新这个状态:
conn.hset("task_status", current_task, "processing")
将上述步骤使用三个方法分别实现,代码如下:
# 初始化任务信息到 Redis 中
def set_task_info(task_id):
conn.hset("task_status", task_id, "init")
# 将任务添加至任务队列
def add_task_to_queue(task_id, priority):
conn.zadd("task_queue", task_id, int(priority))
set_task_info(task_id)
# 从任务队列中取出优先级最高的任务
def get_task():
task_list_by_priority = conn.zrevrange("task_queue", 0, -1)
current_task = task_list_by_priority[0]
conn.zrem('task_queue', current_task)
conn.hset("task_status", current_task, "processing")
2.7 Redis的基本事务
Redis 中的事务是一组命令的集合。事务和命令一样,都是 Redis 的最小执行单位,一个事务中的命令要么都执行,要么都不执行。
例如:在转账过程中,我们需要:
- 将钱从甲的账户中转出
- 将钱向乙的账户中转入
这两个操作要么都执行,要么都不执行,所以这两个操作就属于一个事务内。
Redis 的基本事务要用到 MULTI
命令和 EXEC
命令,我们需要先执行 MULTI
命令,再输入我们要放在事务中的命令,最后再执行 EXEC
命令。在事务执行完毕之后,Redis 才会开始处理其他客户端提交的命令。所以我们要是希望一组命令不被打断的依次执行时,也可以使用事务。
当 Redis 接收到 MULTI
命令时,会将之后接收到的所有命令都放入一个队列中,直到接收到 EXEC
命令。然后 Redis 再在不被打断的情况下,连续的执行队列中的命令。
在 Python 中,Redis 事务是通过 pipeline()
方法实现的,我们通过 pipeline()
方法创建一个事务,再将所有需要执行的命令都放进这个事务中,最后通过 execute()
方法执行这个事务。下面我们通过转账事务作为示例:
pipe = conn.pipeline()
pipe.decr('a_account', 500)
pipe.incr('b_account', 500)
pipe.execute()
pipeline()
方法通过存储事务包含的若干命令,一次性提交所有命令减少了 Redis 与客户端之间的通信次数,提升了事务命令执行的效率。
值得一提的是,Redis 的事务没有关系型数据库中事务提供的回滚(rollback)功能,所以,如果假如事务在执行过程中出错了,你需要手动将数据库恢复到事务执行前的状态。不过,也正是因为不支持回滚功能,Redis 在事务的处理上才能一直保持简洁和快速。
2.8 排序(SORT)命令
SORT
命令可以根据字符串、列表、集合、有序集合、哈希这 5 种键中存储的数据,对列表、集合和有序集合进行排序。在某种程度上,你可以把 SORT
命令看作是关系型数据库中的 order by
子句。
SORT
命令的语法如下:
SORT key [BY pattern] [LIMIT offset count] [GET pattern [GET pattern ...]] [ASC | DESC] [ALPHA] [STORE destination]
SORT
命令是 Redis 中功能最强大的命令之一,根据 SORT
命令提供的选项,可以实现:
- 根据升序(默认)/降序进行排序
- 将元素解释为数值(默认)/二进制字符串进行排序
- 使用元素之外的其他值/指定的外部键值进行排序
例如:
>>> conn.rpush('sort-list', 23, 15, 110, 7)
# 根据数值大小进行排序
>>> conn.sort('sort-list')
['7', '15', '23', '110']
# 根据字符顺序进行排序
>>> conn.sort('sort-list', alpha=True)
['110', '15', '23', '7']
>>> conn.hset('d-7', 'f', 2)
>>> conn.hset('d-15', 'f', 1)
>>> conn.hset('d-23', 'f', 3)
>>> conn.hset('d-110', 'f', 4)
# 将哈希的域作为权重,对 sort-list 列表进行排序
>>> conn.sort('sort-list', by='d-*->f')
['15', '7', '23', '110']
# 将哈希的值作为排序后的返回值
>>> conn.sort('sort-list', by='d-*->f', get='d-*->f')
['1', '2', '3', '4']
上述示例中,有两个特殊的参数,by
参数和 get
参数,他们大大的增强了 SORT
命令的功能。
2.8.1 BY 参数
很多情况下,列表(或集合、有序集合)中存储的元素值大多是对象 ID,单纯的对 ID 进行排序没有过大的意义,更多的时候,我们是希望根据 ID 对应的对象的某个属性来进行排序。例如:
- 任务队列
task_queue
中存储的是若干个任务 ID - 任务的详细信息通过哈希
task_*_info
存储- 其中包括一个域为
time
- 存储的值为任务的创建时间
- 其中包括一个域为
此时我们想根据任务的创建时间将任务队列中的所有任务进行排序,以便于调整任务的优先级,那么我们就可以通过 BY
参数来实现。
BY
参数又称为 BY
参考键,其中参考键可以是字符串类型键或者是哈希类型键的某个域(写做:键名->域名)。如果提供了 BY
参数,SORT
命令就不再依据元素自身的值进行排序,而是对每个元素使用元素的值替换参考键中的第一个 * 并获取其值,然后再依据这个值对元素排序。
回到上面的例子,我们就可以这样实现:
>>> conn.lpush('task_queue', '3', '4', '2', '1')
>>> conn.hmset('task_1_info', {'time': 1541158465.641236})
>>> conn.hmset('task_2_info', {'time': 1541158497.192748})
>>> conn.hmset('task_3_info', {'time': 1541158525.584697})
>>> conn.hmset('task_4_info', {'time': 1541158547.424744})
>>> conn.sort('task_queue', by='task_*_info->time')
['1', '2', '3', '4']
SORT
命令会读取 task_1_info
,task_2_info
,task_3_info
,task_4_info
哈希键中的 time
域的值,并根据这个值将 task_queue
中的任务 ID 排序。
当然 BY
参数还可以使用字符串类型作为参考键。
2.8.2 GET 参数
上面所说的 BY
参数让你能够使用外部的值辅助排序,而接下来要介绍的 GET
参数则让你能够更方便的根据排序结果取出外部的值。
GET
参数和 BY
参数的规则一致,也支持字符串类型和哈希类型的键,并使用 * 作为占位符。例如我们要在带优先级的任务队列(有序集合 task_queue)排序后取出任务 ID 对应的任务创建时间时,可以这样做:
>>> conn.zadd('task_queue', '1', 3, '2', 4, '3', 1, '4', 2)
>>> conn.sort('task_queue', by='score', desc=True)
['2', '1', '4', '3']
>>> conn.sort('task_queue', by='score', desc=True, get='task_*_info->time')
['1541158497.192748', '1541158465.641236', '1541158547.424744', '1541158525.584697']
这里我们还使用了
DESC
参数(desc=True
)来使用倒序排序。
在一个 SORT
命令中可以有多个 GET
参数(但注意,BY
参数只能有一个),你可以根据需求从不同的键中取出需要的值,以一次性取出所有需要的数据,降低客户端与 Redis 间的通信次数。
最后需要提醒你的是,SORT
命令是 Redis 中最强大最复杂的命令之一,如果你使用不当很容易成为性能瓶颈之一。所以,在你使用 SORT
命令的时候,需要注意:
- 尽可能减少待排序元素的个数
- 使用
SORT
选项限制要获取的数据量 - 使用
STORE
参数将结果存储
2.9 Redis的键过期时间
在使用 Redis 存储数据时,可能某些数据在一段时间后就不再有用了。这时我们可以通过 DEL
命令显式地删除这些无用数据,也可以通过 Redis 的过期时间让一个键在指定的时间后自动被删除。
在 Redis 中可以使用 EXPIRE
命令设置一个键的生存时间,到时间后 Redis 则会自动删除该键,该命令的语法为:
EXPIRE key seconds
seconds
表示键的生存时间,单位是秒。
假如我们想让 task_1_info
键在一天之后被删除,可以这样做:
>>> conn.expire('task_1_info', 24 * 60 * 60)
True
当返回值:
- 为 True 时表示设置成功
- 为 False 时表示键不存在或设置失败
如果你想知道一个键还有多久过期,则可以使用 TTL
命令查看键的剩余时间(单位:秒):
>>> conn.ttl('task_1_info')
86257L
>>> conn.ttl('task_2_info')
>>>
当一个键不存在或没有为该键设置过期时间时,
TTL
命令的返回值都是 -1,但 Python 客户端对这个返回值做了一些处理,使它变成了 None。
需要注意的是,EXPIRE
命令和 TTL
命令的单位都是秒,如果需要更加精确的控制键的生存时间,则应该使用 PEXPIRE
命令,该命令可以将生存时间精确到毫秒级,与之对应的也有 PTTL
命令来查看键的剩余时间(单位:毫秒)。
不同的键中取出需要的值,以一次性取出所有需要的数据,降低客户端与 Redis 间的通信次数。
最后需要提醒你的是,SORT
命令是 Redis 中最强大最复杂的命令之一,如果你使用不当很容易成为性能瓶颈之一。所以,在你使用 SORT
命令的时候,需要注意:
- 尽可能减少待排序元素的个数
- 使用
SORT
选项限制要获取的数据量 - 使用
STORE
参数将结果存储
2.9 Redis的键过期时间
在使用 Redis 存储数据时,可能某些数据在一段时间后就不再有用了。这时我们可以通过 DEL
命令显式地删除这些无用数据,也可以通过 Redis 的过期时间让一个键在指定的时间后自动被删除。
在 Redis 中可以使用 EXPIRE
命令设置一个键的生存时间,到时间后 Redis 则会自动删除该键,该命令的语法为:
EXPIRE key seconds
seconds
表示键的生存时间,单位是秒。
假如我们想让 task_1_info
键在一天之后被删除,可以这样做:
>>> conn.expire('task_1_info', 24 * 60 * 60)
True
当返回值:
- 为 True 时表示设置成功
- 为 False 时表示键不存在或设置失败
如果你想知道一个键还有多久过期,则可以使用 TTL
命令查看键的剩余时间(单位:秒):
>>> conn.ttl('task_1_info')
86257L
>>> conn.ttl('task_2_info')
>>>
当一个键不存在或没有为该键设置过期时间时,
TTL
命令的返回值都是 -1,但 Python 客户端对这个返回值做了一些处理,使它变成了 None。
需要注意的是,EXPIRE
命令和 TTL
命令的单位都是秒,如果需要更加精确的控制键的生存时间,则应该使用 PEXPIRE
命令,该命令可以将生存时间精确到毫秒级,与之对应的也有 PTTL
命令来查看键的剩余时间(单位:毫秒)。