實現高效能高併發的計數器功能
在專案中有很多場景需要應用到計數器的功能,我們經常需要給某些資料表新增一些需要經常更新的統計欄位,例如: 使用者的積分、檔案的下載次數、喜歡數、評論數、瀏覽數,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_id
, pond
)
)ENGINE=InnoDB;
小訪問量的隨機池子100個肯定多了,三五個足矣。每次訪問的時候,隨機一個數字(1-100)作為pond,如何該pond存在則更新view+1,否則插入,view=1。藉助DUPLICATE KEY,不然在程式裡是實現得先SELECT,判斷一下再INSERT或者UPDATE。
INSERT INTO article_view
(article_id
, pond
, view
)
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["HTTPUSER_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
相關文章
- 利用Redis實現高併發計數器Redis
- 【計算機內功心法】七:高併發高效能伺服器是如何實現的計算機伺服器
- 計數器方式實現非同步併發非同步
- Redis 實現高併發下的搶購 / 秒殺功能Redis
- 用PHP實現高併發伺服器PHP伺服器
- Nginx 實現高併發的原理分析Nginx
- TCP併發伺服器的程式設計實現TCP伺服器程式設計
- iOS 點贊功能高併發的思考iOS
- Springboot:高併發下耗時操作的實現Spring Boot
- django框架怎麼實現高併發Django框架
- Unity——計時器功能實現Unity
- 如何快速實現高併發短文檢索
- 基於協程的高效能高併發伺服器框架—協程模組伺服器框架
- 如何構建高可用、高併發、高效能的雲原生容器網路?
- 九種高效能可用高併發的技術架構架構
- 高併發文章瀏覽量計數系統設計
- 併發場景下資料寫入功能的實現
- 高併發系統的限流演算法與實現演算法
- 《java併發程式設計的藝術》併發底層實現原理Java程式設計
- php利用pcntl擴充套件實現高併發PHP套件
- Java ConcurrentHashMap 高併發安全實現原理解析JavaHashMap
- 一個高效能,高併發,高可用的系統是如何演變來的
- 非同步程式設計CompletableFuture實現高併發系統優化之請求合併非同步程式設計優化
- 併發數、併發以及高併發分別是什麼意思?
- Jquery 實現頁面倒數計時的功能jQuery
- Golang 併發程式設計(channel實現)Golang程式設計
- 常用高併發網路執行緒模型效能優化實現-體驗百萬級高併發執行緒模型設計執行緒模型優化
- 資料庫系列:InnoDB下實現高併發控制資料庫
- Flutter倒數計時/計時器的實現Flutter
- 如何設計高併發介面?
- 高併發設計筆記筆記
- 使用 .NET Core 高效能併發程式設計程式設計
- 實戰Java高併發程式設計模式視訊Java程式設計設計模式
- 如何提升伺服器的高併發能力伺服器
- Nginx Ingress 高併發實踐Nginx
- 線上人數統計功能怎麼實現?
- vue + axios 實現分頁引數傳遞,高階搜尋功能實現VueiOS
- 從零開始的高併發(二)--- Zookeeper實現分散式鎖分散式
- [java併發程式設計]基於訊號量semaphore實現限流器Java程式設計