高并发场景下的缓存架构演进:从单一 Redis 到多层缓存的实战之路
深入剖析高并发系统中缓存架构的演进路径,详细介绍本地缓存、分布式缓存、多级缓存、缓存预热、缓存雪崩/击穿/穿透的解决方案
引言:为什么需要缓存?#
在一个典型的电商系统中,读操作占比往往超过 80%。如果每次请求都打到数据库,单表的 QPS 上限大约在 1000-5000(取决于硬件配置)。当并发量达到 10 万+ 时,数据库必然成为瓶颈。
缓存是解决读性能问题的银弹,但它也带来了一系列挑战:数据一致性、缓存雪崩、缓存击穿、缓存穿透。本文将深入探讨这些问题及其解决方案。
一、缓存架构的演进路径#
阶段1:无缓存时代#
┌─────────┐
│ 客户端 │
└────┬────┘
│
▼
┌─────────┐
│Web服务 │
└────┬────┘
│
▼
┌─────────┐
│ 数据库 │ ← 瓶颈
└─────────┘
问题:数据库成为唯一瓶颈,QPS 上限低。
阶段2:单一 Redis 缓存#
┌─────────┐
│ 客户端 │
└────┬────┘
│
▼
┌─────────┐
│Web服务 │
└────┬────┘
│
├───────────┐
▼ ▼
┌─────────┐ ┌─────────┐
│ Redis │ │ 数据库 │
└─────────┘ └─────────┘
实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| type CacheService struct {
redis *redis.Client
db *sql.DB
}
func (s *CacheService) GetUser(ctx context.Context, userID string) (*User, error) {
// 1. 尝试从 Redis 获取
cached, err := s.redis.Get(ctx, fmt.Sprintf("user:%s", userID)).Result()
if err == nil {
var user User
json.Unmarshal([]byte(cached), &user)
return &user, nil
}
// 2. Redis 未命中,查询数据库
user := &User{}
if err := s.db.QueryRow("SELECT * FROM users WHERE id = ?", userID).
Scan(user); err != nil {
return nil, err
}
// 3. 写入 Redis
data, _ := json.Marshal(user)
s.redis.Set(ctx, fmt.Sprintf("user:%s", userID), data, 30*time.Minute)
return user, nil
}
|
问题:
- Redis 单点故障
- 高并发下 Redis 网络带宽成为瓶颈
- 热点 Key 导致 Redis 负载不均
阶段3:Redis 高可用集群#
┌─────────┐
│ 客户端 │
└────┬────┘
│
┌────▼────┐
│ 代理层 │
│ (Twemproxy)
└────┬────┘
│
┌────────┴────────┐
│ │
┌───▼────┐ ┌────▼───┐
│Redis │ │Redis │
│Master │◄────►│Slave │
└────────┘ └────────┘
Redis Sentinel 高可用配置:
1
2
3
4
5
6
| # sentinel.conf
port 26379
sentinel monitor mymaster 127.0.0.1 6379 2
sentinel down-after-milliseconds mymaster 5000
sentinel parallel-syncs mymaster 1
sentinel failover-timeout mymaster 60000
|
Go 代码集成:
1
2
3
4
5
6
7
8
9
10
11
| import "github.com/go-redis/redis/v8"
func NewRedisClient() *redis.Client {
return redis.NewFailoverClient(&redis.FailoverOptions{
MasterName: "mymaster",
SentinelAddrs: []string{":26379", ":26380", ":26381"},
Password: "",
PoolSize: 100,
MinIdleConns: 10,
})
}
|
阶段4:多级缓存架构#
┌─────────┐
│ 客户端 │
└────┬────┘
│
▼
┌─────────┐
│ CDN │ ← 第一级:静态资源
└────┬────┘
│
▼
┌─────────┐
│ Nginx │ ← 第二级:反向代理缓存
└────┬────┘
│
▼
┌─────────┐
│Web服务 │
└────┬────┘
│
├───────────┬───────────┐
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│本地缓存 │ │Redis集群│ │ 数据库 │
│(Bigcache)│ │ │ │ │
└─────────┘ └─────────┘ └─────────┘
多级缓存实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
| type MultiLevelCache struct {
l1 *bigcache.BigCacheServer // 本地缓存
l2 *redis.ClusterClient // Redis 集群
db *sql.DB // 数据库
}
func (c *MultiLevelCache) Get(ctx context.Context, key string) ([]byte, error) {
// L1: 本地缓存
if data, err := c.l1.Get(key); err == nil {
metrics.CacheHit("l1")
return data, nil
}
// L2: Redis 集群
data, err := c.l2.Get(ctx, key).Bytes()
if err == nil {
metrics.CacheHit("l2")
// 回写 L1
c.l1.Set(key, data)
return data, nil
}
// L3: 数据库
result := c.queryFromDB(ctx, key)
if result != nil {
// 异步回写 L1 和 L2
go func() {
c.l1.Set(key, result)
c.l2.Set(ctx, key, result, 1*time.Hour)
}()
}
return result, nil
}
|
二、本地缓存选型与实现#
2.1 BigCache vs sync.Map vs Ristretto#
| 特性 | BigCache | sync.Map | Ristretto |
|---|
| 并发性能 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| GC 压力 | 极低 | 高 | 低 |
| 淘汰策略 | LRU | 无 | TinyLFU |
| 过期时间 | 固定 | 无 | 支持 |
| 适用场景 | 高并发读 | 低并发 | 通用 |
2.2 BigCache 实战#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| import (
"github.com/allegro/bigcache/v3"
)
func NewBigCache() (*bigcache.BigCache, error) {
return bigcache.New(context.Background(), bigcache.DefaultConfig(10 * time.Minute).
WithShards(1024). // 分片数,必须是 2 的幂
WithLifeWindow(10 * time.Minute). // 条目过期时间
WithCleanWindow(5 * time.Minute). // 清理窗口
WithMaxEntrySize(500). // 最大条目大小(KB)
WithMaxEntriesInWindow(10000 * 10). // 窗口内最大条目数
WithHardMaxCacheSize(256). // 最大缓存大小(MB)
WithLogger(log.New(os.Stderr, "BigCache: ", log.LstdFlags)))
}
|
本地缓存的问题:
- 数据不一致:多个实例的本地缓存数据不一致
- 内存占用:每个实例都有完整副本,内存消耗大
- 缓存失效:需要主动通知其他实例
解决方案:Redis Pub/Sub 通知
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| type CacheInvalidationNotifier struct {
redis *redis.Client
localCache *bigcache.BigCache
}
func (n *CacheInvalidationNotifier) Subscribe() {
pubsub := n.redis.Subscribe(ctx, "cache:invalidate")
_, err := pubsub.Receive(ctx)
if err != nil {
return
}
ch := pubsub.Channel()
for msg := range ch {
// 收到失效通知,删除本地缓存
n.localCache.Delete(msg.Payload)
}
}
func (n *CacheInvalidationNotifier) Invalidate(key string) {
// 发布失效通知
n.redis.Publish(ctx, "cache:invalidate", key)
// 删除本地缓存
n.localCache.Delete(key)
// 删除 Redis 缓存
n.redis.Del(ctx, key)
}
|
三、缓存问题的解决方案#
3.1 缓存雪崩#
问题:大量缓存同时失效,请求全部打到数据库
正常情况:
请求 -> 缓存命中 (99%) -> 数据库 (1%)
雪崩情况:
请求 -> 缓存全部失效 -> 数据库 (100% 爆炸)
解决方案1:随机过期时间
1
2
3
4
5
6
7
| func SetWithRandomExpire(client *redis.Client, key string, value interface{}, baseExpire time.Duration) {
// 在基础过期时间上增加随机偏移(±20%)
randomOffset := time.Duration(rand.Int63n(int64(baseExpire) / 5))
actualExpire := baseExpire + randomOffset - (baseExpire / 10)
client.Set(ctx, key, value, actualExpire)
}
|
解决方案2:缓存预热
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| type CacheWarmer struct {
cache *MultiLevelCache
db *sql.DB
}
func (w *CacheWarmer) WarmUp(keys []string) error {
// 1. 从数据库批量查询
rows, err := w.db.Query(`
SELECT id, name, email FROM users WHERE id IN (?)
`, strings.Join(keys, ","))
if err != nil {
return err
}
defer rows.Close()
users := []User{}
for rows.Next() {
var user User
rows.Scan(&user.ID, &user.Name, &user.Email)
users = append(users, user)
}
// 2. 异步写入缓存(不阻塞启动)
go func() {
for _, user := range users {
data, _ := json.Marshal(user)
w.cache.Set(ctx, fmt.Sprintf("user:%s", user.ID), data, 1*time.Hour)
}
}()
return nil
}
|
解决方案3:互斥锁防止击穿
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
| type Lock struct {
redis *redis.Client
}
func (l *Lock) TryLock(ctx context.Context, key string, expire time.Duration) bool {
return l.redis.SetNX(ctx, fmt.Sprintf("lock:%s", key), 1, expire).Val()
}
func (l *Lock) Unlock(ctx context.Context, key string) {
l.redis.Del(ctx, fmt.Sprintf("lock:%s", key))
}
// 使用互斥锁防止缓存击穿
func (s *CacheService) GetUserWithLock(ctx context.Context, userID string) (*User, error) {
// 1. 尝试获取缓存
if user, err := s.getFromCache(ctx, userID); err == nil {
return user, nil
}
// 2. 缓存未命中,尝试获取锁
lockKey := fmt.Sprintf("query_user:%s", userID)
if !s.lock.TryLock(ctx, lockKey, 10*time.Second) {
// 获取锁失败,等待 100ms 后重试(避免并发查询数据库)
time.Sleep(100 * time.Millisecond)
return s.GetUserWithLock(ctx, userID)
}
defer s.lock.Unlock(ctx, lockKey)
// 3. 双重检查:可能其他 goroutine 已经更新了缓存
if user, err := s.getFromCache(ctx, userID); err == nil {
return user, nil
}
// 4. 查询数据库
user := s.queryFromDB(ctx, userID)
// 5. 写入缓存
s.setToCache(ctx, userID, user)
return user, nil
}
|
3.2 缓存击穿#
问题:热点 Key 过期瞬间,大量请求直接打到数据库
解决方案:布隆过滤器预判
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| import "github.com/bits-and-blooms/bloom/v3"
type CacheWithBloomFilter struct {
cache *MultiLevelCache
bloom *bloom.BloomFilter
}
func NewCacheWithBloomFilter() *CacheWithBloomFilter {
// 预估 1000 万个 Key,误判率 0.01%
bf := bloom.NewWithEstimates(10_000_000, 0.0001)
// 初始化布隆过滤器(从数据库加载所有 Key)
// ...
return &CacheWithBloomFilter{
cache: NewMultiLevelCache(),
bloom: bf,
}
}
func (c *CacheWithBloomFilter) Get(ctx context.Context, key string) ([]byte, error) {
// 1. 布隆过滤器预判
if !c.bloom.Test([]byte(key)) {
// Key 一定不存在,直接返回
return nil, ErrKeyNotFound
}
// 2. 尝试从缓存获取
return c.cache.Get(ctx, key)
}
|
3.3 缓存穿透#
问题:查询不存在的数据,缓存和数据库都没有,每次都打到数据库
解决方案1:缓存空值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| func (s *CacheService) GetUser(ctx context.Context, userID string) (*User, error) {
// 1. 尝试从缓存获取
cached, err := s.redis.Get(ctx, fmt.Sprintf("user:%s", userID)).Result()
if err == nil {
if cached == "NULL" {
// 缓存了空值
return nil, ErrUserNotFound
}
var user User
json.Unmarshal([]byte(cached), &user)
return &user, nil
}
// 2. 查询数据库
user := &User{}
err = s.db.QueryRow("SELECT * FROM users WHERE id = ?", userID).Scan(user)
if errors.Is(err, sql.ErrNoRows) {
// 用户不存在,缓存空值(较短的过期时间)
s.redis.Set(ctx, fmt.Sprintf("user:%s", userID), "NULL", 5*time.Minute)
return nil, ErrUserNotFound
}
// 3. 写入缓存
data, _ := json.Marshal(user)
s.redis.Set(ctx, fmt.Sprintf("user:%s", userID), data, 30*time.Minute)
return user, nil
}
|
解决方案2:布隆过滤器
见上文 3.2 的实现。
四、缓存更新策略#
4.1 Cache Aside(旁路缓存)#
最常用的缓存策略:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| // 读:先读缓存,未命中则读数据库,然后写缓存
func (s *CacheService) Get(ctx context.Context, key string) ([]byte, error) {
if data, err := s.cache.Get(ctx, key); err == nil {
return data, nil
}
data := s.db.Query(ctx, key)
s.cache.Set(ctx, key, data)
return data, nil
}
// 写:先更新数据库,然后删除缓存
func (s *CacheService) Set(ctx context.Context, key string, value []byte) error {
if err := s.db.Update(ctx, key, value); err != nil {
return err
}
// 延迟双删:先删除,休眠,再删除
s.cache.Delete(ctx, key)
time.Sleep(500 * time.Millisecond)
s.cache.Delete(ctx, key)
return nil
}
|
4.2 Write Through(写穿透)#
写操作同时更新缓存和数据库:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| func (s *CacheService) SetWithWriteThrough(ctx context.Context, key string, value []byte) error {
// 同时更新缓存和数据库
if err := s.cache.Set(ctx, key, value); err != nil {
return err
}
if err := s.db.Update(ctx, key, value); err != nil {
// 数据库更新失败,回滚缓存
s.cache.Delete(ctx, key)
return err
}
return nil
}
|
4.3 Write Behind(异步写回)#
写操作只更新缓存,异步批量写入数据库:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
| type WriteBehindCache struct {
cache *bigcache.BigCache
db *sql.DB
buffer chan WriteOperation
ticker *time.Ticker
}
type WriteOperation struct {
Key string
Value []byte
}
func NewWriteBehindCache() *WriteBehindCache {
c := &WriteBehindCache{
cache: NewBigCache(),
db: NewDB(),
buffer: make(chan WriteOperation, 10000),
ticker: time.NewTicker(1 * time.Second),
}
go c.flusher()
return c
}
func (c *WriteBehindCache) Set(key string, value []byte) {
// 1. 立即写入缓存
c.cache.Set(key, value)
// 2. 异步写入数据库
c.buffer <- WriteOperation{Key: key, Value: value}
}
func (c *WriteBehindCache) flusher() {
batch := []WriteOperation{}
for {
select {
case op := <-c.buffer:
batch = append(batch, op)
// 批量大小达到阈值,执行批量写入
if len(batch) >= 1000 {
c.batchWrite(batch)
batch = []WriteOperation{}
}
case <-c.ticker.C:
// 定时刷新
if len(batch) > 0 {
c.batchWrite(batch)
batch = []WriteOperation{}
}
}
}
}
func (c *WriteBehindCache) batchWrite(ops []WriteOperation) {
tx := c.db.Begin()
defer tx.Rollback()
stmt, _ := tx.Prepare("INSERT INTO cache (key, value) VALUES (?, ?) ON DUPLICATE KEY UPDATE value = ?")
defer stmt.Close()
for _, op := range ops {
stmt.Exec(op.Key, op.Value, op.Value)
}
tx.Commit()
}
|
五、监控与告警#
5.1 缓存命中率监控#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| type CacheMetrics struct {
hits atomic.Int64
misses atomic.Int64
}
func (m *CacheMetrics) RecordHit() {
m.hits.Add(1)
}
func (m *CacheMetrics) RecordMiss() {
m.misses.Add(1)
}
func (m *CacheMetrics) HitRate() float64 {
total := m.hits.Load() + m.misses.Load()
if total == 0 {
return 0
}
return float64(m.hits.Load()) / float64(total)
}
// 暴露 Prometheus 指标
func (m *CacheMetrics) Describe(ch chan<- *prometheus.Desc) {
prometheus.NewDesc("cache_hits_total", "Total cache hits", nil, nil)
prometheus.NewDesc("cache_misses_total", "Total cache misses", nil, nil)
}
|
5.2 Big Key 检测#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| func (s *CacheService) ScanBigKeys(ctx context.Context) error {
var cursor uint64
for {
keys, nextCursor, err := s.redis.Scan(ctx, cursor, "*", 100).Result()
if err != nil {
return err
}
for _, key := range keys {
// 使用 DEBUG OBJECT 命令获取 Key 信息
info, err := s.redis.Do(ctx, "debug", "object", key).Result()
if err != nil {
continue
}
// 解析序列化长度
if serializedLength := extractSerializedLength(info); serializedLength > 1024*1024 { // > 1MB
log.Printf("Big Key detected: %s, size: %d bytes", key, serializedLength)
// 发送告警
}
}
cursor = nextCursor
if cursor == 0 {
break
}
}
return nil
}
|
六、最佳实践总结#
- 缓存不是银弹:过度缓存会带来复杂度,要从业务需求出发
- 多级缓存:L1(本地)+ L2(Redis)+ L3(数据库)是成熟方案
- 防止雪崩:随机过期时间 + 缓存预热 + 互斥锁
- 防止击穿:热点 Key 永不过期 + 布隆过滤器
- 防止穿透:缓存空值 + 布隆过滤器
- 监控先行:缓存命中率、大 Key、慢查询都要监控
- 优雅降级:缓存故障时,要有降级方案
缓存是性能优化的利器,但也是复杂度的来源。理解业务,合理设计,才是王道。