Python 缓存框架 Tache 使用介绍

1,308 阅读3分钟
原文链接: zhuanlan.zhihu.com

介绍

Tache 是一个 Python 的缓存框架。它基于如下的目标而设计:

  • 同时支持 Python2 和 Python3
  • 支持缓存普通函数/实例方法/类方法/静态方法
  • 支持 Batch 批量缓存
  • 支持基于 Tag 的缓存和失效
  • 支持基于参数显式声明 key 格式

前两个目标是必须的,后面三个目标是我们设计 Tache 的初衷。

Tag 的使用

案例一: 变更批量失效。当一个对象变更时,需要失效所有与该改对象相关的查询。

最初对象相关的查询可能比较少,随着业务的增长后来的查询条件越来越多,对象字段越来越多,甚至变成了一个聚合对象,内部各字段甚至都不来自一个存储,一个服务,每处都加上了缓存。这时本来只是一处的变更,要失效的代码散落在各地很容易出漏。怎么办呢,根据对象给缓存打上同样的标签,失效时只需失效一处。

假设有如下的代码 (以下代码均为演示而设计,勿对号入座):

@cache(tags=["comment:{0}"])
def get_comment(comment_id):
    ...

@cache(tags=["comment:{0}"])
def get_comment_status_by_id(comment_id):
   ... 
   
@cache(tag=["comment:{0}"])
def get_comment_likes_by_id(comment_id):
   ... 

@cache(tag=["comment:{0}", "member:{1}"])
def get_replyed_comment_by_member(comment_id, member_id):

那么最后实际需要失效的代码还需要多少呢?只需一行:

cache.invalidate_tag("comment:{0}".format(comment_id))

案例二: 分页操作。针对如下的分页操作,因为 offset, limit 的组合太多,如果对上面的操作整体缓存,如何保证失效时全部失效?

def get_comments(object_type, object_id, status, offset, limit):
...

一样可以用 tag 来控制:

@cache(tags=["comment:{0}:{1}"])
def get_comments(object_type, object_id, status, offset, limit):
    comments = comment_dao.get_comments(..., offset, limit)
    results = []
    for c in comments:
        results.append(format_comment(c))
    return results

这样当增加和删除评论时,只要失效 tag, 所有的 offset/limit 查询缓存全部都失效了。等等,你这样有另一个问题,会不会缓存失效粒度过大,造成数据库大量查询?

很遗憾,确实会这样。基于此,我们将基于 Batch 批量缓存来优化这个分页操作。

Batch 的使用

Batch 可以同时对列表中多个元素进行批量缓存,当失效列表中某一元素时不会影响被缓存的其他元素。当列表中有缓存和未缓存的数据都存在时,仅会对未缓存的数据进行查询。如:

list_objects(1,2,3,4,5) # no cache, 调用完毕全部一次缓存
list_objects(3,4,5,6,7) # 3,4,5 从缓存中取,6,7 在调用完毕一次缓存

接着针对上面的分页操作,我们把它分为两步:
1. 分页取 id 列表
2. 用 id 列表批量获取实体 id 列表

以下是 get_comments 的重构:

@cache(tags=["comment:{0}:{1}"])
def get_comments(object_type, object_id, status, offset, limit):
    comment_ids = comment_dao.get_comment_ids(..., offset, limit)
    results = get_comments_byids(*comment_ids)
    return results
 
@batch()
def get_comments_byids(*comment_ids):
    results = []
    for cid in comment_ids:
        comment = comment_dao.get_comment(cid)
        results.append(format_comment(comment))
    return results
 
def add_comment():
    ...
    cache.invalidate_tag("comment:{0}:{1}".format(obtype, obid))

这样在失效缓存时,我们失效的只是用 offset, limit 取 id 列表这个查询, 从 id 列表取出实体都还是被缓存的。

显式声明 Key

开源的 Python 缓存库通常生成缓存 key 的规则都是基于模块名、类名、方法名等的组合自动生成。作为一个有追求的程序员,重构是经常要做的事。但是重构过程中,如果改了函数名、方法名,或者移动了模块位置,经常会造成缓存失效,对于流量比较大的接口这是比较危险的事情。

Tache 允许你显式声明 Key 的生成规则, 这样不论代码如何重构生成的 key 都不会改变。

 class B:
  
        def __init__(self):
            self.count = 0

        @cache.cached("counter.B.add|{0}-{1}")
        def add(self, a, b):
            self.count += 1
            return a + b + self.count

只要不改 key 的规则,不论代码如何重构,上述生成的 key 都会形如 counter.B.add|{0}-{1}.format(a, b)

最后

  • 基于 tag 的缓存是个杀器,但要谨慎使用,使用场景推荐用于粗粒度的聚合接口,不建议用来缓存请求量大的细粒度接口。因为每次读取时会多取一次 tag, 因此对接口缓存的请求量会放大一倍
  • 更详细的使用说明参见 Tache 的项目文档