示例:自动淘汰冷门数据

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

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

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

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

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


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

  1. class AutoComplete:
  2.  
  3. def __init__(self, client):
  4. self.client = client
  5.  
  6. def feed(self, content, weight=1, timeout=None):
  7. """
  8. 根据用户输入的内容构建自动补全结果,
  9. 其中 content 参数为内容本身,而可选的 weight 参数则用于指定内容的权重值,
  10. 至于可选的 timeout 参数则用于指定自动补全结果的保存时长(单位为秒)。
  11. """
  12. for i in range(1, len(content)):
  13. key = "auto_complete::" + content[:i]
  14. self.client.zincrby(key, weight, content)
  15. if timeout is not None:
  16. self.client.expire(key, timeout) # 设置/更新键的生存时间
  17.  
  18. def hint(self, prefix, count):
  19. """
  20. 根据给定的前缀 prefix ,获取 count 个自动补全结果。
  21. """
  22. key = "auto_complete::" + prefix
  23. return self.client.zrevrange(key, 0, count-1)

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

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

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

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

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

  1. >>> ac.hint("Re", 10)
  2. ['Redis']
  3.  
  4. >>> ac.hint("Co", 10)
  5. []

表 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) ,返回空列表 [] 为结果。

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