缓存是构建高性能 Web 系统的核心基础设施。从早期工程师在单机内存里放一个 HashMap 临时存数据,到今天支撑亿万 QPS 的分布式多级缓存体系,缓存架构经历了一场深刻的技术演进。理解这一演进过程,不仅有助于我们在不同业务阶段做出合理的架构选型,更能帮助我们在面对电商大促、社交热点等高并发场景时从容应对。本文将系统梳理缓存架构从单体到分布式的完整演进路径,并深入探讨热点 Key 治理、一致性哈希、缓存预热及成本优化等生产实践议题。
一、缓存基础知识:为什么需要缓存
1.1 局部性原理
缓存有效性的理论基础是计算机体系结构中的局部性原理,主要包括两个方面。
时间局部性(Temporal Locality):如果一个数据项被访问,那么它在不久的将来很可能再次被访问。例如,社交平台上某明星发布动态后,前几分钟内会被反复查看。将这条动态缓存起来,能够显著降低后端数据库压力。
空间局部性(Spatial Locality):如果一个数据项被访问,那么与它相邻的数据项也很可能被访问。例如,读取一篇长文章时,用户往往会继续浏览文章的后续段落;电商展示商品详情时,往往同时需要加载商品的规格、评价和推荐列表。
1.2 缓存的核心价值
| 维度 | 价值 | 典型指标 |
|---|---|---|
| 降低延迟 | 内存访问纳秒级 vs 磁盘访问毫秒级 | P99 从 100ms 降至 5ms |
| 减少后端负载 | 拦截 90% 以上重复读请求 | DB QPS 降低 10 倍 |
| 提升吞吐 | 单机 Redis 可达 10W+ QPS | 系统整体 TPS 提升 5-10 倍 |
| 保障可用 | 后端故障时缓存可降级兜底 | 故障期间依然提供部分服务 |
缓存并非银弹,引入缓存意味着系统中存在两份数据(缓存与数据库),必然带来一致性问题。架构演进中所有的设计取舍,本质上都是在性能、一致性和成本之间寻找平衡点。
二、第一阶段:单机 Redis
2.1 最简单的缓存形态
业务初期,单体架构下引入缓存的最直接方式是在应用服务器本地用一个 Map 做内存缓存。但这种方案存在明显缺陷:应用重启数据丢失、多个实例间数据不共享、本地内存有限。
引入单机 Redis 后,架构变为:
+----------+ +--------+ +----------+
| Client | --> | App | --> | PostgreSQL|
+----------+ +--------+ +----------+
|
v
+----------+
| Redis |
| (单节点) |
+----------+
2.2 典型配置与编码
# application.yml (Spring Boot)
spring:
redis:
host: 192.168.1.10
port: 6379
password: ${REDIS_PASSWORD}
timeout: 2000ms
lettuce:
pool:
max-active: 20
max-idle: 10
// Go 缓存封装
package cache
import (
"context"
"encoding/json"
"time"
"github.com/redis/go-redis/v9"
)
type Cache struct {
client *redis.Client
defaultTTL time.Duration
}
func (c *Cache) Get(ctx context.Context, key string, dest any) error {
data, err := c.client.Get(ctx, key).Bytes()
if err == redis.Nil {
return ErrCacheMiss
}
if err != nil {
return err
}
return json.Unmarshal(data, dest)
}
func (c *Cache) Set(ctx context.Context, key string, value any, ttl time.Duration) error {
if ttl == 0 {
ttl = c.defaultTTL
}
data, err := json.Marshal(value)
if err != nil {
return err
}
return c.client.Set(ctx, key, data, ttl).Err()
}
2.3 单机 Redis 的瓶颈
单机 Redis 虽然开发简单、运维轻量,但存在三个致命上限:
| 瓶颈项 | 上限值 | 影响 |
|---|---|---|
| 内存容量 | 单机通常 64GB - 256GB | 无法存储超大规模数据集 |
| CPU 单核 | 单线程处理命令 | 10W QPS 是天花板 |
| 可用性 | 单点故障 = 全服不可用 | 故障即雪崩 |
当业务发展到一定规模,单机 Redis 必然成为瓶颈,架构需要向更高可用、更大容量的方向演进。
三、第二阶段:Redis Sentinel 高可用
3.1 主从复制 + 自动故障转移
Redis Sentinel(哨兵)是 Redis 官方提供的高可用方案。它通过主从复制实现数据冗余,并在主节点故障时自动完成故障转移。
+-----------+
| Sentinel |
| (监控) |
+-----------+
/ | \
+----+ +-------+ +----+
| S1 | | S2 | | S3 |
+----+ +-------+ +----+
|
+----+----+
| |
+----v----+ +--v------+
| Master | | Slave |
| 读写 | | 只读 |
+---------+ +---------+
Sentinel 的核心功能:
- 监控(Monitoring):周期性检查主从节点的健康状态
- 通知(Notification):节点异常时通过 API 向管理员报警
- 自动故障转移(Automatic Failover):主节点宕机时,选举从节点为新主节点
- 配置提供(Configuration Provider):为客户端提供当前主节点地址
3.2 故障转移流程
1. Sentinel 检测到 Master 主观下线 (SDOWN)
2. 足够数量的 Sentinel 达成客观下线共识 (ODOWN)
3. 选举 Leader Sentinel(Raft 算法)
4. Leader 从 Slave 中选择最优节点(优先级、复制偏移量、RunID)
5. 向选定 Slave 发送 SLAVEOF NO ONE 提升为主节点
6. 通知其他 Slave 重新配置主节点
7. 通知客户端更新主节点地址
Sentinel 帮助业务度过初期高可用阶段,但它的主节点仍然只有一个,写入无法水平扩展,且从节点故障转移期间存在短暂的写入中断。
四、第三阶段:Redis Cluster 分片集群
4.1 数据分片原理
Redis Cluster 是官方提供的分布式方案,采用无中心架构,将 16384 个哈希槽(Slot)分配到各个主节点,每个节点负责一部分 Slot。
+-----------+ +-----------+ +-----------+
| Master-A | | Master-B | | Master-C |
| Slot 0-5k | | Slot 5-10k| | Slot 10-16k|
+-----+-----+ +-----+-----+ +-----+-----+
| | |
+----v----+ +----v----+ +----v----+
| Slave-A | | Slave-B | | Slave-C |
+---------+ +---------+ +---------+
键的哈希槽计算:
slot = CRC16(key) mod 16384
Redis Cluster 将键名映射到固定范围的 Slot,再由 Slot 映射到具体节点。这种设计使得键的路由非常高效,且 Slot 迁移时无需重新计算整个键空间。
4.2 客户端路由与 MOVED/ASK 重定向
// go-redis Cluster 客户端自动处理重定向
import "github.com/redis/go-redis/v9"
func NewClusterClient(addrs []string) *redis.ClusterClient {
return redis.NewClusterClient(&redis.ClusterOptions{
Addrs: addrs,
Password: "",
PoolSize: 20,
// 自动处理 MOVED/ASK 重定向
ReadOnly: false,
})
}
当客户端访问的 Key 不在当前节点负责的 Slot 范围内时,节点会返回 MOVED slot ip:port 或 ASK slot ip:port 重定向响应。现代客户端(如 go-redis、Jedis、lettuce)均能自动处理这些重定向,对业务透明。
4.3 集群扩容与缩容
Redis Cluster 支持在线水平扩容:
# 1. 启动新节点
redis-server --port 6380 --cluster-enabled yes
# 2. 将新节点加入集群
redis-cli --cluster add-node 192.168.1.20:6380 192.168.1.10:6379
# 3. 分配 Slot 到新节点
redis-cli --cluster reshard 192.168.1.10:6379 \
--cluster-from all \
--cluster-to <new-node-id> \
--cluster-slots 4096 \
--cluster-yes
迁移过程中,源节点和目标节点通过 MIGRATE 命令逐 Key 迁移数据。Redis 保证了迁移期间客户端请求的一致性:如果 Key 正在迁移,节点返回 ASK 重定向,客户端需要向目标节点发送 ASKING 命令后再执行操作。
五、第四阶段:多级缓存体系
当单一 Redis 集群仍无法满足极致性能要求时,多级缓存体系应运而生。多级缓存的核心思想是:数据离用户越近,访问速度越快,但容量越小、成本越高。
5.1 五级缓存架构
+-------------------------------------------------------------------+
| 用户请求链路 |
+-------------------------------------------------------------------+
| | | | |
v v v v v
+--------+ +------+ +---------+ +--------+ +------------------+
| Browser| | CDN | | Edge | | L1 | | L2 |
| Cache | | Edge | | Cache | | Local | | Redis Cluster |
| | | | | | | Cache | | |
+--------+ +------+ +---------+ +--------+ +------------------+
~0ms ~10ms ~20ms ~1ms ~5ms
最大 较大 中等 较小 海量
| 层级 | 位置 | 容量 | 延迟 | 典型技术 |
|---|---|---|---|---|
| Browser | 用户浏览器 | 极小 | ~0ms | Cache-Control, ETag |
| CDN | CDN 边缘节点 | 小 | ~10ms | Cloudflare, Akamai |
| Edge Cache | LVS/Nginx 边缘 | 中 | ~20ms | Nginx Proxy Cache, Varnish |
| L1 Local | 应用服务器本地 | 较小 | ~1ms | Caffeine, BigCache |
| L2 Redis | Redis 集群 | 海量 | ~5ms | Redis Cluster |
5.2 各级缓存详解
浏览器缓存:通过 HTTP 响应头控制,适合不常变更的静态资源。
Cache-Control: public, max-age=3600
ETag: "33a64df5"
Last-Modified: Wed, 21 Oct 2026 07:28:00 GMT
CDN 边缘缓存:将静态资源缓存到全球各地的 CDN 节点,用户请求被调度到最近的节点。CDN 缓存适合图片、CSS、JS 以及 API 响应。
Nginx / Edge 缓存:在接入层缓存热点 API 响应,避免请求打到应用服务器。
# Nginx 代理缓存配置
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=api_cache:10m;
location /api/v1/hot-products {
proxy_cache api_cache;
proxy_cache_valid 200 5m;
proxy_pass http://backend;
}
L1 本地缓存:应用进程内的内存缓存,访问速度最快但无共享能力。适合访问极其频繁且允许短暂不一致的数据。
import "github.com/allegro/bigcache/v3"
// Go 本地缓存
func NewLocalCache() *bigcache.BigCache {
cache, _ := bigcache.New(bigcache.DefaultConfig(10 * time.Minute))
return cache
}
L2 Redis 缓存:共享的分布式缓存,保证多实例间数据一致,是缓存体系的主干。
5.3 多级缓存一致性策略
多级缓存面临的核心挑战是一致性。常用策略:
| 策略 | 说明 | 适用场景 |
|---|---|---|
| TTL 过期 | 各层设置不同 TTL,自然过期 | 读多写少,容忍短暂不一致 |
| 主动失效 | 写数据时同时清除各层缓存 | 一致性要求高 |
| 消息广播 | 通过 MQ 广播缓存失效事件 | 大规模集群缓存同步 |
六、第五阶段:缓存即服务(Cache-as-a-Service)
6.1 企业级缓存平台
当公司内多个业务线都重度依赖缓存时,自建 Redis 集群的运维成本急剧上升。此时需要抽象出统一的缓存即服务平台,具备以下能力:
+---------------------+
| 缓存管控平台 |
| (申请/监控/告警) |
+----------+----------+
|
+------+------+--------+
| | |
+---v----+ +----v---+ +--v-----+
| Proxy | | Proxy | | Proxy |
| 层 | | 层 | | 层 |
+--------+ +--------+ +--------+
| | |
+--------v--+ +--v--------+ +--v------+
| Cluster-A | | Cluster-B | |Cluster-C|
| (业务线A) | | (业务线B) | | (通用) |
+-----------+ +-----------+ +---------+
企业级缓存平台通常包含:
- 统一接入层:通过 Proxy(如 Twemproxy、Predixy、Codis)提供统一入口,屏蔽底层分片细节
- 资源隔离:按业务线划分独立集群,避免相互影响
- 多租户管理:支持应用自助申请缓存空间、配额和过期策略
- 智能监控:热点 Key 自动检测、大 Key 扫描、慢查询报警
- 自动扩缩容:基于流量和容量指标自动进行节点扩容或 Slot 迁移
6.2 云厂商托管方案
云托管的缓存服务让企业无需关心底层运维:
| 云厂商 | 服务名 | 特点 |
|---|---|---|
| AWS | ElastiCache | 支持 Redis 和 Memcached,自动故障转移 |
| 阿里云 | Tair / Redis | 性能增强型,支持持久内存 |
| 腾讯云 | CRS | 支持读写分离和全球多活 |
| Google Cloud | Memorystore | 与 GCP 生态深度集成 |
托管方案的 Trade-off:节省运维成本,但单价更高,且在高并发场景下自定义能力受限。
七、热点 Key 问题与解决方案
7.1 热点 Key 的形成
热点 Key 是指被高频访问的个别 Key,可能导致单节点 CPU 或带宽被打满。典型场景:
- 电商大促期间的商品详情页缓存
- 社会热点事件的微博/文章缓存
- 排行榜、计数器等集中式数据
7.2 热点 Key 检测
# 方法1:Redis 4.0+ 的 hotkeys 参数 (需要内存采样)
redis-cli --hotkeys
# 方法2:通过 MONITOR 命令分析 (生产慎用,性能开销大)
redis-cli MONITOR | head -n 10000 | awk '{print $4}' | sort | uniq -c | sort -rn | head -20
# 方法3:Proxy 层埋点统计
# 在 redis-proxy 中记录每个 Key 的访问频率,超过阈值报警
7.3 热点 Key 解决方案
| 方案 | 原理 | 实现复杂度 | 效果 |
|---|---|---|---|
| Key 拆分 | 将热点 Key 拆分为 N 份,如 product:1001:0 到 product:1001:9 | 中 | 分散读压力到多节点 |
| 本地缓存缓冲 | L1 缓存拦截大部分读请求 | 低 | 降低 Redis 流量 |
| 读写分离 | 从节点承担读流量 | 低 | 减轻主节点压力 |
| 限流降级 | 对热点 Key 请求限流,超限时返回降级数据 | 中 | 保护后端 |
Key 拆分是最常用的方案:
// 热点 Key 拆分读取
func GetHotKeyWithSharding(ctx context.Context, client redis.UniversalClient,
baseKey string, shardCount int) (string, error) {
// 随机选择分片
shard := rand.Intn(shardCount)
key := fmt.Sprintf("%s:%d", baseKey, shard)
return client.Get(ctx, key).Result()
}
// 写入时同时写入所有分片
func SetHotKeyWithSharding(ctx context.Context, client redis.UniversalClient,
baseKey string, value string, shardCount int, ttl time.Duration) error {
pipe := client.Pipeline()
for i := 0; i < shardCount; i++ {
key := fmt.Sprintf("%s:%d", baseKey, i)
pipe.Set(ctx, key, value, ttl)
}
_, err := pipe.Exec(ctx)
return err
}
八、大规模缓存:一致性哈希与虚拟节点
8.1 普通哈希的扩容灾难
传统取模哈希 node = hash(key) % N 在节点数量 N 变化时,几乎所有 Key 的路由都会发生改变,导致缓存雪崩。
节点数 N=3 时:hash(key) % 3
节点数 N=4 时:hash(key) % 4
> 75% 的 Key 会映射到不同的节点,全部缓存失效
8.2 一致性哈希环
一致性哈希(Consistent Hashing)将节点和 Key 都映射到一个虚拟的哈希环上(0 ~ 2^32-1)。Key 顺时针找到最近的节点负责。
0 (2^32)
|
Node-A o | o Node-D
\ | /
\ | /
Key-5 o -----+----- o Key-1
/ | \
Node-B o | o Node-C
|
2^31 (中点)
Key-1 归 Node-D 管理
Key-5 归 Node-B 管理
当新增或删除节点时,只有该节点附近的 Key 需要重新映射,大部分 Key 不受影响。
8.3 虚拟节点
普通一致性哈希存在数据倾斜问题:如果节点哈希位置不均匀,部分节点可能承载大量数据。
虚拟节点(Virtual Node)方案为每个物理节点创建多个虚拟副本,均匀散布在哈希环上:
物理节点 Node-A 对应 150 个虚拟节点:
Node-A#1, Node-A#2, ..., Node-A#150
物理节点 Node-B 对应 150 个虚拟节点:
Node-B#1, Node-B#2, ..., Node-B#150
每个虚拟节点独立计算哈希值并放置在环上
Key 先映射到虚拟节点,再映射回物理节点
Go 实现示例:
package consistenthash
import (
"hash/crc32"
"sort"
"strconv"
)
type HashFunc func(data []byte) uint32
type Map struct {
hash HashFunc
replicas int // 每个物理节点的虚拟节点数
keys []int // 排序后的虚拟节点哈希值
hashMap map[int]string // 虚拟节点哈希 -> 物理节点
}
func New(replicas int, fn HashFunc) *Map {
m := &Map{
replicas: replicas,
hash: fn,
hashMap: make(map[int]string),
}
if m.hash == nil {
m.hash = crc32.ChecksumIEEE
}
return m
}
func (m *Map) Add(keys ...string) {
for _, key := range keys {
for i := 0; i < m.replicas; i++ {
hash := int(m.hash([]byte(strconv.Itoa(i) + key)))
m.keys = append(m.keys, hash)
m.hashMap[hash] = key
}
}
sort.Ints(m.keys)
}
func (m *Map) Get(key string) string {
if len(m.keys) == 0 {
return ""
}
hash := int(m.hash([]byte(key)))
// 二分查找第一个 >= hash 的位置
idx := sort.Search(len(m.keys), func(i int) bool {
return m.keys[i] >= hash
})
// 环形回绕
if idx == len(m.keys) {
idx = 0
}
return m.hashMap[m.keys[idx]]
}
九、缓存预热与预加载
9.1 为什么需要预热
新部署的缓存节点或应用重启后,缓存是空的,所有请求直接穿透到数据库,可能造成数据库过载。缓存预热(Warming)即在系统正式上线或大促前,提前将热点数据加载到缓存中。
9.2 预热策略
| 策略 | 触发时机 | 适用场景 |
|---|---|---|
| 全量预热 | 系统上线前 | 数据量可控的中小型系统 |
| 增量预热 | 定时执行 | 数据持续更新的场景 |
| 按需预热 | 首次访问时 | 长尾数据为主的场景 |
| 预测预热 | 基于算法预测热点 | 活动大促前 |
9.3 预热实现方案
// 全量预热:从数据库读取热点数据分批写入 Redis
func WarmCache(ctx context.Context, db *sql.DB, client redis.UniversalClient) error {
// 1. 读取热点数据 ID 列表
rows, err := db.QueryContext(ctx,
"SELECT id, name, price FROM products WHERE hot_score > ?", 80)
if err != nil {
return err
}
defer rows.Close()
// 2. 分批写入 Redis(避免一次性 Pipeline 过大)
const batchSize = 500
pipe := client.Pipeline()
count := 0
for rows.Next() {
var p Product
if err := rows.Scan(&p.ID, &p.Name, &p.Price); err != nil {
continue
}
data, _ := json.Marshal(p)
pipe.Set(ctx, fmt.Sprintf("product:%d", p.ID), data, 24*time.Hour)
count++
if count%batchSize == 0 {
if _, err := pipe.Exec(ctx); err != nil {
log.Printf("warm batch failed: %v", err)
}
pipe = client.Pipeline()
}
}
// 3. 提交剩余批次
if count%batchSize != 0 {
_, err = pipe.Exec(ctx)
}
log.Printf("cache warming completed: %d items", count)
return err
}
9.4 预热注意事项
- 灰度执行:预热不应一次性全量写入,避免瞬间流量打满 Redis 带宽
- 分批控制:每批 Pipeline 控制在几百条命令以内
- 预热时机:选择业务低峰期执行,避开流量高峰
- 预热监控:记录预热成功率、耗时和写入量,失败时报警
- 双缓存切换:重要场景使用双缓存策略(新缓存预热完成后再切流量)
十、案例研究:电商秒杀系统的缓存架构
10.1 业务特征
秒杀场景的核心挑战:
- 瞬时流量激增:平时 1K QPS,秒杀开始时飙升至 100K+ QPS
- 库存敏感:超卖是不可接受的,必须保证库存扣减的准确性
- 读多写少:商品详情浏览量是下单量的 100 倍以上
10.2 多级缓存架构设计
+-----------------------------------------------------------+
| 用户浏览器 |
| 静态资源缓存 (js/css/商品图片 CDN) |
+------------+----------------------------------------------+
|
+------------v----------------------------------------------+
| CDN / WAF |
| 秒杀页面静态化,边缘缓存 5 秒 |
+------------+----------------------------------------------+
|
+------------v----------------------------------------------+
| Nginx 接入层 |
| - 秒杀接口 Lua 限流(令牌桶 / 漏桶) |
| - 热点数据 Nginx Shared Dict 缓存 |
+------------+----------------------------------------------+
|
+------------v-----------+----------------+------------------+
| 微服务网关 | | |
| - 用户鉴权 | | |
| - 请求路由 | | |
+--------+---------------+ | |
| | |
+--------v----------+ +---------------v----------+ |
| 商品服务 | | 库存服务 | |
| - Caffeine L1 | | - Lua 原子扣减库存 | |
| - Redis L2 | | - 异步 MQ 同步数据库 | |
| - 缓存商品详情 | | - Redis 预扣库存 | |
+-------------------+ +--------------------------+ |
10.3 核心代码:Redis + Lua 原子扣减库存
-- stock_deduct.lua
-- KEYS[1]: 库存 Key
-- KEYS[2]: 用户已抢购记录 Key
-- ARGV[1]: 商品 ID
-- ARGV[2]: 用户 ID
-- ARGV[3]: 限每人购买数量
local stock = tonumber(redis.call('GET', KEYS[1]) or 0)
local bought = tonumber(redis.call('GET', KEYS[2]) or 0)
local limit = tonumber(ARGV[3])
if bought >= limit then
return {-1, "已达购买上限"} -- 限流
end
if stock <= 0 then
return {-2, "库存不足"} -- 无库存
end
-- 原子扣减
redis.call('DECR', KEYS[1])
redis.call('INCR', KEYS[2])
redis.call('EXPIRE', KEYS[2], 86400) -- 24h 过期
-- 发送异步消息,同步到数据库
redis.call('LPUSH', 'order_queue', cjson.encode({
user_id = ARGV[2],
product_id = ARGV[1],
time = redis.call('TIME')[1]
}))
return {1, "秒杀成功"}
// Go 调用 Lua 脚本扣减库存
func (s *SecKillService) DeductStock(ctx context.Context, userID, productID string) (int, error) {
keys := []string{
fmt.Sprintf("stock:%s", productID),
fmt.Sprintf("user:%s:%s", userID, productID),
}
argv := []any{productID, userID, s.limitPerUser}
result, err := s.client.Eval(ctx, stockDeductScript, keys, argv...).Result()
if err != nil {
return 0, err
}
arr := result.([]any)
code := arr[0].(int64)
if code != 1 {
return int(code), fmt.Errorf("%v", arr[1])
}
return 1, nil
}
10.4 秒杀系统关键经验
- 页面静态化:将秒杀商品详情页生成静态 HTML,推送到 CDN,完全避开动态查询
- 读链路多级缓存:浏览器 -> CDN -> Nginx -> L1 -> L2,层层拦截读请求
- 写链路异步化:Redis 扣减后发送 MQ,异步写数据库,削峰填谷
- 流量漏斗:Nginx 限流 -> 网关鉴权 -> 库存预扣,逐层过滤,最终到达数据库的请求不到总量的 1%
十一、缓存成本优化策略
11.1 内存压缩
Redis 的数据结构存在固有开销,可以通过以下手段降低内存占用:
| 手段 | 效果 | 注意 |
|---|---|---|
| Hash 结构 + ziplist | 小 Hash 节省 30-50% | 配置 hash-max-ziplist-entries |
| 整数编码 | 纯整数 String 仅占用 8 字节 | 自动生效,无需干预 |
| 共用对象池 | Redis 内部共享 0-9999 的整数对象 | 内部机制 |
| 序列化压缩 | 用 MessagePack 替代 JSON | CPU 换空间 |
| 淘汰低频数据 | 自定义淘汰策略 | 需评估业务影响 |
// MessagePack 压缩示例
import "github.com/vmihailenco/msgpack/v5"
func CompressData(v any) ([]byte, error) {
return msgpack.Marshal(v)
}
func DecompressData(data []byte, v any) error {
return msgpack.Unmarshal(data, v)
}
11.2 TTL 精细化设计
合理设置 TTL 是平衡命中率和内存占用的关键:
// 分层 TTL 策略
type TTLStrategy struct {
HotData time.Duration // 热点数据: 24h
WarmData time.Duration // 温数据: 4h
ColdData time.Duration // 冷数据: 30min
SessionData time.Duration // 会话数据: 30min
}
func (s *TTLStrategy) GetTTL(accessCount int) time.Duration {
switch {
case accessCount > 1000:
return s.HotData
case accessCount > 100:
return s.WarmData
default:
return s.ColdData
}
}
11.3 混合存储:内存 + 磁盘
并非所有数据都需要常驻内存。对于访问频率中等、允许稍高延迟的数据,可以采用混合存储方案:
+---------+ +-------------------+ +-----------+
| 应用 | --> | Proxy | --> | 热数据 |
| | | (智能路由) | | (Redis) |
+---------+ +---------+---------+ +-----------+
|
+-----v-----+
| 温/冷数据 |
| (RocksDB) |
+-----------+
- 热数据:访问频率最高的数据,存储在 Redis(内存)
- 温/冷数据:访问频率较低的数据,存储在磁盘型 KV(如 RocksDB、SSD-based Redis)
阿里云 Tair 的持久内存型、AWS 的 ElastiCache for Redis 的 data tiering 都提供了类似的混合存储能力。
11.4 云上成本优化 Checklist
- 监控内存碎片率,定期执行
MEMORY PURGE - 淘汰无用大 Key,使用
redis-cli --bigkeys扫描 - 对 String 类型的大 Value 启用压缩(客户端压缩)
- 利用 Redis 的
lazyfree机制,避免删除大 Key 阻塞主线程 - 按照数据热度分级,冷数据下迁到磁盘或归档存储
- 定期检查长期未访问的 Key,设置合理的主动过期策略
# 查看内存中 Key 的分布情况
redis-cli --memkeys-samples 1000
# 分析大 Key
redis-cli --bigkeys
# 查看内存统计
redis-cli INFO memory
结语
缓存架构的演进是一个由简单到复杂、由单体到分布式的持续迭代过程。没有最好的架构,只有最适合当前业务规模和团队能力的方案。初创团队从单机 Redis 起步是务实的选择;日活百万的平台引入 Sentinel 或 Cluster 保障高可用;千万级 QPS 的电商大促则需要多级缓存体系配合热点治理和精细化的成本管理。
无论处在哪个阶段,理解缓存的根本局限性始终重要:缓存不是数据库的替代品,而是数据库的性能加速器。在引入任何一层缓存之前,先问自己三个问题:数据一致性容忍度是多少?缓存失效策略是否清晰?故障降级方案是否就绪?做好了这些基础功课,缓存才能真正成为架构中可靠的一环。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。