自动过期 ============== 在构建应用时, 我们常常会碰到一些在特定时间之后就不再有用的数据, 比如说: - 随着内容的不断更新, 一个网页的缓存可能在 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`` .. literalinclude:: code/expire/unsafe_volatile_cache.py ---- 以下代码简单地展示了这个缓存程序的使用方法: :: >>> from redis import Redis >>> from unsafe_volatile_cache import VolatileCache >>> client = Redis(decode_responses=True) >>> cache = VolatileCache(client) >>> cache.set("homepage", "

hello world

", 10) # 设置缓存 >>> cache.get("homepage") # 这个缓存在 10 秒钟之内有效 '

hello world

' >>> cache.get("homepage") # 10 秒钟过后,缓存自动被移除 >>> .. 演示如何在程序里面集成这个缓存程序: 用一个 flask 的 GET /user/ 例子来演示如何使用这个程序在 web 应用中缓存用户数据 :: cached_value = GET cache_key if cached_value is None: value = GET key SET cache_key value EX timeout return value else: return cached_value 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`` .. literalinclude:: code/expire/volatile_cache.py ---- 重写之后的缓存程序实现是“安全的”: 设置缓存和设置生存时间这两个操作要么就一起成功, 要么就一起失败, “设置缓存成功了,但是设置生存时间却失败了”这样的情况将不会出现。 后续的章节也会介绍如何通过 Redis 的事务功能来保证执行多条命令时的安全性。 其他信息 ^^^^^^^^^^^^^^^^^^ +---------------+-------------------------------------------------------------------------------+ | 属性 | 值 | +===============+===============================================================================+ | 复杂度 | O(1) | +---------------+-------------------------------------------------------------------------------+ | 版本要求 | 带有 ``EX`` 选项和 ``PX`` 选项的 ``SET`` 命令从 Redis 2.6.12 版本开始可用。 | +---------------+-------------------------------------------------------------------------------+ 示例:带有自动释放特性的锁 ----------------------------------- 在前面的《字符串》一章, 我们曾经实现过一个锁程序, 它的其中一个缺陷就是无法自行释放: 如果锁的持有者因为故障下线, 那么锁将一直处于持有状态, 导致其他进程永远也无法获得锁。 为了解决这个问题, 我们可以在获取锁的同时, 通过 Redis 的自动过期特性为锁设置一个最大加锁时限, 这样的话, 即使锁的持有者由于故障下线, 锁也会在时限到达之后自动释放。 代码清单 12-3 展示了使用上述原理实现的锁程序。 ---- 代码清单 12-3 带有自动释放特性的锁:\ ``/expire/timing_lock.py`` .. literalinclude:: code/expire/timing_lock.py ---- 以下代码演示了这个锁的自动释放特性: :: >>> 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 # 因为之前获取的锁已经自动被释放,所以这次将成功取得新的锁 .. 练习 不过这种锁也会带来一个问题 —— 可能持有者还在使用锁, 但锁已经因为到期被释放了, 这时就会出现两个锁同时存在的情况, 为此, 我们可以创建一个 refresh() 方法, 让它去加长锁的使用时间, 直到持有者愿意释放为止。 不过这种方法也有一个问题, 就是可能会导致其他进程饥饿, 不过这可以通过设置一个 MAX_REFRESH_TIME 来防止: 当进程 refresh() 的次数达到 MAX_REFRESH_TIME 时, 这个进程就不得再使用锁, 必须释放, 这样问题就解决了。 请据此写出 refresh() 方法 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 的自动过期功能不足的一个地方。 .. 过期时间和生存时间本质上是一样的 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ **设置生存时间的调用会被转换为设置过期时间的调用,比如 EXPIRE key 30 会被转换为 EXPIREAT key (current_ux_time+30) , PEXPIRE 命令的执行方式也是类似** 其他信息 ^^^^^^^^^^^^^^^^^ +---------------+-------------------------------------------------------------------+ | 属性 | 值 | +===============+===================================================================+ | 复杂度 | ``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 会话程序创建的散列数据结构 .. image:: image/expire/IMAGE_SESSION_HASHS.png ---- 在学习了 Redis 的自动过期特性之后, 我们可以对会话程序进行修改, 通过给会话令牌设置过期时间来让它在指定的时间之后自动被移除。 这样一来, 程序只需要检查会话令牌是否存在, 就能够知道是否应该让用户重新登录了。 代码清单 12-4 展示了修改之后的会话程序。 因为 Redis 的自动过期特性只能对整个键使用, 所以这个程序使用了字符串而不是散列来储存会话令牌, 但总的来说, 这个程序的逻辑跟之前的会话程序的逻辑基本相同。 不过由于新程序无需手动检查会话是否过期, 所以它的逻辑简洁了不少。 ---- 代码清单 12-4 带有自动过期特性的会话程序:\ ``/expire/login_session.py`` .. literalinclude:: code/expire/login_session.py ---- 以下代码展示了这个会话程序的基本使用方法: :: >>> 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`` .. literalinclude:: code/expire/auto_complete.py ---- 在以下代码中, 我们同时向自动补全程序输入了 ``"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)`` , | 执行 ``ac.feed("Coffee", timeout=10)`` , | | | 将自动补全结果的生存时间设置为 10 秒钟。 | 将自动补全结果的生存时间设置为 10 秒钟。 | +-----------------------+-------------------------------------------+-------------------------------------------------------+ | 0001 | 自动补全结果的生存时间变为 9 秒钟。 | 自动补全结果的生存时间变为 9 秒钟。 | +-----------------------+-------------------------------------------+-------------------------------------------------------+ | 0002 | 自动补全结果的生存时间变为 8 秒钟。 | 自动补全结果的生存时间变为 8 秒钟。 | +-----------------------+-------------------------------------------+-------------------------------------------------------+ | …… | …… | …… | +-----------------------+-------------------------------------------+-------------------------------------------------------+ | 0007 | 执行 ``ac.feed("Redis", timeout=10)`` , | 自动补全结果的生存时间变为 3 秒钟。 | | | 将自动补全结果的生存时间更新为 10 秒钟。 | | +-----------------------+-------------------------------------------+-------------------------------------------------------+ | 0008 | 自动补全结果的生存时间变为 9 秒钟。 | 自动补全结果的生存时间变为 2 秒钟。 | +-----------------------+-------------------------------------------+-------------------------------------------------------+ | 0009 | 自动补全结果的生存时间变为 8 秒钟。 | 自动补全结果的生存时间变为 1 秒钟。 | +-----------------------+-------------------------------------------+-------------------------------------------------------+ | 0010 | 自动补全结果的生存时间变为 7 秒钟。 | ``"Coffee"`` 关键字的自动补全结果因为过期而被移除。 | +-----------------------+-------------------------------------------+-------------------------------------------------------+ | 0011 | 自动补全结果的生存时间变为 6 秒钟。 | 自动补全结果已不存在。 | +-----------------------+-------------------------------------------+-------------------------------------------------------+ | 0012 | 自动补全结果的生存时间变为 5 秒钟。 | 自动补全结果已不存在。 | +-----------------------+-------------------------------------------+-------------------------------------------------------+ | 0013 | 执行 ``ac.hint("Re", 10)`` , | 执行 ``ac.hint("Co", 10)`` , | | | 返回结果 ``['Redis']`` 。 | 返回空列表 ``[]`` 为结果。 | +-----------------------+-------------------------------------------+-------------------------------------------------------+ ---- 除了自动补全程序之外, 我们还可以把这一机制应用到其他需要淘汰冷门数据的程序上面。 为了做到这一点, 我们必须理解上面所说的“不断更新键的生存时间,使得它一直存在”这个原理。 .. 示例:限时抢购 --------------------------------- TODO 使用 EXPIREAT 指定抢购的截止时间,使用 TTL 返回抢购的剩余时间。 TODO 添加更多 EXPIREAT 和 PEXPIREAT 的使用示例…… 重点回顾 -------------------- - ``EXPIRE`` 命令和 ``PEXPIRE`` 命令可以为键设置生存时间, 当键的生存时间随着时间的流逝而消耗殆尽时, 键就会被移除。 - 对已经带有生存时间的键执行 ``EXPIRE`` 命令或是 ``PEXPIRE`` 命令, 将导致键已有的生存时间被新的生存时间替代。 - 为了方便用户, Redis 给 ``SET`` 命令增加了 ``EX`` 和 ``PX`` 两个选项, 它们可以让用户在执行 ``SET`` 命令的同时, 执行 ``EXPIRE`` 命令或是 ``PEXPIRE`` 命令。 - ``EXPIREAT`` 命令和 ``PEXPIREAT`` 命令可以为键设置 UNIX 时间戳格式的过期时间, 当系统时间超过这个过期时间时, 键就会被移除。 - Redis 的自动过期特性只能应用于整个键, 它无法对键中的某个元素单独执行过期操作。 - ``TTL`` 命令和 ``PTTL`` 命令可以分别以秒级和毫秒级这两种精度来获取键的剩余生存时间。 - 通过重复对键执行 ``EXPIRE`` 命令或是 ``PEXPIRE`` 命令, 程序可以构建出一种自动淘汰冷数据并保留热数据的机制。