缓存架构演进之路:从单机 Redis 到亿级分布式多级缓存体系

系统梳理缓存架构从单机到分布式的五大演进阶段,涵盖本地缓存、Redis 集群、多级缓存、热点 Key 治理与成本优化策略

缓存是构建高性能 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 的核心功能:

  1. 监控(Monitoring):周期性检查主从节点的健康状态
  2. 通知(Notification):节点异常时通过 API 向管理员报警
  3. 自动故障转移(Automatic Failover):主节点宕机时,选举从节点为新主节点
  4. 配置提供(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:portASK 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用户浏览器极小~0msCache-Control, ETag
CDNCDN 边缘节点~10msCloudflare, Akamai
Edge CacheLVS/Nginx 边缘~20msNginx Proxy Cache, Varnish
L1 Local应用服务器本地较小~1msCaffeine, BigCache
L2 RedisRedis 集群海量~5msRedis 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) |  | (通用)  |
+-----------+  +-----------+  +---------+

企业级缓存平台通常包含:

  1. 统一接入层:通过 Proxy(如 Twemproxy、Predixy、Codis)提供统一入口,屏蔽底层分片细节
  2. 资源隔离:按业务线划分独立集群,避免相互影响
  3. 多租户管理:支持应用自助申请缓存空间、配额和过期策略
  4. 智能监控:热点 Key 自动检测、大 Key 扫描、慢查询报警
  5. 自动扩缩容:基于流量和容量指标自动进行节点扩容或 Slot 迁移

6.2 云厂商托管方案

云托管的缓存服务让企业无需关心底层运维:

云厂商服务名特点
AWSElastiCache支持 Redis 和 Memcached,自动故障转移
阿里云Tair / Redis性能增强型,支持持久内存
腾讯云CRS支持读写分离和全球多活
Google CloudMemorystore与 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:0product: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 预热注意事项

  1. 灰度执行:预热不应一次性全量写入,避免瞬间流量打满 Redis 带宽
  2. 分批控制:每批 Pipeline 控制在几百条命令以内
  3. 预热时机:选择业务低峰期执行,避开流量高峰
  4. 预热监控:记录预热成功率、耗时和写入量,失败时报警
  5. 双缓存切换:重要场景使用双缓存策略(新缓存预热完成后再切流量)

十、案例研究:电商秒杀系统的缓存架构

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 秒杀系统关键经验

  1. 页面静态化:将秒杀商品详情页生成静态 HTML,推送到 CDN,完全避开动态查询
  2. 读链路多级缓存:浏览器 -> CDN -> Nginx -> L1 -> L2,层层拦截读请求
  3. 写链路异步化:Redis 扣减后发送 MQ,异步写数据库,削峰填谷
  4. 流量漏斗:Nginx 限流 -> 网关鉴权 -> 库存预扣,逐层过滤,最终到达数据库的请求不到总量的 1%

十一、缓存成本优化策略

11.1 内存压缩

Redis 的数据结构存在固有开销,可以通过以下手段降低内存占用:

手段效果注意
Hash 结构 + ziplist小 Hash 节省 30-50%配置 hash-max-ziplist-entries
整数编码纯整数 String 仅占用 8 字节自动生效,无需干预
共用对象池Redis 内部共享 0-9999 的整数对象内部机制
序列化压缩用 MessagePack 替代 JSONCPU 换空间
淘汰低频数据自定义淘汰策略需评估业务影响
// 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

  1. 监控内存碎片率,定期执行 MEMORY PURGE
  2. 淘汰无用大 Key,使用 redis-cli --bigkeys 扫描
  3. 对 String 类型的大 Value 启用压缩(客户端压缩)
  4. 利用 Redis 的 lazyfree 机制,避免删除大 Key 阻塞主线程
  5. 按照数据热度分级,冷数据下迁到磁盘或归档存储
  6. 定期检查长期未访问的 Key,设置合理的主动过期策略
# 查看内存中 Key 的分布情况
redis-cli --memkeys-samples 1000

# 分析大 Key
redis-cli --bigkeys

# 查看内存统计
redis-cli INFO memory

结语

缓存架构的演进是一个由简单到复杂、由单体到分布式的持续迭代过程。没有最好的架构,只有最适合当前业务规模和团队能力的方案。初创团队从单机 Redis 起步是务实的选择;日活百万的平台引入 Sentinel 或 Cluster 保障高可用;千万级 QPS 的电商大促则需要多级缓存体系配合热点治理和精细化的成本管理。

无论处在哪个阶段,理解缓存的根本局限性始终重要:缓存不是数据库的替代品,而是数据库的性能加速器。在引入任何一层缓存之前,先问自己三个问题:数据一致性容忍度是多少?缓存失效策略是否清晰?故障降级方案是否就绪?做好了这些基础功课,缓存才能真正成为架构中可靠的一环。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「database」更多文章

  1. Redis 7.x 重大新特性与架构升级深度解析
  2. Redis 消息队列深度对比:Pub/Sub、Streams 与 Kafka/RabbitMQ 选型指南
  3. Redis 数据迁移与集群扩容:从单节点到分布式的大规模迁移实战