一、Redis中的数据结构
任务描述
本关任务:启动
Redis客户端并创建一些值。相关知识
为了完成本关任务,你需要掌握:
1.Redis简介,2.快速安装Redis与Python,3.Redis数据结构简介。Redis简介
Redis是一个速度非常快的非关系型数据库(non-relational database),它可以存储键(key)和五种不同类型的值(value)之间的映射(mapping),可基于内存存储亦可持久化到硬盘的日志型,Key-Value数据库。Redis与其他数据库的对比
如果你使用过关系型数据库,例如:
Mysql,那么你肯定写过关联两张表数据的查询语句。而Redis属于NoSQL,它不使用表,也不会预定义数据模式或强制用户对Redis的各种数据进行关联。
NoSQL(Not Only SQL)意指“不仅仅是
SQL”,其泛指非关系型数据库,主要分为四类:键值(Key-Value)存储数据库,列存储数据库,文档型数据库,图形(Graph)数据库。
Redis也经常与高性能键值缓存服务器memcached做比较:两者均可用于存储键值映射,性能相差也甚少,但Redis能存储除普通字符串值之外的四种数据结构,而memcached只能存储普通的字符串值。这些不同使得Redis能够解决更为广泛的问题,而且既能作为主数据库使用,也可以作为辅助数据库使用。0我们通过一张表来对比常用的数据库与缓存服务器:
名称 类型 数据存储选项 查询类型 附加功能 Redis基于内存的非关系型数据库 字符串、列表、集合、哈希、有序集合 针对数据类型有专属命令,另有批量操作和不完全的事务支持 发布与订阅、复制、持久化、脚本扩展 memcached基于内存的键值缓存 键值映射 创建、读取、更新、删除等 多线程支持 MySQL关系型数据库 数据表、视图等 查询、插入、更新、删除、内置函数、自定义存储过程等 支持 ACID性质、复制等MongoDB基于硬盘的非关系型文档存储数据库 无 schema的BSON文档创建、读取、更新、删除、条件查询等 复制、分片、空间索引等 Redis的特性
由于
Redis是内存型数据库,在使用之前就要考虑当服务器被关闭时,服务器存储的数据是否能保留。Redis 拥有两种不同形式的持久化方法,都可以用紧凑的格式将数据写入硬盘:
RDB持久化
- 在指定的时间间隔内生成数据集的时间点快照
AOF持久化
- 记录服务器执行的所有写操作命令
- 新命令会被追加到文件的末尾
- 在服务器启动时,通过重新执行这些命令还原数据集
除此之外,为了扩展
Redis的读性能,并为Redis提供故障转移支持,Redis实现了主从复制特性:
- 执行复制的从服务器连接主服务器
- 接收主服务器发送的初始副本
- 接收主服务器执行的所有写命令
- 在从服务器上执行所有写命令,实时更新数据库
- 读命令可以向任意一个从服务器发送
快速安装 Redis
docker-compose部署redis
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各种数据结构和其命令的使用。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)- 尝试获取不存在的键时会得到一个
nilRedis中的列表
就像前面所说的,
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"我们可以看出,在列表中,元素可以重复出现。在后续的实训中,我们还会介绍更多列表命令,现在我们先来了解以下
Redis中的集合。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命令的执行速度会很慢,所以要谨慎的使用这个命令。Redis中的哈希
哈希可以存储多个键值对之间的映射。和字符串一样,哈希存储的值既可以是字符串又可以是数字值,并且可以对数字值进行自增/自减操作。
哈希就像是一个缩小版的
Redis,有一系列命令对哈希进行插入、获取、删除:
- HSET key field value:设置哈希字段的值
- HGET key field:获取哈希字段的值
- HDEL key field:删除哈希字段
- HEXISTS key field:检查哈希字段是否存在
- HKEYS key:获取哈希的所有字段
- HVALS key:获取哈希的所有值
- HGETALL key:获取哈希的所有字段和值
- HLEN key:获取哈希的字段数量
- HMSET key field1 value1 [field2 value2 ]:设置多个哈希字段
- HMGET key field1 [field2]:获取多个哈希字段的
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用户删除哈希中的某一个域Redis中的有序集合
有序集合和哈希一样,也是存储键值对。
只是有序集合的键被称为成员(
member),每个成员都是唯一的,有序集合的值则被称为分值(score),这个分值必须为浮点数。所以有序集合既可以通过成员访问元素,也可以通过分值来排序元素。我们可以通过:
ZADD命令将带有指定分值的成员添加到有序集合中ZRANGE命令根据分值有序排列后的集合获取到指定范围的元素ZRANGEBYSCORE命令获取指定分值范围内的元素ZREM命令从有序集合中删除指定成员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"编程要求
根据提示,打开命令行,启动
Redis客户端并创建一些值:
- 使用默认配置后台启动 Redis 服务器
- 启动 Redis 客户端
redis-cli- 设置字符串
- 键为
hello- 值为
redis- 设置列表,键为
educoder-list
- 从列表左侧推入元素
hello- 从列表右侧推入元素
educoder- 从列表右侧推入元素
bye- 从列表右侧弹出一个元素
- 设置集合,键为
educoder-set
- 添加元素
c- 添加元素
python- 添加元素
redis- 删除元素
c- 设置哈希,键为
educoder-hash
- 添加键:
python,值为:language- 添加键:
ruby,值为:language- 添加键:
redis,值为:database- 删除键
ruby- 设置有序列表,键为
educoder-zset
- 添加成员
jack,分值为200- 添加成员
rose,分值为400- 添加成员
lee,分值为100
redis-cli
set hello redis
LPUSH educoder-list hello
RPUSH educoder-list educoder
RPUSH educoder-list bye
RPOP educoder-list
sadd educoder-set c
sadd educoder-set python
sadd educoder-set redis
srem educoder-set c
hset educoder-hash python language
hset educoder-hash ruby language
hset educoder-hash redis database
hdel educoder-hash ruby
zadd educoder-zset 200 jack
zadd educoder-zset 400 rose
zadd educoder-zset 100 lee
二、使用 Python 与 Redis 交互
任务描述
本关任务:使用
Python编写程序与Redis交互。相关知识
为了完成本关任务,你需要掌握:
1.如何使用Python连接Redis,2.通过客户端对Redis的数据进行操作。如何使用 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操作了。通过客户端对 Redis 的数据进行操作
通过客户端对
Redis的数据进行操作和第一关直接在Redis中的操作命令基本相同。只是在客户端中操作如下,要在命令前加上客户端的名字和.(假设使用方法2创建客户端r):# 使用 SET 命令设置一个字符串键 r.set("test", "hello") # 显示字符串键 test 的值 print(r.get("test"))编程要求
根据提示,在右侧
Begin-End区域补充代码,实现使用Python编写程序与Redis交互:
- 使用方法
2创建客户端r1连接到Redis- 设置下表中的两个字符串键:
键 值 test1hellotest2Redis
#!/usr/bin/env python
#-*- coding:utf-8 -*-
import redis
def write_redis():#********* Begin *********#r1 = redis.Redis(host='localhost', port=6379, db=0)# 设置键值对r1.set('test1', 'hello')r1.set('test2', 'Redis')#********* End *********#
三、使用Python+Redis实现文章投票网站后端功能
任务描述
本关任务:编写一个简化版文章投票网站的后端处理逻辑。
相关知识
第一关中,我们对
Redis提供的五种数据结构有了基本的了解,这一关我们学习如何使用这些数据结构来解决实际问题。大多数网站都提供了对新闻、文章或者问答进行投票的功能,并根据文章的发布时间/投票数量进行排序。本关卡中,我们将使用
Redis构建简单的文字投票及排序功能。为了完成本关任务,你需要掌握:
1.实现投票功能,2.创建文章数据,3.对文章进行排序。实现投票功能
实现投票功能,要注重文章的时效性与投票的公平性,所以需要给投票功能加上一些约束条件:
- 文章发布满一个星期后,不再允许用户对该文章投票
- 一个用户对一篇文章只能投一次票
所以我们需要使用:
- 一个有序集合
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# 尝试将用户添加到已经投票的用户集合中# 如果用户是第一次投票,那么 sadd 方法会返回 1# 如果用户已经投过票,那么 sadd 方法会返回 0if r.sadd('voted:' + article_id, user_id):# 如果用户是第一次投票,那么增加文章的得分r.zincrby('score', article_id, 1)在这段代码中,我们使用了Redis的有序集合(zset)和集合(set)数据类型。zscore方法用于获取有序集合中元素的分数,sadd方法用于向集合中添加元素,zincrby方法用于增加有序集合中元素的分数
当用户尝试投票时,使用
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:是要增加的分值创建文章数据
现在系统中还缺少文章数据,所以我们要提供一个创建文章的函数,并把文章数据存储到
Redis中。创建文章的步骤如下:
- 创建新的文章
ID- 将文章作者加入到这篇文章的已投票用户名单中
- 存储文章详细信息到
Redis中- 将文章的发布时间和初始投票数加入到
time和score两个有序集合中
r.expire(key, seconds)
key:要设置过期时间的键名seconds:过期时间的长度(单位:秒)这里我们要设置的时间是一周,所以我们可以使用上面定义好的全局变量
ONE_WEEK_IN_SECONDS。在这段代码中,我们使用了Redis的字符串(string)、集合(set)、哈希(hash)和有序集合(zset)数据类型。incr命令用于对字符串值进行自增操作,sadd命令用于向集合中添加元素,expire命令用于设置键的过期时间,hmset命令用于向哈希中添加多个字段和值,zadd命令用于向有序集合中添加元素。
将文章作者加入已投票用户名单中和之前一样,这里不再赘述,但在这里我们需要为这个已投票用户名单设置一个过期时间,让它在一周后(到期后)自动删除,减少
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命令来实现:我们可以使用
Python的散列来一次性存储多个字段(域)到Redis,只需要将整个散列当作key对应的值通过hmset函数设置进去就行。最后,将初始投票数和创建时间设置到
score和time中都可以通过ZADD命令来实现:
r.zadd(key, member, score)
key:有序集合的键名member:要加入有序集合的成员score:该成员的分值这里需要注意的是,因为该篇文章的作者已经被加入到该文章已投票用户名单中,为了保持数据一致性,我们需要将文章的初始投票数设为
1。对文章进行排序
实现了文章投票和创建文章功能,接下来我们就需要将评分最高的文章和最新发布的文章从
Redis中取出了。
首先我们要根据排序方式的不同:
- 按评分排序,则从
score有序集合中取出一定量的文章ID(score有序集合存放文章ID和对应的投票数)- 按时间排序,则从
time有序集合中取出一定量的文章ID(time有序集合存放文章ID和对应的发布时间)构成一个有序文章信息列表,每个元素都:
- 使用
HGETALL命令,取出每篇文章的全部信息def get_articles(r, start, end, order='score'):# 使用Redis的ZREVRANGE命令获取有序集合中的一系列元素。这些元素是按照分数从高到低排序的,范围是从start到endids = r.zrevrange(order, start, end)# 创建一个空列表,用于存储获取到的文章articles = []# 遍历获取到的元素IDfor id in ids:# 使用Redis的HGETALL命令获取哈希中的所有字段和值,这些字段和值构成了文章的数据article_data = r.hgetall(id)# 将元素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中是作为键名存储的,不在值当中,所以我们需要附加这个字段到文章信息中。实现这些方法后,我们大体实现了一个文章投票的后端处理逻辑,能够为文章投票并能根据投票结果改变文章的排序情况。
编程要求
根据提示,在右侧
Begin-End区域补充代码,完成简化版文章投票网站的后端处理逻辑:
在
article_vote()函数中:
- 该方法作用是:对文章投票
- 参数说明:
r:Redis 客户端
user_id:投票用户
article_id:被投票文章
- 已提供一周前
Unix时间戳,存放在变量cutoff- 当满足以下条件时,为文章投一票:
- 该文章发布不超过一周
该用户没有为该文章投过票
在
post_article()函数中:
- 该方法作用是:创建文章
- 参数说明:
r:Redis 客户端
user:发布用户
title:文章标题
link:文章链接
- 已提供:
article_id,新文章ID
voted,新文章已投票用户名单存储键名
article,新文章详细信息存储键名
now,文章创建时间
- 按照
ID递增的顺序依次创建文章- 保证发布文章的用户不能给自己的文章投票
- 文章在发布一周后删除已投票用户名单
- 存储文章详细信息到
Redis中,包括字段:
- 文章标题
文章链接
发布用户
- 存储文章的发布时间和初始投票数
- 初始投票数为
1在
get_articles()函数中:
- 该方法作用是:对文章进行排序
- 参数说明:
r:Redis 客户端start:从排序为start的文章开始获取
end:到排序为end的文章结束获取
order:排序方式,分为两种:
time:按时间排序score:按投票数排序已提供文章信息空列表,
articles实现按时间/投票数排序
将排序后的文章及其全部信息组成一个列表:
- 按照不同排序规则取出排序在参数提供的区间范围内的文章
- 及每篇文章的全部信息,包括文章
ID
#!/usr/bin/env python
#-*- coding:utf-8 -*-import timeONE_WEEK_IN_SECONDS = 7 * 24 * 60 * 60def article_vote(r, user_id, article_id):cutoff = time.time() - ONE_WEEK_IN_SECONDS# 请在下面完成要求的功能#********* Begin *********#if r.zscore('time', article_id) < cutoff:returnif r.sadd('voted:' + article_id, user_id):r.zincrby('score', article_id, 1)#********* End *********#def post_article(r, user, title, link):article_id = str(r.incr('article'))voted = 'voted:' + article_idnow = time.time()article = 'article:' + article_id# 将发布文章的用户添加到投票集合中,并设置投票集合的过期时间为一周r.sadd(voted, user)r.expire(voted, ONE_WEEK_IN_SECONDS)# 将文章的标题、链接和发布者添加到哈希中,并将文章添加到得分和时间有序集合中r.hmset(article, {'title': title,'link': link,'poster': user,})r.zadd('score', article_id, 1)r.zadd('time', article_id, now)return article_iddef get_articles(r, start, end, order='score'):articles = []# 请在下面完成要求的功能#********* Begin *********#ids = r.zrevrange(order, start, end)for id in ids:article_data = r.hgetall(id)article_data['id'] = idarticles.append(article_data)#********* End *********#return articles