首页 > 数据库 >NoSQL数据库实习头歌实验知识点整理(三)-Redis部分

NoSQL数据库实习头歌实验知识点整理(三)-Redis部分

时间:2024-12-05 11:57:34浏览次数:7  
标签:知识点 NoSQL redis Redis 命令 key 集合 conn

文章目录

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基于硬盘的非关系型文档存储数据库schemaBSON 文档创建、读取、更新、删除、条件查询等复制、分片、空间索引等

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 命令可将元素推入列表的右侧
  • LPOPRPOP 就分别从列表的左侧和右侧弹出元素
  • 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
    • 使用了连接池总揽多个客户端与服务端的连接
    • 不需要手动删除客户端
    • 同时有效的减少多个客户端连接的损耗

所以我们在实际开发中使用第二种方法较多。

在创建了客户端之后,你就可以使用 coonr 这个客户端来进行 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 中
  • 将文章的发布时间和初始投票数加入到 timescore 两个有序集合中
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 函数设置进去就行。

最后,将初始投票数和创建时间设置到 scoretime 中都可以通过 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 字符串执行自增和自减的命令列表如下:

命令用法说明
INCRINCR key将 key 存储的值加上 1
DECRDECR key将 key 存储的值减去 1
INCRBYINCRBY key increment将 key 存储的值加上 increment
DECRBYDECRBY key decrement将 key 存储的值减去 decrement
INCRBYFLOATINCRBYFLOAT key increment将 key 存储的值加上浮点数 increment

注意:INCRBYFLOAT 只能在 Redis 版本 >= 2.6 时可用

当用户将一个值存储到 Redis 字符串中,Redis 会检测这个值是否可以被解释(interpret)为十进制整数或者浮点数。如果可以,则允许用户对该字符串进行自增和自减操作。

在前面也提到过,如果用户对一个不存在的键或者一个保存了空串的键执行了自增或自减操作,Redis 都会:

  • 先将该键的值置为 0
  • 再对该键的值进行自增或自减操作

需要额外提到的是,Python 的 Redis 库在 incr(key, increment=1) 方法中同时实现了 INCRINCRBY 命令,该方法的第二个参数 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 还可以对字节串的一部分内容进行读取/写入:

命令用法说明
APPENDAPPEND key value将 value 追加到 key 键存储的值的末尾
GETRANGEGETRANGE key start end获取 start 到 end 间的子串
SETRANGESETRANGE 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 提供了丰富的列表操作命令,从而使得列表的应用场景非常广泛,例如:存储任务队列,记录最近的操作/数据变化,作为日志收集器等。

首先我们介绍一些常用的列表命令:

命令用法说明
LPUSHLPUSH key value [value ...]将一个或多个 value 推入到列表的左侧
RPUSHRPUSH key value [value ...]将一个或多个 value 推入到列表的右侧
LLENLLEN key返回列表 key 的长度
LREMLREM 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 提供了针对单个集合以及多集合间处理的命令:

命令用法说明
SCARDSCARD key返回集合 key 中元素的数量
SRANDMEMBERSRANDMEMBER key [count]返回集合中的 1 或 count 个随机元素
SPOPSPOP key移除并返回集合中的一个随机元素
SMOVESMOVE 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 集合还有更为强大的功能 —— 组合和关联多个集合

命令用法说明
SDIFFSDIFF key [key ...]返回所有给定集合之间的差集
SINTERSINTER key [key ...]返回所有给定集合的交集
SUNIONSUNION key [key ...]返回所有给定集合的并集

上述三个命令是差集,交集,并集运算的“返回结果”版本,同时 Redis 还提供了“存储结果”版本,你可以参考 Redis 命令参考 中的 SDIFFSTORESINTERSTORESUNIONSTORE 命令。

2.4 常用哈希命令

Redis 的哈希允许用户将多个键值存储到一个 Redis 键中,使得哈希十分适合将一些相关的数据存储在一起。我们可以把这种数据看作是关系型数据库中的行。

常用的哈希命令包括之前介绍过的添加和删除域-值对命令、获取所有域-值对命令以及对域-值对的值进行自增/自减操作的命令:

命令用法说明
HMSETHMSET key field value [field value ...]同时将多个 field-value (域-值)对设置到哈希表 key 中
HMGETHMGET key field [field ...]返回哈希表 key 中,一或多个给定域的值
HDELHDEL key field [field ...]删除哈希表 key 中的一或多个指定域
HLENHLEN key返回哈希表 key 中域的数量

在上一节中,我们使用过 HMSET 命令来批量的存储域-值对信息,实际上 HMSETHMGET 命令既可以通过批量处理给用户带来便利,又减少了命令的调用次数,提升了客户端与 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 命令的多参数形式传入参数。而之前介绍的 HGETHSET 命令则分别是 HMGETHMSET 命令的单参数版本,每次执行时只能处理一个键值对。

Redis 哈希还支持一些更高级的批量操作

命令用法说明
HEXISTSHEXISTS key field查看哈希表 key 中,给定域 field 是否存在
HKEYSHKEYS key返回哈希表 key 中所有域
HVALSHVALS key返回哈希表 key 中所有域的值
HINCRBYHINCRBY key field increment为哈希表 key 中的域 field 的值加上 increment

在哈希包含的值的体积都十分大时,我们应该使用 HKEYS 命令获取所有的域,再使用 HGET 一个个的从哈希中取出域的值,从而避免 Redis 因为一次性获取多个大体积的值而导致服务器阻塞。甚至,我们可以只获取必要的值来减少传输的数据量。

2.5 常用有序集合命令

有序集合与哈希类似,也存储着成员(member)和分值(score)之间的映射关系。Redis 为有序集合提供了分值处理命令,并能根据分值大小有序的排列成员:

命令用法说明
ZCARDZCARD key返回有序集合 key 的成员总数
ZCOUNTZCOUNT key min max返回有序集合 key 中, score 值在 min 和 max 之间的成员数量
ZRANKZRANK key member返回有序集合 key 中成员 member 的排名
ZSCOREZSCORE 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_infotask_2_infotask_3_infotask_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 命令来查看键的剩余时间(单位:毫秒)。

标签:知识点,NoSQL,redis,Redis,命令,key,集合,conn
From: https://blog.csdn.net/TluoshangY/article/details/144261750

相关文章

  • 头歌解答 Redis基本命令
    第1关:字符串、列表与集合编程要求根据提示,在右侧Begin-End区域补充代码,完成任务分配的后端处理逻辑:在task_empty()方法中:从Redis中获取列表task_list的长度,判断是否为0若为0,则返回True若不为0,则返回False在get_task()方法中:从列表task_lis......
  • Redis探秘Sentinel(哨兵模式)
    概述Redis的高可用机制有持久化、复制、哨兵和集群。其主要的作用和解决的问题分别是:持久化:持久化是最简单的高可用方法(有时甚至不被归为高可用的手段),主要作用是数据备份,即将数据存储在硬盘,保证数据不会因进程退出而丢失。复制:复制是高可用Redis的基础,哨兵和集群都是在复制......
  • JVM优化,Redis,MySQL相关面试题
    一、平常对SQL优化的了解1.索引优化创建索引:为常用的查询字段创建索引,可以显著提高查询速度。例如,为订单金额的字段创建索引,可以加速按订单金额的排序操作。优化索引:定期维护索引,避免索引碎片化,保持索引性能。使用覆盖索引:通过创建覆盖索引,可以避免回表操作,进一步提高查......
  • Redis指南【5】图解深入 RDB 与 AOF
    前言Redis它是一个键值对的内存数据库,读写数据都是基于内存的,所以它的性能非常高,但同时如果服务器一旦宕机,那么内存的数据是不可恢复的,所以,redis想到了持久化,如何把内存中的数据优雅的同步到磁盘中,以便redis在重启时能够恢复原有的数据,这就是持久化。Redis的持久化有三......
  • 哈希表(【通俗易懂】知识点讲解,可速通,小白友好)
    一、哈希表的目的哈希表是用在查找问题中的。我们知道,一条数据包含了关键字和其他信息,所以一般查找问题的流程是:根据某条数据的关键字(key),在一个数据结构中(可能是线性表,也可能其他存储数据的结构),查找这条数据全部的内容。哈希表的目的是,只要知道了要查找数据的关键字,那么就可......
  • 第58篇 Redis常用命令
    1.基本操作2.字符串(Strings)3.列表()4.哈希(Hashes)5.位图(Bitmaps)6.位域(Bitfields)7.集合(Sets)8.有序集合(SortedSets)9.流(Streams)10。地理空间(Geospatial)11.HyperLogLog......
  • Redis——个人笔记留存
    今日内容1.redis1.概念2.下载安装3.命令操作1.数据结构4.持久化操作5.使用Java客户端操作redisRedis1.概念:redis是一款高性能的NOSQL系列的非关系型数据库1.1.什么是NOSQLNoSQL(NoSQL=NotOnlySQL),意即“不仅......
  • filebeat收集日志输出到redis
    目的:使用filebeat收集日志,将日志信息存储到redis---持久化存储,减轻localstorage的压力前提:安装了Elasticsearch跟filebeat,并启动Elasticsearch问题:redis安装成功,配置文件修改成功,重新启动数据库。然后在另一台节点使用nc命令模拟客户端发送数据并指定端口(在.yml......
  • Redis7缓存介绍及在Window下配置启动过程
    Redis介绍Redis(RemoteDictionaryServer),即远程字典服务,是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。redis是一个key-value存储系统。和Memcached类似,它支持存储的value类型相对更多,包括string(字符串)......
  • redis开门之批量插入pipeLine
    Redis开门之批量插入Pipeline下发数据同步到Redis中,数据少的话几千条,多则达百万级。其中一个场景是把下发的数据同步到Redis中,数据同步完成后,把数据写入到文件中,下发给客户,客户调用。某天......产品经理:小A,我发现我们这个数据整体下发的流程耗时有点长啊...从拉取数据到处理下发......