Browse > Home / Archive: 十二月 2009

| 订阅RSS

Web中通用列表缓存的策略和实现。

十二月 23rd, 2009 | No Comments | Posted in cache, 软件架构

在当前的web网站中,缓存扮演越来越重要的角色,是应付网站高并发的重要技术手段,在缓存中,对于单一记录的缓存是最简单的,只要卡住所有的保存、删除、更新操作,然后向缓存控制中心提交对应的事件即可,而对于list的缓存通常是很难解决的,这里说说最近在弄的缓存,自我感觉良好。

第零种:系统中最新博客文章,策略是采用ttl,每隔5‘过期一次。

第一种:最简单的list缓存,策略是只要有数据更新要删除此类的所有list缓存。(目前hibernate的二级缓存的实现策略)

第二种: 条件缓存,譬如:某人的博客;博客系统中的用户和文章的关系,当某一用户更新了自己的文章,不应该删除其他人的博客缓存,策略用用户的id参入到缓存的key中

第N种: 非固定值多条件缓存,譬如:某人9:00~10:00 的博客文章列表;策略自定义缓存策略

谈谈我的思路,一样卡住所有的增、删、改操作,当发生增删该的时候,向缓存服务器发送事件。

public class CacheEvent<T> {

private Class<T> targetClass;

private T targetObject;

private Operation operation;

public CacheEvent(Class<T> targetClass, T targetObject, Operation operation) {

this.targetClass = targetClass;

this.targetObject = targetObject;

this.operation = operation;

}

对于第一种:那么我的缓存定义:

@Cacheable(clazz=TestMember.class)
public List<TestMember> getList();

通过方法和实体类产生一个缓存key, 只要发现此这样的实体发生增、删操作,即刻删除此缓存(关于如何删除,见下文)

对于第二种:我的缓存定义为:
@Cacheable(clazz=TestArticle.class,
namespaces={@CacheNsParameter(name=”member”,keyIndex=1)})
public List<TestArticle> getList(@CacheKeyParameter TestMember member,
@CacheKeParameter start,@CacheKeyParameter offset);
通过方法和namespace的定义产生一个缓存key,接受事件,当发生实体的增、删操作,并且TestArticle的member属性和@CacheKeyparameter 标记的属性相等时候,即刻删除此缓存。
如果缓存的key中参数为固定对象主键(譬如:ns中用户id,且文章表中的member_id 不会被update,要么insert ,要么delete,一旦设置就不会更改)或者参数和实体本身无关(譬如:分页参数start、offset)的时候,那么上述两种方法,基本上能满足所有缓存的需求。
对于第三种缓存用的相对少点,需要自定义,我的定义为:
@Cacheable(clazz=TestArticle.class,strategy=”XX”)
public List<TestArticle> getList(@CacheKeyParameter TestMember member,@CacheKeyParameter int month);
由自定义的xx服务类(我使用的是Tapestry5-IOC)来进行处理。
上面说到中很关键的一个东西就是怎么批量的高效删除缓存,譬如:在第二种中多次运行产生的cacheKey为
TestArticle_member_1_0_10   (后三位分别为member_id,start,offset)
TestArticle_member_1_1_30
TestArticle_member_1_40_30
TestArticle_member_2_0_10
TestArticle_member_2_1_10
TestArticle_member_2_30_10
。。。。。。。。。。
很多这样的key,如果要是每次把key记录一下,那这个代价也是挺高的,也要弄一个缓存的东西给记录下来,现在大家注意一下这个缓存的key,用户1增加文章的时候,实际就是删除所有key以TestArticle_member_1 开头的缓存即可,这样就省事多了,问题转换为怎么批量删除,在缓存中没有批量删除这个概念:( ,因为要遍历所有的key进行操作,效率自然高不了,挠头ing,我搜呀搜,无意中发现了官方的FAQ:http://code.google.com/p/memcached/wiki/FAQ#Namespaces 如果我们把TestArticle_member_1 加个版本号的话,不久好了吗,那么第一次时候产生的key为:

TestArticle_member_1_v0_0_10   (后四位分别为member_id,version,start,offset)
TestArticle_member_1_v0_1_30
TestArticle_member_1_v0_40_30
TestArticle_member_2_v0_0_10
TestArticle_member_2_v0_1_10
TestArticle_member_2_v0_30_10
当memeber 1增加文章的时候,我版本号加一,那么下次请求缓存时候key就变成了:

TestArticle_member_1_v1_0_10   (后四位分别为member_id,version,start,offset)
TestArticle_member_1_v1_1_30
TestArticle_member_1_v1_40_30
TestArticle_member_2_v0_0_10
TestArticle_member_2_v0_1_10
TestArticle_member_2_v0_30_10
cache没找到对应的缓存记录,那么执行方法,然后缓存起来,这样就变相实现了缓存的批量删除,有人会说,那岂不参生垃圾数据了,不用担心缓存系统一般都有LRU算法自动删除没用的数据。 :)
问题就豁然开朗了。其他情况也就迎刃而解了。
可以看看偶的测试用例:
约束、限制、建议:
1) 所有实体的缓存都是以实体本身向下延展。
2) 放入到缓存中的列表都是ID集合
3) 尽量以外建、主键、索引作为缓存的key
4) 使用hibernate的时候,不使用ManyToMany关系,用 one-to-many 和中间实体替代,譬如用户的角色列表。
有待完善:
1) 没次读取缓存的时候,需要分析方法中的Annotation,需要使用LocalCache的方式进行缓存起来。
2) 在更高量的访问时候,应该从memcache中读取的数据一部分放入本地缓存中。
3) 加入压力测试。
不过总算一个还算通用的列表缓存实现,通过前三种方式,系统的list缓存自定义方便了许多。借此拍砖,欢迎抛玉。