实现高性能高并发的计数器功能

来源:互联网 发布:vc 界面编程 编辑:程序博客网 时间:2024/05/16 09:55

在项目中有很多场景需要应用到计数器的功能,我们经常需要给某些数据表添加一些需要经常更新的统计字段,例如: 用户的积分、文件的下载次数、喜欢数、评论数、浏览数,etc. 而当这些数据更新的频率比较频繁的时候,数据库的压力也随之增大不少.

通常在实现网站文章点击数的时候,是这么设计数据表的,如:”article_id, menu_id, article_name, article_content, article_author, article_view...
在article_view中记录该文章的浏览量, 而这仅仅试用于访问量比较小的场景. 在访问量大的站点应该如何设计计数器呢?

MySql计数器: 多行并行更新

对文章资讯类为主的站点,在浏览一个页面的时候不但要进行大量的查(查询上文的记录,已经所属分类的名字、热门文章资讯评论、TAG等),还要进行写操作(更新浏览数点击数)。把文章的详细内容和计数器放在一张表尽管对开发很方便,但是会造成数据库的压力过大. 那么,分两张表存放就好了么?一张表存文章详细信息,另一张表单独存计数器。

CREATE TABLE article_view(
article_id int(11) NOT NULL,
view int(11) NOT NULL,
PRIMARY KEY (article_id)
)ENGINE=InnoDB;

这种方式,虽然分担了文章表的压力,但是每当有一个进程请求更新的时候,都会产生全局的互斥锁,只能串行,不能并行。在高并发下会有较长的等待时间。

另一种比较好的办法是对每一个文章的计数器不是一行,而是多行,比如吧,一百行。每次随机更新其中一行,该文章的浏览数就是所有行的和。

CREATE TABLE article_view(
article_id int(11) NOT NULL,
pond tinyint(4) NOT NULL COMMENT '池子,就是用来随机用的',
view int(11) NOT NULL,
PRIMARY KEY (article_idpond)
)ENGINE=InnoDB;

小访问量的随机池子100个肯定多了,三五个足矣。每次访问的时候,随机一个数字(1-100)作为pond,如何该pond存在则更新view+1,否则插入,view=1。借助DUPLICATE KEY,不然在程序里是实现得先SELECT,判断一下再INSERT或者UPDATE。

INSERT INTO article_view (article_idpondview) VALUES (123, RAND()*100, 1) ON DUPLICATE KEY UPDATE view=view+1

获取指定文章的总访问量的时候:

SELECT SUM(view) FROM article_view WHERE article_id='123'

MySql计数器: 延迟更新

延迟更新功能是指我们可以给统计字段的更新设置一个延迟时间,在这个时间段内所有的更新会被累积缓存起来,然后定时地统一更新数据库。这比较适合某个字段经常需要递增或者递减,并且对实时性要求没有那么严格的情况。

以ThinkPHP的延迟更新方法为例:http://document.thinkphp.cn/manual_3_2.html#update_data
3.2.3版本开始,setInc和setDec方法支持延迟更新,用法如下:
$Article = M("Article"); // 实例化Article对象
$Article->where('id=5')->setInc('view',1); // 文章阅读数加1
$Article->where('id=5')->setInc('view',1,60); // 文章阅读数加1,并且延迟60秒更新(写入)

/ThinkPHP/Library/Think/Model.class.php

/**

  • 字段值增长
  • @access public
  • @param string $field 字段名
  • @param integer $step 增长值
  • @param integer $lazyTime 延时时间(s)
  • @return boolean */ public function setInc($field,$step=1,$lazyTime=0) { if($lazyTime>0) {// 延迟写入 $condition = $this->options['where']; $guid = md5($this->name.''.$field.''.serialize($condition)); $step = $this->lazyWrite($guid,$step,$lazyTime); if(false === $step ) return true; // 等待下次写入 } return $this->setField($field,array('exp',$field.'+'.$step)); }

/**

  • 延时更新检查 返回false表示需要延时
  • 否则返回实际写入的数值
  • @access public
  • @param string $guid 写入标识
  • @param integer $step 写入步进值
  • @param integer $lazyTime 延时时间(s)
  • @return false|integer */ protected function lazyWrite($guid,$step,$lazyTime) { if(false !== ($value = S($guid))) { // 存在缓存写入数据 if(NOW_TIME > S($guid.'time')+$lazyTime) { // 延时更新时间到了,删除缓存数据 并实际写入数据库 S($guid,NULL); S($guid.'_time',NULL); return $value+$step; }else{ // 追加数据到缓存 S($guid,$value+$step); return false; } }else{ // 没有缓存数据 S($guid,$step); // 计时开始 S($guid.'time',NOW_TIME); return false; } }

/ThinkPHP/Common/functions.php

/**

  • 缓存管理
  • @param mixed $name 缓存名称,如果为数组表示进行缓存设置
  • @param mixed $value 缓存值
  • @param mixed $options 缓存参数
  • @return mixed */ function S($name,$value='',$options=null) { static $cache = ''; if(is_array($options)){ // 缓存操作的同时初始化 $type = isset($options['type'])?$options['type']:''; $cache = Think\Cache::getInstance($type,$options); }elseif(is_array($name)) { // 缓存初始化 $type = isset($name['type'])?$name['type']:''; $cache = Think\Cache::getInstance($type,$name); return $cache; }elseif(empty($cache)) { // 自动初始化 $cache = Think\Cache::getInstance(); } if(''=== $value){ // 获取缓存 return $cache->get($name); }elseif(is_null($value)) { // 删除缓存 return $cache->rm($name); }else { // 缓存数据 if(is_array($options)) { $expire = isset($options['expire'])?$options['expire']:NULL; }else{ $expire = is_numeric($options)?$options:NULL; } return $cache->set($name, $value, $expire); } }

MySql+Memcache计数器: 延迟更新

结合上图,解析下流程图:

1.资源浏览量,比如blog详情页的浏览量 views = mysql(数据表的浏览量) + memcache(浏览量) 每次访客访问blog详情页,浏览量就会+1,使用浏览延迟更新,仅更新memcache中的浏览量,并把浏览量缓存的key hash到array中(这很重要),当memcache中的浏览量达到某一值,比如100时,做一次update mysql数据的浏览量,并即时把memcache的浏览量设置为0。

2.由于资源浏览量部分保存在memcache中,重启memcache,或者其他原因,浏览量会丢失,需要额外开发一个定时任务更新缓存的浏览量到mysql中;

3.图中浏览量定时任务(比如凌晨3点)更新memcache缓存到数据库中,这时(第一点hash的key就显得特别重要),定时任务就变成了遍历hash数组,每一个hash 的value就是一组浏览量缓存Key的集合,再遍历这些浏览量的key 获取某资源的浏览量,update到mysql,在更新前及时设置这个浏览量缓存为0,以便新的浏览量更新到缓存不受定时任务影响。

https://github.com/JingwenTian/CodeLibrary/tree/master/backend/Server/memcache

Redis计数器

$r = new Redis();
$r->connect("127.0.0.1", "6379");

$URL = $SERVER["SCRIPT_URI"];
$UA = $_SERVER["HTTP
USER_AGENT"];
$d = date("Ymd");

$userkey_ua = "stats:" . $d . ":ua:" . md5($URL);
$userkey_url = "stats:" . $d . ":url:" . md5($URL);
$userkey_glob = "stats:" . $d;

$r->sadd($userkey_ua, md5($UA));
$r->incr($userkey_url);
$r->incr($userkey_glob);

// Optionally set expire 25 hours from now one, 
// to be sure will be available until tomorrow.
$r->expire($userkey_ua, 3600 * 25);
$r->expire($userkey_url, 3600 * 25);
// we want $userkey_glob to expire in 32 days
$r->expire($userkey_glob, 3600 * 24 * 32);

...
// Somewhere at the end of the page...

echo sprintf(
"This page was visited %d times today, with %d different browsers!",
$r->get($userkey_url),
$r->scard($userkey_ua)
);

Reference

Visitor Tracking with Redis and PHP http://www.ebrueggeman.com/blog/redis-visitor-tracking
Simple realtime web counter http://redis4you.com/code.php?id=009



http://edagarli.logdown.com/posts/306223/performance-counters-for-high-concurrency-features-such-as-the-article-hits

0 0
原创粉丝点击