文章瀏覽數、商品點讚數等高頻計數場景,若採用單行 UPDATE cnt SET v=v+1 WHERE id=1 會產生嚴重行鎖競爭,TPS 急劇下降。本文演進三種方案,從單行 → 分表 → 分桶隨機更新,並給出基準測試參考。
CREATE TABLE article_view ( article_id INT PRIMARY KEY, view_cnt BIGINT NOT NULL DEFAULT 0 ) ENGINE=InnoDB;
並發 1000 QPS 時鎖等待 > 200ms,吞吐崩潰。
CREATE TABLE article_view ( article_id INT PRIMARY KEY, view_cnt BIGINT NOT NULL DEFAULT 0 ) ENGINE=InnoDB; -- 文章詳情表分離,計數器獨立表,減少主表鎖持有時間
讀寫分離但計數器更新仍串行,並發 2000 QPS 時仍達瓶頸。
CREATE TABLE article_view_bucket ( article_id INT NOT NULL, bucket_id TINYINT NOT NULL COMMENT '0-99 分桶', view_cnt BIGINT NOT NULL DEFAULT 0, PRIMARY KEY (article_id, bucket_id) ) ENGINE=InnoDB;
更新邏輯:
-- 隨機選擇一個桶 INSERT INTO article_view_bucket (article_id, bucket_id, view_cnt) VALUES (123, FLOOR(RAND()*100), 1) ON DUPLICATE KEY UPDATE view_cnt = view_cnt + 1;
讀取總數:
SELECT SUM(view_cnt) FROM article_view_bucket WHERE article_id = 123;
| 並發量級 | 桶數建議 | 預期鎖競爭降低 |
|---|---|---|
| < 500 QPS | 10-20 | 10-20 倍 |
| 500-5000 QPS | 50-100 | 50-100 倍 |
| > 5000 QPS | 200-1000 | 200+ 倍 |
桶數過大會增加 SUM() 掃描成本,可配合 INNODB_BUFFER_POOL 緩存熱點桶。
INCRBY article:123:view 1,每分鐘 Lua 腳本將增量寫回 MySQL。方案 | 1000 並發 QPS | 平均延遲 | P99 延遲 單行 | 1,200 | 85 ms | 420 ms 分表隔離 | 2,800 | 35 ms | 180 ms 分桶 100 桶 | 18,500 | 5 ms | 22 ms Redis+定時落地| 50,000+ | 1 ms | 5 ms