標簽系統是內容平台、電商、社交應用的核心功能。關係型數據庫與鍵值存儲在建模、查詢、擴展性上有本質差異。本文通過具體表結構、數據映射、核心查詢對比,揭示兩種方案的優劣與適用場景。
-- 內容表
CREATE TABLE `content` (
`id` INT UNSIGNED NOT NULL PRIMARY KEY,
`name` VARCHAR(60) DEFAULT NULL,
`created_timestamp` TIMESTAMP NULL DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 標簽表
CREATE TABLE `tag` (
`tag_name` VARCHAR(60) NOT NULL PRIMARY KEY,
`visit_count` INT UNSIGNED NOT NULL DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 多對多關聯表
CREATE TABLE `content_tag_relation` (
`content_id` INT UNSIGNED NOT NULL,
`tag_name` VARCHAR(60) NOT NULL,
PRIMARY KEY (`content_id`, `tag_name`),
FOREIGN KEY (`content_id`) REFERENCES `content`(`id`),
FOREIGN KEY (`tag_name`) REFERENCES `tag`(`tag_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 1. 獲取標簽對應的文章名
SELECT c.name
FROM content c
JOIN content_tag_relation ctr ON c.id = ctr.content_id
WHERE ctr.tag_name = 'mysql';
-- 2. 訪問量Top 3標簽
SELECT tag_name FROM tag ORDER BY visit_count DESC LIMIT 3;
優點:ACID事務、成熟生態、複雜關聯查詢靈活、工具鏈完善。
缺點:多表JOIN性能隨數據量增長下降、寫入需維護三表、水平擴展困難。
利用Redis豐富數據類型,針對不同訪問模式優化存儲:
-- String + JSON(簡單、整體讀寫)
SET string:content_id:4 '{"name":"test48601","created_timestamp":"2012-01-01 05:41:01"}'
GET string:content_id:4
-- 缺點:需應用層解析JSON,無法單獨獲取/更新某字段
-- Hash(字段級操作,高效)
HSET hset:content_id:4 name 'test48601'
HSET hset:content_id:4 created_timestamp '2012-01-01 05:41:01'
HGET hset:content_id:4 name -- 直接獲取單字段
HGETALL hset:content_id:4 -- 獲取所有字段
結論:高頻單字段訪問用Hash,整體讀寫用String+JSON。
-- ZADD score=訪問量 member=標簽名
ZADD zset:tag 680 'database'
ZADD zset:tag 469 'db2'
-- 獲取Top 3(按分數降序)
ZREVRANGEBYSCORE zset:tag +inf 0 LIMIT 0 3
-- 返回: mongodb, sql, postgresql
優勢:原生支持排序、分頁、範圍查詢,O(log N + M)極高效。
-- 內容4擁有標簽: role, mongodb, database
SADD set:content_id:4 'role' 'mongodb' 'database'
-- 獲取內容4的所有標簽
SMEMBERS set:content_id:4
-- 返回: database, role, mongodb
優勢:O(1)增刪查、自動去重、支持交並差集運算。
SELECT c.name FROM content c
JOIN content_tag_relation ctr ON c.id = ctr.content_id
WHERE ctr.tag_name = 'mysql';
方案A:應用層掃描(低效、不推薦生產)
# Python偽代碼
content_id_keys = r.keys('set:content_id:*')
for key in content_id_keys:
if r.sismember(key, 'mysql'):
content_key = key.replace('set:', 'string:')
name = json.loads(r.get(content_key))['name']
print(name)
缺點:KEYS阻塞、全掃描O(N)、應用層邏輯複雜。
方案B:反向冗余索引(推薦)
-- 標簽 -> 內容名/ID 映射集合
SADD tag:mysql 'test123' 'test133' 'test144' 'test155'
SMEMBERS tag:mysql -- 直接獲取
寫入時雙寫:SADD set:content_id:4 tag 同時 SADD tag:tag content_id。
空間換時間,查詢O(1),完美解決「反向查詢」難題。
| 維度 | MySQL | Redis |
|---|---|---|
| 數據一致性 | 強一致性(ACID) | 最終一致性(需應用保證雙寫) |
| 查詢靈活性 | 極高(任意JOIN/聚合) | 低(需預建索引結構) |
| 寫入性能 | 中等(多表、索引維護) | 極高(O(1)數據結構操作) |
| 讀取性能(熱數據) | 中等(依賴緩存/索引) | 極高(內存、專用結構) |
| 存儲成本 | 低(磁盤) | 高(內存) |
| 擴展性 | 垂直為主、分庫分表複雜 | 原生Cluster水平擴展 |
| 運維複雜度 | 成熟、工具豐富 | 需關注持久化、內存、集群運維 |
| 適用場景 | 核心業務、異構查詢、強一致要求 | 高併發標簽統計、推薦、緩存、會話 |
生產環境常采用 MySQL為主存儲(權威數據源) + Redis為輔助索引/緩存(高頻查詢加速):