分页

分页程序对于每个包含大量资料的应用来说都是必不可少的,比方说:

  • 新闻应用通常会按照事件发生的先后顺序,使用分页程序分割最近出现的所有新闻。这样一来,用户只需要打开应用就能够看到最新发生的事件,并通过不断地向后翻页或滚动来查看之前发生的事件。

  • 短视频应用会通过分页程序,每次向用户推荐一批他可能会感兴趣的视频,用户可以从被推荐的视频中进行选择,又或者通过滚动屏幕获取上一批/下一批推荐视频。

  • 无论是博客还是微博客,通常都会根据博文的分布时间,按照从新到旧的顺序分割博客中的多篇博文,这样读者就可以通过不断地翻页来阅读更多文章。

除了上述场景之外,分页程序在网络论坛、社交应用、CMS系统和网购应用中也会频繁用到。

需求描述

你想要使用Redis实现分页程序,以此来为应用提供翻页浏览功能。

解决方案

实现分页功能的关键是要维持一个按位置排列元素的列表,这个列表需要保存多个元素,并记录每个元素的相对位置(也即是它们的索引)。在执行分页操作的时候,程序需要先根据指定的页数以及每页包含的元素数量计算出目标元素在列表中的索引范围,然后通过命令返回位于列表指定索引范围内的元素。

以表 TABLE_TOPICES 为例,假设现在有一个列表,它保存了20篇文章的ID,分别储存在列表索引019对应的元素中。这时如果我们需要以每页5篇文章的方式对这个列表进行分页,那么程序应该在第一页返回位于索引04上的文章ID,在第二页返回位于索引59上的文章ID,在第三页返回位于索引1014上的文章ID,并在第四页返回位于索引1519上的文章ID。


表 TABLE_TOPICES 储存文章ID的列表

索引

文章ID

0

topic:10086

1

topic:10001

2

topic:10000

3

topic:9500

4

topic:9321

5

topic:9005

6

topic:9004

....

...

18

topic:8123

19

topic:8007


为了实现具有上述特性的列表,程序可以使用Redis列表作为分页程序的底层数据结构,而各个列表命令则分别用于实现不同的分页操作:

  • 添加被分页元素的工作可以通过执行LPUSH命令来完成,在持续向列表推入多个元素之后,越接近列表左端的元素就越新,也越早会被程序返回,而越接近列表右端的元素就越旧,也越晚会被程序返回。

  • 获取被分页元素的工作可以通过执行LRANGE命令来完成,其中被返回元素的索引区间需要根据两个参数计算得出:1)想要获取第几页;2)每页需要返回的多少个元素。

  • 获取被分页元素总数量的工作可以通过执行LLEN命令来完成,至于分页程序需要处理的总页数则可以通过计算元素总数量除以每页返回元素数量的商得出。

比如说,通过执行以下命令序列,程序可以向列表TopicList推入20个代表文章ID的元素:

redis> LPUSH TopicList topic:8007
(integer) 1
redis> LPUSH TopicList topic:8123
(integer) 2
redis> LPUSH TopicList topic:8141
(integer) 3
-- 省略部分LPUSH命令
redis> LPUSH TopicList topic:10001
(integer) 19
redis> LPUSH TopicList topic:10086
(integer) 20

在此之后,程序可以通过以下命令序列,以每5个元素为一页的方式,分别获取这个列表第1至第4页的元素:

redis> LRANGE TopicList 0 4
1) "topic:10086"
2) "topic:10001"
3) "topic:10000"
4) "topic:9500"
5) "topic:9321"
redis> LRANGE TopicList 5 9
1) "topic:9005"
2) "topic:9004"
3) "topic:8856"
4) "topic:8696"
5) "topic:8482"
redis> LRANGE TopicList 10 14
1) "topic:8323"
2) "topic:8293"
3) "topic:8269"
4) "topic:8205"
5) "topic:8188"
redis> LRANGE TopicList 15 19
1) "topic:8175"
2) "topic:8151"
3) "topic:8141"
4) "topic:8123"
5) "topic:8007"

还可以通过LLEN命令获取列表的总长度:

redis> LLEN TopicList
(integer) 20

考虑到列表里共有20个元素,如果分页程序以每5个元素为一页,那么整个列表共可以分为4页;而如果以每10个元素一页,那么整个列表共可以分为2页。

实现代码

代码清单 CODE_PAGGING 展示了基于上述原理实现的分页程序。


代码清单 CODE_PAGGING 分页程序 pagging.py

from math import ceil  # 向下取整函数

DEFAULT_SIZE = 10  # 默认每页返回10个元素

class Pagging:

    def __init__(self, client, key):
        self.client = client
        self.key = key

    def add(self, *items):
        """
        将给定的一个或多个元素推入到分页列表中,
        成功时返回列表当前包含的元素数量作为结果。
        """
        return self.client.lpush(self.key, *items)

    def get(self, page, size=DEFAULT_SIZE):
        """
        从指定分页中取出指定数量的元素。
        """
        # 根据给定的页数和元素数量计算出索引范围
        start = (page-1)*size
        end = page*size-1
        # 根据索引从分页列表中获取元素
        return self.client.lrange(self.key, start, end)

    def length(self):
        """
        返回分页列表包含的元素总数量。
        """
        return self.client.llen(self.key)

    def number(self, size=DEFAULT_SIZE):
        """
        返回在获取指定数量的元素时,分页列表包含的页数量。
        如果分页列表为空则返回0。
        """
        return ceil(self.length()/size)

作为例子,以下代码展示了如何使用这个分页程序构建一个包含九个元素的分页列表,然后以每页三个元素的方式返回第一、第二和第三页的各个元素:

>>> from redis import Redis
>>> from pagging import Pagging
>>> client = Redis(decode_responses=True)
>>> page = Pagging(client, "TopicList")
>>> for i in range(1, 10):  # 添加被分页元素
...   page.add("topic:{}".format(i))
...
1
2
# ...
9
>>> page.length()  # 获取元素总数量
9
>>> page.number(3)  # 获取总页数
3
>>> page.get(1, 3)  # 获取分页元素
['topic:9', 'topic:8', 'topic:7']
>>> page.get(2, 3)
['topic:6', 'topic:5', 'topic:4']
>>> page.get(3, 3)
['topic:3', 'topic:2', 'topic:1']

重点回顾

  • 分页程序对于每个包含大量资料的应用来说都是必不可少的,无论是新闻应用、短视频应用还是博客、网络论坛和CMS系统,分页都随处可见。

  • 实现分页功能的关键是要维持一个按位置排列元素的列表,这个列表需要保存多个元素,并记录每个元素的相对位置(也即是它们的索引)。

  • 在执行分页操作的时候,程序需要先根据指定的页数以及每页包含的元素数量计算出目标元素在列表中的索引范围,然后通过命令返回位于列表指定索引范围内的元素。

  • 为了在Redis中实现分页程序,程序可以把Redis列表用作底层数据结构,而各个列表命令则分别用于实现不同的分页操作:其中LPUSH命令用于添加被分页元素,LRANGE命令用于获取被分页元素,而LLEN命令则用于获取列表包含的元素数量。