Redis入门--头歌实验Redis基本命令 谁践踏了优雅 2024-04-17 13:48 21阅读 0赞 > `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`字符串执行自增和自减的命令列表如下: > > <table> > <thead> > <tr> > <th>命令</th> > <th>用法</th> > <th>说明</th> > </tr> > </thead> > <tbody> > <tr> > <td><code>INCR</code></td> > <td><code>INCR key</code></td> > <td>将 <code>key</code> 存储的值加上 <code>1</code></td> > </tr> > <tr> > <td><code>DECR</code></td> > <td><code>DECR key</code></td> > <td>将 <code>key</code> 存储的值减去 <code>1</code></td> > </tr> > <tr> > <td><code>INCRBY</code></td> > <td><code>INCRBY key increment</code></td> > <td>将 <code>key</code> 存储的值加上 <code>increment</code></td> > </tr> > <tr> > <td><code>DECRBY</code></td> > <td><code>DECRBY key decrement</code></td> > <td>将 <code>key</code> 存储的值减去 <code>decrement</code></td> > </tr> > <tr> > <td><code>INCRBYFLOAT</code></td> > <td><code>INCRBYFLOAT key increment</code></td> > <td>将 <code>key</code> 存储的值加上<strong>浮点数</strong> <code>increment</code></td> > </tr> > </tbody> > </table> > > > **注意:**`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 > > ###### 字节串操作 ###### > > `Redis`还可以对字节串的一部分内容进行读取/写入: > > <table> > <thead> > <tr> > <th>命令</th> > <th>用法</th> > <th>说明</th> > </tr> > </thead> > <tbody> > <tr> > <td><code>APPEND</code></td> > <td><code>APPEND key value</code></td> > <td>将 <code>value</code> 追加到 <code>key</code> 键存储的值的<strong>末尾</strong></td> > </tr> > <tr> > <td><code>GETRANGE</code></td> > <td><code>GETRANGE key start end</code></td> > <td>获取 <code>start</code> 到 <code>end</code> 间的子串</td> > </tr> > <tr> > <td><code>SETRANGE</code></td> > <td><code>SETRANGE key offset value</code></td> > <td>从 <code>start</code> 偏移量开始,将与 <code>value</code> 长度一致的子串设置为 <code>value</code></td> > </tr> > </tbody> > </table> > > 在使用 `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`提供了丰富的列表操作命令,从而使得列表的应用场景非常广泛,例如:存储任务队列,记录最近的操作/数据变化,作为日志收集器等。 > > 首先我们介绍一些常用的列表命令: > > <table> > <thead> > <tr> > <th>命令</th> > <th>用法</th> > <th>说明</th> > </tr> > </thead> > <tbody> > <tr> > <td><code>LPUSH</code></td> > <td><code>LPUSH key value [value ...]</code></td> > <td>将一个或多个 <code>value</code> 推入到列表的左侧</td> > </tr> > <tr> > <td><code>RPUSH</code></td> > <td><code>RPUSH key value [value ...]</code></td> > <td>将一个或多个 <code>value</code> 推入到列表的右侧</td> > </tr> > <tr> > <td><code>LLEN</code></td> > <td><code>LLEN key</code></td> > <td>返回列表 <code>key</code> 的长度</td> > </tr> > <tr> > <td><code>LREM</code></td> > <td><code>LREM key count value</code></td> > <td>根据参数 <code>count</code> 的值,移除列表中与参数 <code>value</code> 相等的元素</td> > </tr> > </tbody> > </table> > > 加上我们在上一个实训中已经介绍过的弹出、获取元素等命令,就构成了最为常用的列表命令。使用 `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客户端主页 ][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 提供了针对单个集合以及多集合间处理的命令: > > <table> > <thead> > <tr> > <th>命令</th> > <th>用法</th> > <th>说明</th> > </tr> > </thead> > <tbody> > <tr> > <td><code>SCARD</code></td> > <td><code>SCARD key</code></td> > <td>返回集合 <code>key</code> 中元素的数量</td> > </tr> > <tr> > <td><code>SRANDMEMBER</code></td> > <td><code>SRANDMEMBER key [count]</code></td> > <td>返回集合中的 <code>1</code> 或 <code>count</code> 个随机元素</td> > </tr> > <tr> > <td><code>SPOP</code></td> > <td><code>SPOP key</code></td> > <td><strong>移除</strong>并返回集合中的一个随机元素</td> > </tr> > <tr> > <td><code>SMOVE</code></td> > <td><code>SMOVE source destination member</code></td> > <td>将 <code>member</code> 元素从 <code>source</code> 集合移动到 <code>destination</code> 集合</td> > </tr> > </tbody> > </table> > > 我们通过一些示例来展示上述命令的用法: > > >>> 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`集合还有更为强大的功能 —— 组合和关联多个集合: > > <table> > <thead> > <tr> > <th>命令</th> > <th>用法</th> > <th>说明</th> > </tr> > </thead> > <tbody> > <tr> > <td><code>SDIFF</code></td> > <td><code>SDIFF key [key ...]</code></td> > <td>返回所有给定集合之间的<strong>差集</strong></td> > </tr> > <tr> > <td><code>SINTER</code></td> > <td><code>SINTER key [key ...]</code></td> > <td>返回所有给定集合的<strong>交集</strong></td> > </tr> > <tr> > <td><code>SUNION</code></td> > <td><code>SUNION key [key ...]</code></td> > <td>返回所有给定集合的<strong>并集</strong></td> > </tr> > </tbody> > </table> > > 上述三个命令是差集,交集,并集运算的“返回结果”版本,同时 `Redis`还提供了“存储结果”版本,你可以参考 [Redis 命令参考 ][Redis _]中的 `SDIFFSTORE`,`SINTERSTORE` 和 `SUNIONSTORE` 命令。 > > ##### 编程要求 ##### > > 根据提示,在右侧`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` 中 > * **返回(`return`)`staff` 的值** > * 在 `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`键中,使得哈希十分适合将一些相关的数据存储在一起。我们可以把这种数据看作是关系型数据库中的行。 > > 常用的哈希命令包括之前介绍过的添加和删除域-值对命令、获取所有域-值对命令以及对域-值对的值进行自增/自减操作的命令: > > <table> > <thead> > <tr> > <th>命令</th> > <th>用法</th> > <th>说明</th> > </tr> > </thead> > <tbody> > <tr> > <td><code>HMSET</code></td> > <td><code>HMSET key field value [field value ...]</code></td> > <td>同时将多个 <code>field-value</code> (域-值)对设置到哈希表 <code>key</code> 中</td> > </tr> > <tr> > <td><code>HMGET</code></td> > <td><code>HMGET key field [field ...]</code></td> > <td>返回哈希表 <code>key</code> 中,一或多个给定域的值</td> > </tr> > <tr> > <td><code>HDEL</code></td> > <td><code>HDEL key field [field ...]</code></td> > <td>删除哈希表 <code>key</code> 中的一或多个指定域</td> > </tr> > <tr> > <td><code>HLEN</code></td> > <td><code>HLEN key</code></td> > <td>返回哈希表 <code>key</code> 中域的数量</td> > </tr> > </tbody> > </table> > > 在上一个实训中,我们使用过 `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`哈希还支持一些更高级的批量操作: > > <table> > <thead> > <tr> > <th>命令</th> > <th>用法</th> > <th>说明</th> > </tr> > </thead> > <tbody> > <tr> > <td><code>HEXISTS</code></td> > <td><code>HEXISTS key field</code></td> > <td>查看哈希表 <code>key</code> 中,给定域 <code>field</code> 是否存在</td> > </tr> > <tr> > <td><code>HKEYS</code></td> > <td><code>HKEYS key</code></td> > <td>返回哈希表 <code>key</code> 中所有域</td> > </tr> > <tr> > <td><code>HVALS</code></td> > <td><code>HVALS key</code></td> > <td>返回哈希表 <code>key</code> 中所有域的值</td> > </tr> > <tr> > <td><code>HINCRBY</code></td> > <td><code>HINCRBY key field increment</code></td> > <td>为哈希表 <code>key</code> 中的域 <code>field</code> <strong>的值</strong>加上 <code>increment</code></td> > </tr> > </tbody> > </table> > > 在哈希包含的值的体积都十分大时,我们应该使用 `HKEYS` 命令获取所有的域,再使用 `HGET` 一个个的从哈希中取出域的值,从而避免 `Redis`因为一次性获取多个大体积的值而导致服务器阻塞。甚至,我们可以只获取必要的值来减少传输的数据量。 > > ###### 常用有序集合命令 ###### > > 有序集合与哈希类似,也存储着成员(`member`)和分值(`score`)之间的映射关系。`Redis`为有序集合提供了分值处理命令,并能根据分值大小有序的排列成员: > > <table> > <thead> > <tr> > <th>命令</th> > <th>用法</th> > <th>说明</th> > </tr> > </thead> > <tbody> > <tr> > <td><code>ZCARD</code></td> > <td><code>ZCARD key</code></td> > <td>返回有序集合 <code>key</code> 的成员总数</td> > </tr> > <tr> > <td><code>ZCOUNT</code></td> > <td><code>ZCOUNT key min max</code></td> > <td>返回有序集合 <code>key</code> 中, <code>score</code> 值在 <code>min</code> 和 <code>max</code> 之间的成员数量</td> > </tr> > <tr> > <td><code>ZRANK</code></td> > <td><code>ZRANK key member</code></td> > <td>返回有序集合 <code>key</code> 中成员 <code>member</code> 的排名</td> > </tr> > <tr> > <td><code>ZSCORE</code></td> > <td><code>ZSCORE key member</code></td> > <td>返回有序集合 <code>key</code> 中,成员 <code>member</code> 的分值</td> > </tr> > </tbody> > </table> > > 值得一提的是,之前提过的 `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` 命令有三个参数,依次为 `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") > > ##### 编程要求 ##### > > 根据提示,在右侧`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"` > * **返回(`return`)`current_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_info`,`task_2_info`,`task_3_info`,`task_4_info` 哈希键中的 `time` 域**的值**,并根据这个值将 `task_queue` 中的任务 `ID` 排序。 > > 当然 `BY` 参数还可以使用字符串类型作为参考键,你可以参考 [SORT 命令 —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 命令 ][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` 中包含的元素 > * **返回(`return`)`current_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 *********# [redis_]: https://github.com/andymccurdy/redis-py [Redis _]: http://doc.redisfans.com/ [SORT _ _BY_]: http://doc.redisfans.com/key/sort%EF%BC%8Ehtml#by [SORT _]: http://doc.redisfans.com/key/sort%EF%BC%8Ehtml
还没有评论,来说两句吧...