帖子 博客等资源点击量缓存杀手级解决方案

标题党了 :lol:

关于点击量几年前发过帖子[url]http://www.iteye.com/topic/171240[/url]
现在看来太简单了 而且问题多多

最近有琢磨出了一套新的方案

[b]进入正题[/b]

关于帖子点击量,通常的办法是缓存在内存,然后等到合适的时机写入数据库,一般是设置一个阈值,到达后更新数据库
这种方式主要面临如下几个问题:
1 有些帖子永远到达不了阈值怎么办?如阈值为10,但到9后再也没有人点击了
2 阈值设置多大合适?太大了服务器当机会丢失大量数据,太小了没啥意义
3 每个帖子到达阈值后都要访问数据库,能不能合并起来 只访问一次DB

[color=red]采用阈值方式是被动的,应该用主动的方式来解决问题[/color]
主动方式的思路如下:cache ---》 文件 ---》DB
[color=blue]1 cache中保存两个点击量,我们称之为todayHits和yestodayHits[/color]
todayHits保存资源当天的点击量
yestodayHits保存昨天的点击量

[color=blue]2 定时把cache中发生变化的数据导出到文件,未发生变化的删除[/color]
[color=blue]3 把导出的文件导入到数据库。多数DB都提供命令. mysql 为 load data local infile[/color]
此方法是把文件中的数据追加到表尾,这样会导致一个资源对应多个点击量的问题,我们用导入时间获取最新的点击量
[color=blue]4 删除过期数据。每次追加后会使原来部分数据变得无意义,需要清理掉[/color]
主动方式会定时扫描cache中数据

[b]空说太抽象 直接上代码[/b]
不重要的方法省略
设计Cahce的key和value
HitKey 封装了资源id和资源类型
public final class HitKey implements Serializable {
private Integer id;
private HitType type;
//setter getter ...
}


/**
* 保存资源的点击量
* @author xuliangyong
* 2009-7-12
*/
public class HitValue implements Serializable {
/**
* 今天的总点击量
*/
private Integer todayHits;
/**
* 昨天的总点击量
*/
private Integer yestodayHits;

public HitValue(){}

private HitValue(Integer todayHits, Integer yestodayHits){
this.todayHits = todayHits;
this.yestodayHits = yestodayHits;
}

/**
* 工厂方法
*/
public static HitValue valueOf(Integer todayHits, Integer yestodayHits){
return new HitValue(todayHits, yestodayHits);
}

/**
* 增加点击次数
* @param hit 点击次数
* @return 返回总点击次数
*/
public void addHits(Integer hit){
if(todayHits == null){
todayHits = new Integer(0);
}
todayHits += hit;
}

/**
* 点击次数加1
* @return 返回总点击次数
*/
public void addHits(){
addHits(1);
}

/**
* 把昨天点击量与今天点击量同步。
* 此方法通常在写完日志文件后调用
*/
public void synchronize(){
yestodayHits = todayHits;
}

/**
* 测试点击量是否变化。
*/
public boolean isChanged(){
return yestodayHits != todayHits;
}

}



用一个Map做cache,可更换成第三方缓存,最好是有region概念的缓存
public class HitsFacade {

private static final Map<HitKey, HitValue> HITS_CACHE = Collections.synchronizedMap(new HashMap<HitKey, HitValue>());

private HitsManager hitsManager;

/**
* 获取资源点击量.
* 1 从cache读
* 2 从持久存储读
*/
public Integer get(HitKey hitKey){
HitValue hitValue = HITS_CACHE.get(hitKey);

if(hitValue == null){
Integer hits = getHits(hitKey);
hitValue = HitValue.valueOf(hits, hits);
HITS_CACHE.put(hitKey, hitValue);
}

return hitValue.getTodayHits();
}

/**
* 增加1次点击量
* 用法:
* hitsFacade.add( HitKey.valueOf(blogId, HitType.BLOG) );
* @param hitKey
*/
public void add(HitKey hitKey){
add(hitKey, 1);
}

/**
* 增加点击量
* 用法:
* hitsFacade.add( HitKey.valueOf(blogId, HitType.BLOG), 10 );
* @param hits 增加的次数
*/
public void add(HitKey hitKey, Integer hits){
HitValue hitValue = HITS_CACHE.get(hitKey);
if(hitValue == null){
get(hitKey);
hitValue = HITS_CACHE.get(hitKey);
}

hitValue.addHits(hits);
HITS_CACHE.put(hitKey, hitValue);
}

/**
* 从持久存储加载点击量。
* 为避免并发导致多次访问持久存储,故加synchronized关键字
*/
//TODO 并发如何处理??
protected synchronized Integer getHits(HitKey hitKey) {
return hitsManager.getHits(hitKey);
}
}


至此cache代码处理完毕

接下来处理cache ---》 文件

/**
* 把hits cache中的数据导出到日志文件
*/
public File exportHitsCacheToLog() throws IOException{
Map<HitKey, HitValue> hitsCache = hitsFacade.getCache();
Iterator<HitKey> hitKeyIterator = hitsCache.keySet().iterator();

//创建文件
File logFile = createFile();
BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter(logFile));

while (hitKeyIterator.hasNext()) {
HitKey hitKey = hitKeyIterator.next();
HitValue hitValue = hitsCache.get(hitKey);

if( !hitValue.isChanged() ){
hitKeyIterator.remove();
}else{
StringBuilder sb = new StringBuilder();
sb.append(hitKey.getId()).append("\t")
.append(hitKey.getType()).append("\t")
.append(hitValue.getTodayHits()).append("\t")
.append(DateFormatUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss"))
.append("\n");
bufferedWriter.write(sb.toString());

hitValue.synchronize();
hitsCache.put(hitKey, hitValue);
}
}

//关闭文件
bufferedWriter.flush();
bufferedWriter.close();
return logFile;
}


此方法是核心 尤其是这几句
if( !hitValue.isChanged() ){
//如果今天的数据相比昨天无变化则删除
hitKeyIterator.remove();
}else{
StringBuilder sb = new StringBuilder();
sb.append(hitKey.getId()).append("\t")
.append(hitKey.getType()).append("\t")
.append(hitValue.getTodayHits()).append("\t")
.append(DateFormatUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss"))
.append("\n");
bufferedWriter.write(sb.toString());
//有今天的数据相比昨天发生变化则写入文件,并把昨天的数据与今天同步
hitValue.synchronize();
hitsCache.put(hitKey, hitValue);
}


再接下来 文件 ----》DB
为什么要从文件导入DB而不直接从cache写入DB呢 请参考[url]http://xuliangyong.iteye.com/admin/blogs/424921[/url]
importDB("load data local infile '" + path + "' into table " + tableName);


最后为清理过期的无效数据,此过程可另找时间清理 不必与上述步骤同步进行

该缓存方案已初步完成,随着项目的变化也会做相应调整
到底性能如何 能否应付海量数据 还有待检验

[b]2009-09-20 补充[/b]
最后一步清理过期数据 采用了新的方法
使用
load data local infile ... replace into ...

这样会自动覆盖掉旧点击次数,也就无需清理无效数据了
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值