Redis入门--头歌实验Redis基本命令

Redis 命令十分丰富,包括键(Key)、字符串(String)、哈希(Hash)、列表(List)、集合(Set)、有序集合(Sorted Set)、发布与订阅(Pub/Sub)等 14 个 Redis 命令组,共两百多条 Redis 命令。

一、字符串、列表与集合

任务描述

本关任务:使用 Redis 字符串、列表和集合的常用命令完成任务分配的后端处理逻辑。

相关知识

为了完成本关任务,你需要掌握:1.常用字符串命令,2.常用列表命令,3.常用集合命令。

常用字符串命令

Redis 的字符串可以存储三种类型的值:

  • 整数
  • 浮点数
  • 字节串

取值范围说明

Redis 中整型数据的长度与系统字长一致(例如:32位系统,整型数据为32位有符号整数)

Redis 中浮点数的取值范围与精度都与双精度浮点数(double)一致

数值操作

所以针对存储整型和浮点型的字符串就有自增自减操作。在需要的时候(例如下表的 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
  • 再对该键的值进行自增或自减操作

需要额外提到的是,PythonRedis 库在 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
字节串操作

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 将会从起始偏移量读取到该字符串的末尾。

常用列表命令

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 参数放至最后,在 PythonRedis 客户端中,大多数命令中的数值型参数都被放到了最后,如果弄不清某个方法的参数,你可以到  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']

原子时间

不可再拆分的时间段

意指该操作执行时,不可被其他操作打断,也就是包含在一个原子时间内的若干操作要么都成功要么都失败

常用集合命令

与列表有序不同,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 命令。

编程要求

根据提示,在右侧Begin-End区域补充代码,完成任务分配的后端处理逻辑:

  • 在 task_empty() 方法中:
    • 从 Redis 中获取列表 task_list 的长度,判断是否为 0
      • 若为 0,则返回 True
  • 若不为 0,则返回 False
  • 在 get_task() 方法中:
    • 从列表 task_list 的最右侧弹出一个元素,赋值给 task
    • 将 task 的值设置到 Redis 的字符串键 current_task 中
  • 在 get_unallocated_staff() 方法中:
    • 从集合 unallocated_staff 中随机返回一个元素,赋值给 staff
    • 将上面的 staff 从集合 unallocated_staff 移动到集合 allocated_staff 中
    • 返回(returnstaff 的值
  • 在 allocate_task(staff) 方法中:
    • 将参数 staff 的值追加到 Redis 字符串键 current_task 的尾部,中间以 : 间隔
    • 将追加后的字符串键 current_task 从左侧推入列表 task_queue
    • 将字符串键 current_task 的值设置为 "None"
测试说明

我会对你编写的代码进行测试:

测试输入:

task_1 task_2 task_3 task_4 task_5
staff_1 staff_2 staff_3 staff_4 staff_5

预期输出:

Init task list: ['task_1', 'task_2', 'task_3', 'task_4', 'task_5']
Init staff list: set(['staff_4', 'staff_5', 'staff_1', 'staff_2', 'staff_3'])

Cur task list is empty: False
Get new task: task_5
Current staff is allocated: True
Current staff is unallocated: False
Current task is: None

Allocated all tasks
Task queue length: 5
Task list is empty: True
Allocated_staff: set(['staff_4', 'staff_5', 'staff_1', 'staff_2', 'staff_3'])
Unallocated_staff: set([])
#!/usr/bin/env python
#-*- coding:utf-8 -*-

import redis

conn = redis.Redis()

def task_empty():
    # 请在下面完成判断任务列表是否为空
    # ********* Begin *********#
    if conn.llen("task_list") > 0:
        return False
    else:
        return True
    # ********* End *********#
def get_task():
    # 请在下面完成获取一个任务
    # ********* Begin *********#
    task = conn.rpop("task_list")
    conn.set("current_task", task)
    # ********* End *********#

def get_unallocated_staff():
    # 请在下面完成获取一个未分配的员工
    #********* Begin *********#
    staff = conn.srandmember("unallocated_staff")
    conn.smove("unallocated_staff","allocated_staff", staff)
    return staff;
    #********* End *********#

def allocate_task(staff):
    # 请在下面完成分配任务
    #********* Begin *********#
    conn.append("current_task", ":" + staff)
    conn.lpush("task_queue","current_task")
    conn.set("current_task", "None")
    #********* End *********#

二、哈希与有序集合

任务描述

本关任务:编写带优先级的队列系统的后端处理逻辑。

相关知识

为了完成本关任务,你需要掌握:1.常用哈希命令,2.常用有序集合命令,3.如何实现带优先级的队列系统。

常用哈希命令

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 因为一次性获取多个大体积的值而导致服务器阻塞。甚至,我们可以只获取必要的值来减少传输的数据量。

常用有序集合命令

有序集合与哈希类似,也存储着成员(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,取相同成员中最高的分值作为新分值。
如何实现带优先级的队列系统

上一关中,我们实现了任务分配的后端处理逻辑,在学习了哈希和有序集合的知识后,我们为每个任务带上优先级,使得高优先级的任务优先分配,更加符合实际情况。

首先我们使用哈希存储任务状态,方便我们后续查询任务状态。任务与任务状态构成域-值对,存放在 task_status 键中:

conn.hset("task_status", task_id, "init")

接下来我们要开始构建任务队列了,由于任务具有优先级,所以可以使用有序集合来存储队列信息,其成员是任务 ID,分值是优先级。例如:任务 1 的优先级为 2 时:

conn.zadd('task_queue', '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 命令有三个参数,依次为 keystartstop,其返回有序集合根据排名范围 startstop 中的成员,并按分值从大到小排列。

所以我们可以使用这个命令获取到整个有序集合按照分值从大到小顺序排列的结果,从当中取出第一个成员,就是我们所需要的优先级(分值)最高的成员(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")
编程要求

根据提示,在右侧Begin-End区域补充代码,完成带优先级的队列系统的后端处理逻辑:

  • 在 set_task_info(task_id) 方法中:
    • 使用参数 task_id 作为域,初始状态 "init" 作为值构成域-值对,存放在 task_status 哈希键中。
  • 在 add_task_to_queue(task_id, priority) 方法中:
    • 参数说明:
      • task_id 为任务 ID
  • priority 为任务优先级。
    • 将分值(优先级)为 priority 的成员 task_id 存入有序集合 task_queue 中。
      • 注意将参数 priority 转换为整型
    • 调用 set_task_info() 方法,传入参数 task_id
  • 在 get_task() 方法中:
    • 新建变量 task_list_by_priority,值为:
      • 使用 ZREVRANGE 命令按照分值(优先级)从大到小顺序返回有序集合 task_queue 的全部成员。
    • 新建变量 current_task,值为:
      • task_list_by_priority 中的第一个元素(下标为 0
    • 将成员 current_task 从有序集合 task_queue 中移除
    • 修改哈希 task_status 中的 current_task 域的值为 "processing"
    • 返回(returncurrent_task 的值
测试说明

我会对你编写的代码进行测试:

测试输入:

1 2 3 4 5 6 7 8 9 10
2 4 9 1 0 5 8 6 7 3

预期输出:

Add new task: 1, priority: 2, status: init
Add new task: 2, priority: 4, status: init
Add new task: 3, priority: 9, status: init
Add new task: 4, priority: 1, status: init
Add new task: 5, priority: 0, status: init
Add new task: 6, priority: 5, status: init
Add new task: 7, priority: 8, status: init
Add new task: 8, priority: 6, status: init
Add new task: 9, priority: 7, status: init
Add new task: 10, priority: 3, status: init
Before: task list is: ['3', '7', '9', '8', '6', '2', '10', '1', '4', '5']
Get new task: 3
After: task list is: ['7', '9', '8', '6', '2', '10', '1', '4', '5']
Current task status: processing
#!/usr/bin/env python
#-*- coding:utf-8 -*-

import redis

conn = redis.Redis()

# 初始化任务信息到 Redis 中
def set_task_info(task_id):
    # 请在下面完成要求的功能
    #********* Begin *********#
    conn.hset("task_status", task_id, "init")
    #********* End *********#

# 将任务添加至任务队列
def add_task_to_queue(task_id, priority):
    # 请在下面完成要求的功能
    #********* Begin *********#
    conn.zadd("task_queue", task_id, int(priority))
    set_task_info(task_id)
    #********* End *********#

# 从任务队列中取出优先级最高的任务
def get_task():
    # 请在下面完成要求的功能
    
    #********* Begin *********#
    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")
    return current_task
    #********* End *********#

三、Redis基本事务与其他命令

任务描述

本关任务:编写一个网络约车的后端处理逻辑。

相关知识

为了完成本关任务,你需要掌握:1.Redis的基本事务,2.排序(SORT)命令,3.Redis的键过期时间。

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 在事务的处理上才能一直保持简洁和快速。

排序(SORT)命令

SORT 命令可以根据字符串、列表、集合、有序集合、哈希这 5 种键中存储的数据,对列表、集合和有序集合进行排序。在某种程度上,你可以把 SORT 命令看作是关系型数据库中的 order by 子句。SORT 命令的语法如下:

/* 
 * SORT 命令用于对查询结果进行排序,默认按升序排列。
 * 
 * 参数说明:
 * key: 指定要对其元素进行排序的键名,该键必须为列表、集合或有序集合类型。
 *
 * BY pattern (可选): 指定一个模式,根据这个模式从元素中提取出一个子值来进行排序,而不是直接对整个元素进行排序。
 *
 * LIMIT offset count (可选): 
   - offset: 指定排序后跳过的记录数(索引从0开始)。
   - count: 指定在排序结果中返回多少条记录。

 * GET pattern [GET pattern ...] (可选): 
   - 用于配合 BY 参数使用,指定从每个元素中获取哪些字段值用于排序。
   - 如果没有 BY 参数,这些 GET 指令将被忽略。

 * ASC | DESC (可选):
   - ASC 表示按升序排序,默认值。
   - DESC 表示按降序排序。

 * ALPHA (可选):
   - 当对字符串进行排序时,指定是否按照字典顺序(字母顺序)进行排序。

 * STORE destination (可选):
   - 将排序后的结果存储到一个新的键 `destination` 中,不返回排序结果,而是返回存储结果的元素个数。
 */

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 命令的功能。

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 参数还可以使用字符串类型作为参考键,你可以参考 SORT 命令 —BY选项 

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 参数将结果存储

关于 SORT 命令,我们还有很多没有说到的知识,如果你需要使用到更高级的排序功能,那么请参考 SORT 命令 吧!

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 命令来查看键的剩余时间(单位:毫秒)。

编程要求

根据提示,在右侧Begin-End区域补充代码,完成网络约车的后端处理逻辑:

  • 在 request_cab(user_id, priority) 方法中:
    • 判断是否存在哈希键 request:info:用户ID 的 time 域:
      • 提示:可使用 HEXISTS 命令
      • 若存在,则直接 return
  • 若不存在,做如下操作
    • 使用事务提交下列命令:
      • 将参数 user_id 从最左侧推入列表 cab:queue
  • 使用 HMSET 命令设置哈希键 request:info:用户ID:
    • 域 time,值为 time.time()
    • 域 priority,值为参数 priority
  • 将上述哈希键的过期时间设置为 10分钟
  • 在 allocate() 方法中:
    • 使用 SORT 命令对列表 cab:queue 排序,并将结果赋值给 cab_queue
      • 使用 BY 参数
    • 参考键为哈希键 request:info:*,其中 * 为占位符
    • 使用上述参考键中的 priority 域
  • 使用 DESC 参数做倒序排序
    • 取出 cab_queue 的第一个元素(下标为 0)赋值给 current_respond
    • 从列表 cab:queue 中移除变量 current_respond 中包含的元素
    • 返回(returncurrent_respond
测试说明

我会对你编写的代码进行测试:

测试输入:

1 2 3 4 5 6 7 8 9
9 8 7 6 5 4 3 2 1

预期输出:

Receive new request: 1, priority: 9, is_expired? True
Receive new request: 2, priority: 8, is_expired? True
Receive new request: 3, priority: 7, is_expired? True
Receive new request: 4, priority: 6, is_expired? True
Receive new request: 5, priority: 5, is_expired? True
Receive new request: 6, priority: 4, is_expired? True
Receive new request: 7, priority: 3, is_expired? True
Receive new request: 8, priority: 2, is_expired? True
Receive new request: 9, priority: 1, is_expired? True
Before: request queue: ['1', '2', '3', '4', '5', '6', '7', '8', '9']
Allocate new request: 1
After: request queue: ['2', '3', '4', '5', '6', '7', '8', '9']
Repeat request in few seconds:
Before: request queue length: 8
After: request queue length: 8
#!/usr/bin/env python
#-*- coding:utf-8 -*-

import time
import redis

conn = redis.Redis()

# 用户端发起派车请求
def request_cab(user_id, priority):
    # 请在下面完成要求的功能
    #********* Begin *********#
    if conn.hexists('request:info:' + str(user_id), 'time'):
        return
    # 开始事务
    pipe = conn.pipeline()

    pipe.lpush('cab:queue', user_id)
    
    pipe.execute()
    # 使用 HMSET 命令设置哈希键 request:info:用户ID
    conn.hmset('request:info:' + str(user_id), {'time': time.time(), 'priority': priority})

    conn.expire('request:info:' + str(user_id), 600)
    
    
    #********* End *********#

# 平台选择优先级最高的派车请求并派车
def allocate():
    # 请在下面完成要求的功能
    #********* Begin *********#
   
   # 使用 SORT 命令对列表 cab:queue 排序,并将结果赋值给 cab_queue:
    cab_queue = conn.sort('cab:queue', by='request:info:*->priority', desc=True)

    # 取出 cab_queue 的第一个元素(下标为 0)赋值给 current_respond
    current_respond = cab_queue[0] if cab_queue else None

    # 从列表 cab:queue 中移除变量 current_respond 中包含的元素
    if current_respond:
        conn.lrem('cab:queue', current_respond,1)

    # 返回(return)current_respond
    return current_respond
    #********* End *********#

# 用户端取消派车请求
def cancel_cab(user_id):
    # 请在下面完成要求的功能
    #********* Begin *********#
    conn.expire('request:info:' + str(user_id), 0)
    conn.lrem('cab:queue', user_id)
    #********* End *********#
  • 28
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

烟雨平生9527

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值