在当前的web应用中,memcached因为其搭建简单、功能强大、工作稳定、使用方便而被广泛使用于数据模型、查询列表乃至页面区块的缓存中, 对于上述的这些好处,在我们使用memcached的过程中,对其缓存的批量精确清理一直是一个麻烦的问题,下面描述一下这个问题并试着去描述一些解决方 案。
第一部分:问题描述
以一个简化的论坛系统为例(图一),这个系统包含帖子、讨论区、其中讨论区是帖子的集合,帖 子从属于某个讨论区,在这里,我们假设帖子和讨论区都以memcached作为缓存,帖子的缓存key为thread_id,讨论区的缓存key是 discussion_areaId。当帖子发生改变的时候,我们就调用memecache的清理方法,清理发生变化的帖子cache,然后根据帖子id 得到讨论区的id,清理讨论区cache。

(图一)
随 着业务的发展,有必要给论坛加一个管理后台,便于运营人员维护论坛系统,后台访问的数据库和前台一致,这个后台也有帖子对象,为了减少数据库的访问,帖子 对象和论坛前台帖子使用的是同一个缓存key,这个系统还有一个用于审核的帖子列表,待审核列表按照讨论区分类,展示的是讨论中部分帖子,它也需要缓存, 于是给它定义了另外一个缓存key,名为auditlist_areaId 。于是系统变成了图二的样子。

(图二)
当 两个系统并存的时候,无论是后台还是前台修改了帖子对象,我们都需要清理所有包含帖子对象的列表缓存,确保用户能看到最新的内容。而当前memcache 支持的缓存清理方式有两种,一种是全局清理,另一种则是按照具体的key来清理。前者既无必要对性能开销也大, 对于需要频繁清理的情况,更适合的是使用后面一种清理方式,那么,一个问题就摆在我们的面前,如何让前台和后台能知道对方的缓存key,并进行缓存清理?
在系统开发的初期,对于这种共享又有差异的缓存使用情况,两个系统可以通过硬编码的方式来解决,在两个系统中,如果要清理缓存,除了清理 本系统的缓存,另外一个系统的缓存也"顺带"清理一下……设想一下,这时候又来了一个需求,要求在后台展示每个讨论区中加精的帖子,也要缓存,我们修改一 个帖子,就有可能需要修改三个不同的list缓存,而这三个list的key存在不同的系统中,更远的,如果还有第三个共用数据库的系统加入呢?
硬编码的方式让开发者疲于奔命了……
第二部分:方案选择
-
消息通信
当前台系统修改帖子对象之后,前台系统清理自己的缓存,然后调用有后台管理系统的清理缓存接口。同样,后台系统修改帖子对象,清理系统之后,就调用前台系统的清理缓存接口。
这 个方案的好处是调用方不再需要知道被调用者具体的key,约定一个清理缓存的接口显然比约定无数个缓存key好办的多;不好的地方在于,这样两方乃至多方 的系统就因此产生了错综复杂的依赖关系。如果这类型的缓存不止一个,那清理缓存接口的代码会较为复杂,调用清理缓存的入口会到处都是。
-
订阅消息
在 上一种方案的基础上,我们继续考虑,一种能够解耦的方式就呼之欲出,那就是使用一个中心节点(可以考虑基于zookeeper的配置中心),当一个系统发 生清理缓存的时候,就向中心节点发送一个消息,而需要同步列表的其它系统,因为在中心节点订阅了这个消息,就能够第一时间收到更新消息,从而执行自己的缓 存清理逻辑。
这个方案是上一个方案的进化版,系统的接口相互依赖消除了,连key的依赖也不存在了;但是增加了一个中心节点,增大了系统的维护成本,也必须承担中心节点崩溃带来的风险。
-
经验式的前缀清理
多 系统通过拷贝来共享一份缓存key已经讨论过,是一件费力不讨好的事情,但是我们还有得选择,就是对于要关联清除的缓存,我们可以在多系统中约定一种前 缀,比如,帖子的cache为thread_id,那么对应的帖子列表就可以是thread_list_areaId_1、 thread_list_areaId_2、thread_list_areaId_3,清理完帖子缓存之后,就取出帖子中的讨论区id,然后组合成列表 1、列表2、列表3的缓存key,本着宁可清到没有缓存的区域,也不能少清理的原则,列表最后的n设置为一个适中的值。
这种方案首先也不能完全避免共享常量,只是从共享key变成共享前缀了,使用的范围也比较狭窄,不过好处是避免了系统间的调用依赖,同时也不用再维护一个配置中心。
-
构建底层系统的方案
这 个方案是把所有的数据操作,都放到一个底层系统中,即当前台和后台要取列表的时候,都只能向这个新的底层系统发送请求,底层系统自己来维护所有的缓存关 系。这个方案把多系统的缓存压缩到了一个系统里面,以服务的方式提供数据服务,对其它系统隐藏了数据库的实现,清晰了系统的结构。不好的地方是增加了一层 调用底层的网络开销,另外前台要对数据查询、修改的任何操作都需要走接口实现,很多时候是要修改和重新发布底层系统才能如愿的,这样,底层系统的稳定性和 频繁修改会成为一对矛盾。
-
数据库为中心的清理方案
多系统共享数据库,那数据库就是一个天然的 中心节点,能否复用这个必须的中心节点呢?答案是肯定的,我们可以把细粒度的key(帖子key)、与其关联的key(列表)都存储在数据库中,然后按照 数据库中的关联key来逐一清除缓存,就可以在变动的时候,"替"别的系统完成缓存清理。以我们上面的帖子例子来说:
(1)建立如下的一张表
Id
cacheKey
keyPattern
20
thread_id
discussion_{areaId}
21
thread_id
auditlist_{areaId}
(2)修改id为50000的帖子,修改其缓存,被清理的单个缓存id是thread_50000
(3) 从数据库中取出thread_id对应的两种模式,这两种模式中,{}大括号包裹的部分就是缓存的可变值,约定为Thread对象中存储的某个字段,可以 通过对Thread对象反射得到,一个是discussion_{areaId},auditlist_{areaId},这样,我们就可以通过反射手中 已经得到的Thread对象,得到areaId(比如说是1000),这时候,两个key最终执行的时候就变为 discussion_1000,auditlist_1000。
这个表本身也可以成为后台管理的一部分,当开发人员需要加入新的缓存的 时候,就可以将缓存的key模式和key对应起来存入数据库,所有共用这个模型的系统,都可以共用这个缓存清理模型,更进一步,这个功能可以打成jar 包,供系统做透明的调用,系统的责任,就只要维护好key映射就可以了。
第三部分 总结
没有最好的方案,只有合适的方案,在这几种选项中,第2和第5是我比较中意的方案,相对而言能有效的解决系统耦合的问题,网上也有根据key的存储分布,最终来清理某一个节点的方案,亦有可观之处,上述都是从应用层面上做的考虑,如果读者能有更好的方案,欢迎留言讨论。
,这里就是抛砖引玉说一下了。
