自动过期

在构建应用时, 我们常常会碰到一些在特定时间之后就不再有用的数据, 比如说:

  • 随着内容的不断更新, 一个网页的缓存可能在 5 分钟之后就没有阅读价值了, 为了让用户能够及时地获取到最新的信息, 程序必须定期地移除旧缓存并设置新缓存。

  • 为了保障用户的信息安全, 应用通常会在用户登录一周或者一个月之后移除用户的会话信息, 然后通过强制要求用户重新登录来创建新的会话。

  • 程序在进行聚合计算的时候, 常常会创建出大量临时数据, 这些数据在计算完毕之后通常就不再有用, 而且储存这些数据还会花费大量内存空间和硬盘空间。

在遇到上述情况时, 我们虽然可以自行编写程序来处理这些不再有用的数据, 但如果数据库本身能够提供自动移除无用数据的功能, 那么就会给我们带来非常大的方便。

为了解决这个问题, Redis 提供了自动的键过期功能(key expiring)。 通过这个功能, 用户可以让特定的键在指定的时间之后自动被移除, 从而避免了需要在指定时间内手动执行删除操作的麻烦。

本章将对 Redis 的键过期功能进行介绍, 说明与该功能有关的各个命令的使用方法, 并展示如何使用这一功能去构建一些非常实用的程序。

EXPIRE、PEXPIRE:设置生存时间

用户可以通过执行 EXPIRE 命令或者 PEXPIRE 命令, 为键设置一个生存时间(TTL,time to live): 键的生存时间在设置之后就会随着时间的流逝而不断地减少, 当一个键的生存时间被消耗殆尽时, Redis 就会移除这个键。

Redis 提供了 EXPIRE 命令用于设置秒级精度的生存时间, 它可以让键在指定的秒数之后自动被移除:

EXPIRE key seconds

PEXPIRE 命令则用于设置毫秒级精度的生存时间, 它可以让键在指定的毫秒数之后自动被移除:

PEXPIRE key milliseconds

EXPIRE 命令和 PEXPIRE 命令在生存时间设置成功时返回 1 ; 如果用户给定的键并不存在, 那么命令返回 0 表示设置失败。

以下是一个使用 EXPIRE 命令的例子:

redis> SET msg "hello world"
OK

redis> EXPIRE msg 5
(integer) 1

redis> GET msg    -- 在 5 秒钟之内访问,键存在
"hello world"

redis> GET msg    -- 在 5 秒钟之后访问,键不再存在
(nil)

上面的代码通过执行 EXPIRE 命令为 msg 键设置了 5 秒钟的生存时间:

  • 如果我们在 5 秒钟之内访问 msg 键, 那么 Redis 将返回 msg 键的值 "hello world"

  • 但如果我们在 5 秒钟之后访问 msg 键, 那么 Redis 将返回一个空值, 因为 msg 键已经自动被移除了。

表 12-1 展示了 msg 键从设置生存时间到被移除的整个过程。


表 12-1 msg 键从设置生存时间到被移除的整个过程

时间(以秒为单位)

动作

0000

执行 EXPIRE msg 5 ,将 msg 键的生存时间设置为 5 秒钟。

0001

msg 键的生存时间变为 4 秒钟。

0002

msg 键的生存时间变为 3 秒钟。

0003

msg 键的生存时间变为 2 秒钟。

0004

msg 键的生存时间变为 1 秒钟。

0005

msg 键因为过期被移除。


而以下则是一个使用 PEXPIRE 命令的例子:

redis> SET number 10086
OK

redis> PEXPIRE number 6500
(integer) 1

redis> GET number    -- 在 6500 毫秒(也即是 6.5 秒)之内访问,键存在
"10086"

redis> GET number    -- 在 6500 毫秒之后访问,键不再存在
(nil)

表 12-2 展示了 number 键从设置生存时间到被移除的整个过程。


表 12-2 number 键从设置生存时间到被移除的整个过程

时间(以毫秒为单位)

动作

0000

执行 PEXPIRE number 6500 ,将 number 键的生存时间设置为 6500 毫秒。

0001

number 键的生存时间变为 6499 毫秒。

0002

number 键的生存时间变为 6498 毫秒。

0003

number 键的生存时间变为 6497 毫秒。

……

……

6497

number 键的生存时间变为 3 毫秒。

6498

number 键的生存时间变为 2 毫秒。

6499

number 键的生存时间变为 1 毫秒。

6500

number 键因为过期而被移除。

更新键的生存时间

当用户对一个已经带有生存时间的键执行 EXPIRE 命令或是 PEXPIRE 命令时, 键原有的生存时间将会被移除, 并设置上新的生存时间。

举个例子, 如果我们执行以下命令, 将 msg 键的生存时间设置为 10 秒钟:

redis> EXPIRE msg 10
(integer) 1

然后在 10 秒钟之内执行以下命令:

redis> EXPIRE msg 50
(integer) 1

那么 msg 键的生存时间将被更新为 50 秒钟, 并重新开始倒数, 表 12-3 展示了这个更新过程。


表 12-3 msg 键生存时间的更新过程

时间(以秒为单位)

动作

0000

执行 EXPIRE msg 10 命令, 将 msg 键的生存时间设置为 10 秒钟。

0001

msg 键的生存时间变为 9 秒钟。

0002

msg 键的生存时间变为 8 秒钟。

0003

msg 键的生存时间变为 7 秒钟。

0004

执行 EXPIRE msg 50 命令, 将 msg 键的生存时间更新为 50 秒钟。

0005

msg 键的生存时间变为 49 秒钟。

0006

msg 键的生存时间变为 48 秒钟。

0007

msg 键的生存时间变为 47 秒钟。

……

……


其他信息

属性

复杂度

EXPIRE 命令和 PEXPIRE 命令的复杂度都为 O(1)

版本要求

EXPIRE 命令从 Redis 1.0.0 版本开始可用, PEXPIRE 命令从 Redis 2.6.0 版本开始可用。

示例:带有自动移除特性的缓存程序

用户在使用缓存程序的时候, 必须要考虑缓存的时效性: 对于内容不断变换的应用来说, 一份缓存存在的时间越长, 它与实际内容之间的差异往往也就越大, 因此为了让缓存能够及时地反映真实的内容, 程序必须定期对缓存进行更新。

本书前面在《字符串》一章曾经展示过怎样使用字符串键构建缓存程序, 但那个缓存程序有一个明显的缺陷, 那就是, 它无法自动移除过时的缓存。 如果我们真的要在实际中使用那个程序的话, 那么就必须再编写一个辅助程序来定期地删除旧缓存才行, 这样一来使用缓存将会变得非常麻烦。

幸运的是, 通过使用 Redis 的键过期功能, 我们可以为缓存程序加上自动移除特性, 并通过这个特性自动移除过期的、无效的缓存。

代码清单 12-1 展示了一个能够为缓存设置最大有效时间的缓存程序, 这个程序跟《字符串》一章展示的缓存程序的绝大部分代码都是相同的, 新程序的主要区别在于, 它除了会把指定的内容缓存起来之外, 还会使用 EXPIRE 命令为缓存设置生存时间, 从而使得缓存可以在指定时间到达之后自动被移除。


代码清单 12-1 带有自动移除特性的缓存程序:/expire/unsafe_volatile_cache.py

class VolatileCache:

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

    def set(self, key, value, timeout):
        """
        把数据缓存到键 key 里面,并为其设置过期时间。
        如果键 key 已经有值,那么使用新值去覆盖旧值。
        """
        self.client.set(key, value)
        self.client.expire(key, timeout)

    def get(self, key):
        """
        获取键 key 储存的缓存数据。
        如果键不存在,又或者缓存已经过期,那么返回 None 。
        """
        return self.client.get(key)

以下代码简单地展示了这个缓存程序的使用方法:

>>> from redis import Redis
>>> from unsafe_volatile_cache import VolatileCache
>>> client = Redis(decode_responses=True)
>>> cache = VolatileCache(client)
>>> cache.set("homepage", "<html><p>hello world</p></html>", 10)  # 设置缓存
>>> cache.get("homepage")  # 这个缓存在 10 秒钟之内有效
'<html><p>hello world</p></html>'
>>> cache.get("homepage")  # 10 秒钟过后,缓存自动被移除
>>>

SET 命令的 EX 选项和 PX 选项

在使用键过期功能时, 组合使用 SET 命令和 EXPIRE / PEXIRE 命令的做法非常常见, 比如上面展示的带有自动移除特性的缓存程序就是这样做的。

因为 SET 命令和 EXPIRE / PEXPIRE 命令组合使用的情况是如此的常见, 所以为了方便用户使用这两组命令, Redis 从 2.6.12 版本开始为 SET 命令提供 EX 选项和 PX 选项, 用户可以通过使用这两个选项的其中一个来达到同时执行 SET 命令和 EXPIRE / PEXPIRE 命令的效果:

SET key value [EX seconds] [PX milliseconds]

这也就是说, 如果我们之前执行的是 SET 命令和 EXPIRE 命令:

SET key value
EXPIRE key seconds

那么现在只需要执行一条带有 EX 选项的 SET 命令就可以了:

SET key value EX seconds

与此类似, 如果我们之前执行的是 SET 命令和 PEXPIRE 命令:

SET key value
PEXPIRE key milliseconds

那么现在只需要执行一条带有 PX 选项的 SET 命令就可以了:

SET key value PX milliseconds

组合命令的安全问题

使用带有 EX 选项或 PX 选项的 SET 命令除了可以减少命令的调用数量并提升程序的执行速度之外, 更重要的是保证了操作的原子性, 使得“为键设置值”和“为键设置生存时间”这两个操作可以一起执行。

比如说, 前面在实现带有自动移除特性的缓存程序时, 我们首先使用了 SET 命令设置缓存, 然后又使用了 EXPIRE 命令为缓存设置生存时间, 这相当于让程序依次地向 Redis 服务器发送以下两条命令:

SET key value

EXPIRE key timeout

因为这两条命令是完全独立的, 所以服务器在执行它们的时候, 就可能会出现 SET 命令被执行了, 但是 EXPIRE 命令却没有被执行的情况。 比如说, 如果 Redis 服务器在成功执行 SET 命令之后因为故障下线, 导致 EXPIRE 命令没有被执行, 那么 SET 命令设置的缓存就会一直存在, 而不会因为过期而自动被移除。

与此相反, 使用带有 EX 选项或 PX 选项的 SET 命令就没有这个问题: 当服务器成功执行了一条带有 EX 选项或 PX 选项的 SET 命令时, 键的值和生存时间都会同时被设置好, 因此程序就不会出现只设置了值但是却没有设置生存时间的情况。

基于上述原因, 我们把前面展示的缓存程序实现称之为“不安全”(unsafe)实现。 为了修复这个问题, 我们可以使用带有 EX 选项的 SET 命令来重写缓存程序, 重写之后的程序正如代码清单 12-2 所示。


代码清单 12-2 重写之后的缓存程序:/expire/volatile_cache.py

class VolatileCache:

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

    def set(self, key, value, timeout):
        """
        把数据缓存到键 key 里面,并为其设置过期时间。
        如果键 key 已经有值,那么使用新值去覆盖旧值。
        """
        self.client.set(key, value, ex=timeout)

    def get(self, key):
        """
        获取键 key 储存的缓存数据。
        如果键不存在,又或者缓存已经过期,那么返回 None 。
        """
        return self.client.get(key)

重写之后的缓存程序实现是“安全的”: 设置缓存和设置生存时间这两个操作要么就一起成功, 要么就一起失败, “设置缓存成功了,但是设置生存时间却失败了”这样的情况将不会出现。 后续的章节也会介绍如何通过 Redis 的事务功能来保证执行多条命令时的安全性。

其他信息

属性

复杂度

O(1)

版本要求

带有 EX 选项和 PX 选项的 SET 命令从 Redis 2.6.12 版本开始可用。

示例:带有自动释放特性的锁

在前面的《字符串》一章, 我们曾经实现过一个锁程序, 它的其中一个缺陷就是无法自行释放: 如果锁的持有者因为故障下线, 那么锁将一直处于持有状态, 导致其他进程永远也无法获得锁。

为了解决这个问题, 我们可以在获取锁的同时, 通过 Redis 的自动过期特性为锁设置一个最大加锁时限, 这样的话, 即使锁的持有者由于故障下线, 锁也会在时限到达之后自动释放。

代码清单 12-3 展示了使用上述原理实现的锁程序。


代码清单 12-3 带有自动释放特性的锁:/expire/timing_lock.py

VALUE_OF_LOCK = "locking"

class TimingLock:

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

    def acquire(self, timeout):
        """
        尝试获取一个带有秒级最大使用时限的锁,
        成功时返回 True ,失败时返回 False 。
        """
        result = self.client.set(self.key, VALUE_OF_LOCK, ex=timeout, nx=True)
        return result is not None

    def release(self):
        """
        尝试释放锁。
        成功时返回 True ,失败时返回 False 。 
        """
        return self.client.delete(self.key) == 1

以下代码演示了这个锁的自动释放特性:

>>> from redis import Redis
>>> from timing_lock import TimingLock
>>> client = Redis()
>>> lock = TimingLock(client, "test-lock")
>>> lock.acquire(5)  # 获取一个在 5 秒钟之后自动释放的锁
True
>>> lock.acquire(5)  # 在 5 秒钟之内尝试再次获取锁,但是由于锁未被释放而失败
False
>>> lock.acquire(5)  # 在 5 秒钟之后尝试再次获取锁
True                 # 因为之前获取的锁已经自动被释放,所以这次将成功取得新的锁

EXPIREAT、PEXPIREAT:设置过期时间

Redis 用户不仅可以通过设置生存时间来让键在指定的秒数或毫秒数之后自动被移除, 还可以通过设置过期时间(expire time), 让 Redis 在指定 UNIX 时间来临之后自动移除给定的键。

设置过期时间这一操作可以通过 EXPIREAT 命令或者 PEXPIREAT 命令来完成。 其中, EXPIREAT 命令接受一个键和一个秒级精度的 UNIX 时间戳为参数, 当系统的当前 UNIX 时间超过命令指定的 UNIX 时间时, 给定的键就会被移除:

EXPIREAT key seconds_timestamp

与此类似, PEXPIREAT 命令接受一个键和一个毫秒级精度的 UNIX 时间戳为参数, 当系统的当前 UNIX 时间超过命令指定的 UNIX 时间时, 给定的键就会被移除:

PEXPIREAT key milliseconds_timestamp

EXPIREAT 使用示例

如果我们想要让 msg 键在 UNIX 时间 1450005000 秒之后不再存在, 那么可以执行以下命令:

redis> EXPIREAT msg 1450005000
(integer) 1

在执行这个 EXPIREAT 命令之后, 如果我们在 UNIX 时间 1450005000 秒或之前访问 msg 键, 那么 Redis 将返回 msg 键的值:

redis> GET msg
"hello world"

另一方面, 如果我们在 UNIX 时间 1450005000 秒之后访问 msg 键, 那么 Redis 将返回一个空值, 因为这时 msg 键已经因为过期而自动被移除了:

redis> GET msg
(nil)

表 12-4 展示了 msg 键从设置过期时间到被移除的整个过程。


表 12-4 msg 键从设置过期时间到被移除的整个过程

UNIX 时间(以秒为单位)

动作

1450004000

执行 EXPIREAT msg 1450005000 命令, 将 msg 键的过期时间设置为 1450005000 秒。

1450004001

msg 键未过期,不做动作。

1450004002

msg 键未过期,不做动作。

1450004003

msg 键未过期,不做动作。

……

……

1450004999

msg 键未过期,不做动作。

1450005000

msg 键未过期,不做动作。

1450005001

系统当前的 UNIX 时间已经超过 1450005000 秒,移除 msg 键。


PEXPIREAT 命令使用示例

以下是一个使用 PEXPIREAT 命令设置过期时间的例子, 这个命令可以将 number 键的过期时间设置为 UNIX 时间 1450005000000 毫秒:

redis> PEXPIREAT number 1450005000000
(integer) 1

在 UNIX 时间 1450005000000 毫秒或之前访问 number 键可以得到它的值:

redis> GET number
"10086"

而在 UNIX 时间 1450005000000 毫秒之后访问 number 键则只会得到一个空值, 因为这时 number 键已经因为过期而自动被移除了:

redis> GET number
(nil)

表 12-5 展示了 number 键从设置过期时间到被移除的整个过程。


表 12-5 number 键从设置过期时间到被移除的整个过程

UNIX 时间(以毫秒为单位)

动作

1450003000000

执行 PEXPIREAT number 1450005000000 命令, 将 number 键的过期时间设置为 1450005000000 毫秒。

1450003000001

number 键未过期,不做动作。

1450003000002

number 键未过期,不做动作。

1450003000003

number 键未过期,不做动作。

……

……

1450004999999

number 键未过期,不做动作。

1450005000000

number 键未过期,不做动作。

1450005000001

系统当前的 UNIX 时间已经超过 1450005000000 毫秒,移除 number 键。


更新键的过期时间

EXPIRE / PEXPIRE 命令会更新键的生存时间一样, EXPIREAT / PEXPIREAT 命令也会更新键的过期时间: 如果用户在执行 EXPIREAT 命令或 PEXPIREAT 命令的时候, 给定键已经带有过期时间, 那么命令首先会移除键已有的过期时间, 然后再为其设置新的过期时间。

比如在以下调用中, 第二条 EXPIREAT 命令就将 msg 键的过期时间从原来的 1500000000 修改成了 1600000000

redis> EXPIREAT msg 1500000000
(integer) 1

redis> EXPIREAT msg 1600000000
(integer) 1

自动过期特性的不足之处

无论是本节介绍的 EXPIREAT / PEXPIREAT , 还是前面介绍的 EXPIRE / PEXIRE , 它们都只能对整个键进行设置, 而无法对键中的某个元素进行设置: 比如说, 用户只能对整个集合或者整个散列设置生存时间/过期时间, 但是却无法为集合中的某个元素或者散列中的某个字段单独设置生存时间/过期时间, 这也是目前 Redis 的自动过期功能不足的一个地方。

其他信息

属性

复杂度

EXPIREAT 命令和 PEXPIREAT 命令的复杂度都为 O(1)

版本要求

EXPIREAT 命令从 Redis 1.2.0 版本开始可用, PEXPIREAT 命令从 Redis 2.6.0 版本开始可用。

TTL、PTTL:获取键的剩余生存时间

在为键设置了生存时间或者过期时间之后, 用户可以使用 TTL 命令或者 PTTL 命令查看键的剩余生存时间, 也即是, 键还有多久才会因为过期而被移除。

其中, TTL 命令将以秒为单位返回键的剩余生存时间:

TTL key

PTTL 命令则会以毫秒为单位返回键的剩余生存时间:

PTTL key

作为例子, 以下代码展示了如何使用 TTL 命令和 PTTL 命令去获取 msg 键的剩余生存时间:

redis> TTL msg
(integer) 297       -- msg 键距离被移除还有 297 

redis> PTTL msg
(integer) 295561    -- msg 键距离被移除还有 295561 毫秒

没有剩余生存时间的键和不存在的键

如果给定的键存在, 但是并没有设置生存时间或者过期时间, 那么 TTL 命令和 PTTL 命令将返回 -1

redis> SET song_title "Rise up, Rhythmetal"
OK

redis> TTL song_title
(integer) -1

redis> PTTL song_title
(integer) -1

另一方面, 如果给定的键并不存在, 那么 TTL 命令和 PTTL 命令将返回 -2

redis> TTL not_exists_key
(integer) -2

redis> PTTL not_exists_key
(integer) -2

TTL 命令的精度问题

在使用 TTL 命令时, 我们有时候会碰到命令返回 0 的情况:

redis> TTL msg
(integer) 0

出现这种情况的原因在于 TTL 命令只能返回秒级精度的生存时间, 所以当给定键的剩余生存时间不足一秒钟时, TTL 命令只能返回 0 作为结果。 这时, 如果我们使用精度更高的 PTTL 命令去检查这些键, 那么就会看到它们实际的剩余生存时间, 表 12-6 非常详细地描述了这一情景。


表 12-6 PTTL 命令在 TTL 命令返回 0 时仍然可以检测到键的剩余生存时间

键的剩余生存时间(以毫秒为单位)

TTL 命令的返回值

PTTL 命令的返回值

1001

1

1001

1000

1

1000

999

0

999

998

0

998

997

0

997

……

……

……

2

0

2

1

0

1

0

0

0

-2(键已被移除)

-2

-2


其他信息

属性

复杂度

TTL 命令和 PTTL 命令的复杂度都为 O(1) 。

版本要求

TTL 命令从 Redis 1.0.0 版本开始可用, PTTL 命令从 Redis 2.6.0 版本开始可用。

示例:自动过期的登录会话

在前面的《散列》一章, 我们了解到了如何使用散列去构建一个会话程序。 正如 12-1 所示, 当时的会话程序会使用两个散列分别储存会话的令牌以及过期时间戳。 这种做法虽然可行, 但是储存过期时间戳需要消耗额外的内存, 并且判断会话是否过期也需要用到额外的代码。


图 12-1 会话程序创建的散列数据结构

_images/IMAGE_SESSION_HASHS.png

在学习了 Redis 的自动过期特性之后, 我们可以对会话程序进行修改, 通过给会话令牌设置过期时间来让它在指定的时间之后自动被移除。 这样一来, 程序只需要检查会话令牌是否存在, 就能够知道是否应该让用户重新登录了。

代码清单 12-4 展示了修改之后的会话程序。 因为 Redis 的自动过期特性只能对整个键使用, 所以这个程序使用了字符串而不是散列来储存会话令牌, 但总的来说, 这个程序的逻辑跟之前的会话程序的逻辑基本相同。 不过由于新程序无需手动检查会话是否过期, 所以它的逻辑简洁了不少。


代码清单 12-4 带有自动过期特性的会话程序:/expire/login_session.py

import random
from hashlib import sha256

# 会话的默认过期时间
DEFAULT_TIMEOUT = 3600*24*30    # 一个月

# 会话状态
SESSION_NOT_LOGIN_OR_EXPIRED = "SESSION_NOT_LOGIN_OR_EXPIRED"
SESSION_TOKEN_CORRECT = "SESSION_TOKEN_CORRECT"
SESSION_TOKEN_INCORRECT = "SESSION_TOKEN_INCORRECT"

def generate_token():
    """
    生成一个随机的会话令牌。
    """
    random_string = str(random.getrandbits(256)).encode('utf-8')
    return sha256(random_string).hexdigest()


class LoginSession:

    def __init__(self, client, user_id):
        self.client = client
        self.user_id = user_id
        self.key = "user::{0}::token".format(user_id)

    def create(self, timeout=DEFAULT_TIMEOUT):
        """
        创建新的登录会话并返回会话令牌,
        可选的 timeout 参数用于指定会话的过期时间(以秒为单位)。
        """
        # 生成会话令牌
        token = generate_token()
        # 储存令牌,并为其设置过期时间
        self.client.set(self.key, token, ex=timeout)
        # 返回令牌
        return token

    def validate(self, input_token):
        """
        根据给定的令牌验证用户身份。
        这个方法有三个可能的返回值,分别对应三种不同情况:
        1. SESSION_NOT_LOGIN_OR_EXPIRED —— 用户尚未登录或者令牌已过期
        2. SESSION_TOKEN_CORRECT —— 用户已登录,并且给定令牌与用户令牌相匹配
        3. SESSION_TOKEN_INCORRECT —— 用户已登录,但给定令牌与用户令牌不匹配
        """
        # 获取用户令牌
        user_token = self.client.get(self.key)
        # 令牌不存在
        if user_token is None:
            return SESSION_NOT_LOGIN_OR_EXPIRED
        # 令牌存在并且未过期,那么检查它与给定令牌是否一致
        if input_token == user_token:
            return SESSION_TOKEN_CORRECT
        else:
            return SESSION_TOKEN_INCORRECT

    def destroy(self):
        """
        销毁会话。
        """
        self.client.delete(self.key)

以下代码展示了这个会话程序的基本使用方法:

>>> from redis import Redis
>>> from login_session import LoginSession
>>> client = Redis(decode_responses=True)
>>> uid = "peter"
>>> session = LoginSession(client, uid)  # 创建会话
>>> token = session.create()             # 创建令牌
>>> token
'89e77eb856a3383bb8718286802d32f6d40e135c08dedcccd143a5e8ba335d44'
>>> session.validate("wrong token")      # 验证令牌
'SESSION_TOKEN_INCORRECT'
>>> session.validate(token)
'SESSION_TOKEN_CORRECT'
>>> session.destroy()                    # 销毁令牌
>>> session.validate(token)              # 令牌已不存在
'SESSION_NOT_LOGIN_OR_EXPIRED'

为了演示这个会话程序的自动过期特性, 我们可以创建一个有效期非常短的令牌, 并在指定的时间后再次尝试验证该令牌:

>>> token = session.create(timeout=3)  # 创建有效期为三秒钟的令牌
>>> session.validate(token)            # 三秒内访问
'SESSION_TOKEN_CORRECT'
>>> session.validate(token)            # 超过三秒之后,令牌已被自动销毁
'SESSION_NOT_LOGIN_OR_EXPIRED'

示例:自动淘汰冷门数据

本章开头在介绍 EXPIRE 命令和 PEXPIRE 命令的时候曾经提到过, 当用户对一个已经带有生存时间的键执行 EXPIRE 命令或是 PEXPIRE 命令时, 键原有的生存时间将被新的生存时间取代。 值得一提的是, 这个特性可以用于淘汰冷门数据并保留热门数据。

举个例子, 前面的《有序集合》一章曾经介绍过如何使用有序集合来实现自动补全功能, 但是如果我们仔细地分析这个自动补全程序, 就会发现它有一个潜在的问题: 为了实现自动补全功能, 程序需要创建大量自动补全结果, 而补全结果的数量越多、体积越大, 需要耗费的内存也会越多。

为了尽可能地节约内存, 一个高效的自动补全程序应该只储存热门关键字的自动补全结果, 并移除那些无人访问的冷门关键字的自动补全结果。 要做到这一点, 其中一种方法就是使用《有序集合》里面介绍过的排行榜程序, 为用户输入的关键字构建一个排行榜, 然后定期地删除排名靠后关键字的自动补全结果。

排行榜的方法虽然可行, 但是却需要使用程序定期删除自动补全结果, 使用起来相当麻烦。 一个更方便也更优雅的方法, 就是使用 EXPIRE 命令和 PEXPIRE 命令的更新特性去实现自动的冷门数据淘汰机制: 为此, 我们可以修改自动补全程序, 让它在每次处理用户输入的时候, 为相应关键字的自动补全结果设置生存时间。 这样一来, 对于用户经常输入的那些关键字, 它们的自动补全结果的生存时间将会不断得到更新, 从而产生出一种“续期”效果, 使得热门关键字的自动补全结果可以不断地存在下去, 而冷门关键字的自动补全结果则会由于生存时间得不到更新而自动被移除。

经过上述修改, 自动补全程序就可以在无需手动删除冷门数据的情况下, 通过自动的数据淘汰机制达到节约内存的目的, 代码清单 12-5 展示了修改后的自动补全程序。


代码清单 12-5 能够自动淘汰冷门数据的自动补全程序:/expire/auto_complete.py

class AutoComplete:

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

    def feed(self, content, weight=1, timeout=None):
        """
        根据用户输入的内容构建自动补全结果,
        其中 content 参数为内容本身,而可选的 weight 参数则用于指定内容的权重值,
        至于可选的 timeout 参数则用于指定自动补全结果的保存时长(单位为秒)。
        """
        for i in range(1, len(content)):
            key = "auto_complete::" + content[:i]
            self.client.zincrby(key, weight, content)
            if timeout is not None:
                self.client.expire(key, timeout)  # 设置/更新键的生存时间

    def hint(self, prefix, count):
        """
        根据给定的前缀 prefix ,获取 count 个自动补全结果。
        """
        key = "auto_complete::" + prefix
        return self.client.zrevrange(key, 0, count-1)

在以下代码中, 我们同时向自动补全程序输入了 "Redis""Coffee" 这两个关键字, 并分别为它们的自动补全结果设置了 10 秒钟的生存时间:

>>> from redis import Redis
>>> from auto_complete import AutoComplete
>>> client = Redis(decode_responses=True)
>>> ac = AutoComplete(client)
>>> ac.feed("Redis", timeout=10); ac.feed("Coffee", timeout=10)  # 同时执行两个调用

然后在 10 秒钟之内, 我们再次输入 "Redis" 关键字, 并同样为它的自动补全结果设置 10 秒钟的生存时间:

>>> ac.feed("Redis", timeout=10)

现在, 在距离最初的 feed() 调用执行十多秒钟之后, 如果我们执行 hint() 方法, 并尝试获取 "Re" 前缀和 "Co" 前缀的自动补全结果, 那么就会发现, 只有 "Redis" 关键字的自动补全结果还保留着, 而 "Coffee" 关键字的自动补全结果已经因为过期而被移除了:

>>> ac.hint("Re", 10)
['Redis']

>>> ac.hint("Co", 10)
[]

表 12-7 完整地展示了在执行以上代码时, "Redis" 关键字的自动补全结果是如何进行续期的, 而 "Coffee" 关键字的自动补全结果又是如何被移除的。 在这个表格中, "Redis" 关键字代表的就是热门数据, 而 "Coffee" 关键字代表的就是冷门数据: 一直有用户访问的热门数据将持续地存在下去, 而无人问津的冷门数据则会因为过期而被移除。


表 12-7 冷门数据淘汰示例

时间(以秒为单位)

"Redis" 关键字的自动补全结果

"Coffee" 关键字的自动补全结果

0000

执行 ac.feed("Redis", timeout=10) , 将自动补全结果的生存时间设置为 10 秒钟。

执行 ac.feed("Coffee", timeout=10) , 将自动补全结果的生存时间设置为 10 秒钟。

0001

自动补全结果的生存时间变为 9 秒钟。

自动补全结果的生存时间变为 9 秒钟。

0002

自动补全结果的生存时间变为 8 秒钟。

自动补全结果的生存时间变为 8 秒钟。

……

……

……

0007

执行 ac.feed("Redis", timeout=10) , 将自动补全结果的生存时间更新为 10 秒钟。

自动补全结果的生存时间变为 3 秒钟。

0008

自动补全结果的生存时间变为 9 秒钟。

自动补全结果的生存时间变为 2 秒钟。

0009

自动补全结果的生存时间变为 8 秒钟。

自动补全结果的生存时间变为 1 秒钟。

0010

自动补全结果的生存时间变为 7 秒钟。

"Coffee" 关键字的自动补全结果因为过期而被移除。

0011

自动补全结果的生存时间变为 6 秒钟。

自动补全结果已不存在。

0012

自动补全结果的生存时间变为 5 秒钟。

自动补全结果已不存在。

0013

执行 ac.hint("Re", 10) , 返回结果 ['Redis']

执行 ac.hint("Co", 10) , 返回空列表 [] 为结果。


除了自动补全程序之外, 我们还可以把这一机制应用到其他需要淘汰冷门数据的程序上面。 为了做到这一点, 我们必须理解上面所说的“不断更新键的生存时间,使得它一直存在”这个原理。

重点回顾

  • EXPIRE 命令和 PEXPIRE 命令可以为键设置生存时间, 当键的生存时间随着时间的流逝而消耗殆尽时, 键就会被移除。

  • 对已经带有生存时间的键执行 EXPIRE 命令或是 PEXPIRE 命令, 将导致键已有的生存时间被新的生存时间替代。

  • 为了方便用户, Redis 给 SET 命令增加了 EXPX 两个选项, 它们可以让用户在执行 SET 命令的同时, 执行 EXPIRE 命令或是 PEXPIRE 命令。

  • EXPIREAT 命令和 PEXPIREAT 命令可以为键设置 UNIX 时间戳格式的过期时间, 当系统时间超过这个过期时间时, 键就会被移除。

  • Redis 的自动过期特性只能应用于整个键, 它无法对键中的某个元素单独执行过期操作。

  • TTL 命令和 PTTL 命令可以分别以秒级和毫秒级这两种精度来获取键的剩余生存时间。

  • 通过重复对键执行 EXPIRE 命令或是 PEXPIRE 命令, 程序可以构建出一种自动淘汰冷数据并保留热数据的机制。