论坛首页 Java企业应用论坛

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

浏览 21367 次
精华帖 (0) :: 良好帖 (9) :: 新手帖 (1) :: 隐藏帖 (0)
作者 正文
   发表时间:2009-07-19   最后修改:2009-09-20
标题党了 

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

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

进入正题

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

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

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

空说太抽象 直接上代码
不重要的方法省略
设计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呢 请参考http://xuliangyong.iteye.com/admin/blogs/424921
importDB("load data local infile '" + path + "' into table " + tableName);


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

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

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

这样会自动覆盖掉旧点击次数,也就无需清理无效数据了


   发表时间:2009-07-20  
以前看过一个解决方法用UDP弄,
0 请登录后投票
   发表时间:2009-07-20  
mock1234 写道
使用线程中的线程将 +1 操作异步执行到数据库就可以了,不要搞复杂设计。

这会搞死数据库的
0 请登录后投票
   发表时间:2009-07-20  
mock1234 写道
xly_971223 写道
mock1234 写道
使用线程中的线程将 +1 操作异步执行到数据库就可以了,不要搞复杂设计。

这会搞死数据库的


怎么搞死?有根据吗?


这个.... 
我无语了
0 请登录后投票
   发表时间:2009-07-20  
用得着这么复杂吗, 我定个时间, 每隔30分钟将所有贴子的hits缓存刷到数据库中, 或者10分钟. 或者, 任意定个时间, 至少能解决绝大部分问题
0 请登录后投票
   发表时间:2009-07-20  
srdrm 写道
用得着这么复杂吗, 我定个时间, 每隔30分钟将所有贴子的hits缓存刷到数据库中, 或者10分钟. 或者, 任意定个时间, 至少能解决绝大部分问题

如果缓存了10w帖子 假设有1w个发生了变化
你刷到数据库看看 1W条sql几乎同时到服务器不搞死才怪

10w对一个网站来说是非常小的数目了 如果是100W 1000W呢?
0 请登录后投票
   发表时间:2009-07-20  
xly_971223 写道
srdrm 写道
用得着这么复杂吗, 我定个时间, 每隔30分钟将所有贴子的hits缓存刷到数据库中, 或者10分钟. 或者, 任意定个时间, 至少能解决绝大部分问题

如果缓存了10w帖子 假设有1w个发生了变化
你刷到数据库看看 1W条sql几乎同时到服务器不搞死才怪

10w对一个网站来说是非常小的数目了 如果是100W 1000W呢?

恕我孤陋寡闻,您能不能给几个每天有10w,100w,1000w个帖子被点击过的网站?
0 请登录后投票
   发表时间:2009-07-20  
量或许是很大, 但一天之内有hit的贴子量不会是这种100w量级的, 10w量级的贴子更新数我认为对于网站来说不小了. 刷到数据库不一定要同时刷, 这个不要求即时的,可以悠着点来,怎么悠怎么来,只要在一个合理时间内完成就行
0 请登录后投票
   发表时间:2009-07-20  
xly_971223 写道
srdrm 写道
用得着这么复杂吗, 我定个时间, 每隔30分钟将所有贴子的hits缓存刷到数据库中, 或者10分钟. 或者, 任意定个时间, 至少能解决绝大部分问题

如果缓存了10w帖子 假设有1w个发生了变化
你刷到数据库看看 1W条sql几乎同时到服务器不搞死才怪

10w对一个网站来说是非常小的数目了 如果是100W 1000W呢?


小数目?? 什么网站可以有30min分钟10w条帖子的量? 好奇中.

引用

mock1234 写道
xly_971223 写道
mock1234 写道
使用线程中的线程将 +1 操作异步执行到数据库就可以了,不要搞复杂设计。

这会搞死数据库的

怎么搞死?有根据吗?

这个.... 
我无语了

性能这东西不是蒙出来的. 况且你连数据规模都不知道.....

关于你的那个测试 由于只有结果没有过程不好评论. 
http://xuliangyong.iteye.com/blog/424921
但你的结果和预想的实在相差太远.  怀疑造成这么大性能差距是由于其它的原因造成的.
0 请登录后投票
   发表时间:2009-07-20  
我觉得10w只是楼主一个夸张的说法嘛,同一时间执行几千条sql对数据库也是个性能挑战,一个连接池里不也只有上百个连接吗,估计一个门户特定时间更新数据库,这样玩缓存就玩完了。
0 请登录后投票
论坛首页 Java企业应用版

跳转论坛:
Global site tag (gtag.js) - Google Analytics