MySQL 高並發計數器設計:從單行熱點到隨機分桶槽位優化方案
文章瀏覽數、商品點讚數、API 呼叫計數 — 高並發計數器是典型熱點寫入場景。單行 UPDATE counter SET cnt=cnt+1 會導致行鎖爭用、間隙鎖衝突、Redo Log 爭用,嚴重限制吞吐。本文演進三代方案,實現無鎖高並發計數。
演進路線圖
| 代數 | 架構 | 核心思想 | 並發能力 | 缺點 |
| 第 1 代 | 業務表混合計數欄 | article_view INT 在主表 | 極低 (行鎖串行) | 讀寫耦合、主表膨脹、鎖爭用劇烈 |
| 第 2 代 | 獨立計數表 + 單行 | article_id PK, view BIGINT | 低 (仍是單行熱點) | 分離了壓力但未解決熱點 |
| 第 3 代 | 分桶槽位 | article_id, slot, view 多行聚合 | 高 (分散鎖爭用) | 讀取需 SUM 聚合、槽位數需調優 |
第 3 代方案:隨機分桶槽位
表結構
CREATE TABLE article_view (
article_id BIGINT NOT NULL,
slot TINYINT UNSIGNED NOT NULL COMMENT '槽位 0-99',
view BIGINT NOT NULL DEFAULT 0,
PRIMARY KEY (article_id, slot)
) ENGINE=InnoDB;
寫入邏輯:隨機選槽 + UPSERT
-- 單條 SQL 完成「存在則加1,不存在則插1」
INSERT INTO article_view (article_id, slot, view)
VALUES (12345, FLOOR(RAND()*100), 1)
ON DUPLICATE KEY UPDATE view = view + 1;
RAND()*100:0-99 隨機槽位,將寫入壓力分散到 100 行
ON DUPLICATE KEY UPDATE:原子性 UPSERT,無需 SELECT 判斷
- 槽位數建議:CPU 核心數 × 4 - 100,視實際並發調整
讀取邏輯:聚合求和
-- 獲取文章總瀏覽數
SELECT SUM(view) AS total_views FROM article_view WHERE article_id = 12345;
效能對比(模擬 1000 並發寫入)
| 方案 | QPS | 平均延遲 | 鎖等待 |
| 主表單行 UPDATE | ~200 | 50ms+ | 極高 |
| 獨立表單行 UPDATE | ~500 | 20ms | 高 |
| 100 槽位分桶 | ~8000 | 1-2ms | 極低 |
| 1000 槽位分桶 | ~15000 | <1ms | 可忽略 |
進階優化技巧
- 熱點預熱:應用啟動時預先插入所有熱門文章的 100 槽位,避免冷啟動首次 INSERT 爭用
- 讀取快取:Redis 快取
SUM(view) 結果,定期 (如每分鐘) 同步回 MySQL 或直接以 Redis 為準
- 分區表:
PARTITION BY HASH(article_id) 進一步分散 I/O
- 非同步寫入:應用層本地累加,批次刷新 DB (如每秒 flush),極致吞吐犧牲強一致性
- 槽位動態調整:監控單槽位鎖等待,熱門文章自動擴槽 (如 100→500)
適用場景與限制
| 適用 | 不適用 |
| 文章瀏覽/點讚/分享計數 | 金融帳戶餘額、庫存扣減 (需強一致、不可丟失) |
| API 呼叫統計、錯誤計數 | 需要精確實時值的業務決策 |
| 日誌/審計行數統計 | 槽位數極大導致聚合查詢慢 (需快取) |
遷移指南:從單行遷移到分桶
- 建新分桶表
- 雙寫:應用同時寫舊單行表 + 新分桶表
- 回填歷史:舊表
view 平均分配到 100 槽位
- 驗證聚合結果一致
- 切換讀取路徑 → 下線舊表
分桶槽位是「空間換時間、隨機換確定」的經典權衡。寫入無鎖化、讀取聚合化、快取前置化 — 三板斧解決 99% 高並發計數場景。切記:槽位數不是越大越好,聚合成本與寫入分散收益需找平衡點,生產環境以壓測結果為準。