介绍
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 的项目文档。