锁定老帖子 主题:帖子 博客等资源点击量缓存杀手级解决方案
精华帖 (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 ... 这样会自动覆盖掉旧点击次数,也就无需清理无效数据了 声明:ITeye文章版权属于作者,受法律保护。没有作者书面许可不得转载。
推荐链接
|
|
返回顶楼 | |
发表时间:2009-07-20
以前看过一个解决方法用UDP弄,
|
|
返回顶楼 | |
发表时间:2009-07-20
mock1234 写道 使用线程中的线程将 +1 操作异步执行到数据库就可以了,不要搞复杂设计。
这会搞死数据库的 |
|
返回顶楼 | |
发表时间:2009-07-20
mock1234 写道 xly_971223 写道 mock1234 写道 使用线程中的线程将 +1 操作异步执行到数据库就可以了,不要搞复杂设计。
这会搞死数据库的 怎么搞死?有根据吗? 这个.... 我无语了 |
|
返回顶楼 | |
发表时间:2009-07-20
用得着这么复杂吗, 我定个时间, 每隔30分钟将所有贴子的hits缓存刷到数据库中, 或者10分钟. 或者, 任意定个时间, 至少能解决绝大部分问题
|
|
返回顶楼 | |
发表时间:2009-07-20
srdrm 写道 用得着这么复杂吗, 我定个时间, 每隔30分钟将所有贴子的hits缓存刷到数据库中, 或者10分钟. 或者, 任意定个时间, 至少能解决绝大部分问题
如果缓存了10w帖子 假设有1w个发生了变化 你刷到数据库看看 1W条sql几乎同时到服务器不搞死才怪 10w对一个网站来说是非常小的数目了 如果是100W 1000W呢? |
|
返回顶楼 | |
发表时间:2009-07-20
xly_971223 写道 srdrm 写道 用得着这么复杂吗, 我定个时间, 每隔30分钟将所有贴子的hits缓存刷到数据库中, 或者10分钟. 或者, 任意定个时间, 至少能解决绝大部分问题
如果缓存了10w帖子 假设有1w个发生了变化 你刷到数据库看看 1W条sql几乎同时到服务器不搞死才怪 10w对一个网站来说是非常小的数目了 如果是100W 1000W呢? 恕我孤陋寡闻,您能不能给几个每天有10w,100w,1000w个帖子被点击过的网站? |
|
返回顶楼 | |
发表时间:2009-07-20
量或许是很大, 但一天之内有hit的贴子量不会是这种100w量级的, 10w量级的贴子更新数我认为对于网站来说不小了. 刷到数据库不一定要同时刷, 这个不要求即时的,可以悠着点来,怎么悠怎么来,只要在一个合理时间内完成就行
|
|
返回顶楼 | |
发表时间: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 但你的结果和预想的实在相差太远. 怀疑造成这么大性能差距是由于其它的原因造成的. |
|
返回顶楼 | |
发表时间:2009-07-20
我觉得10w只是楼主一个夸张的说法嘛,同一时间执行几千条sql对数据库也是个性能挑战,一个连接池里不也只有上百个连接吗,估计一个门户特定时间更新数据库,这样玩缓存就玩完了。
|
|
返回顶楼 | |