极简架构的缓存分层设计:浏览器、CDN、应用与数据库的全链路缓存策略 极简架构的缓存分层设计浏览器、CDN、应用与数据库的全链路缓存策略一、同一条 SQL 被执行了 4700 次对一个营销落地页做性能分析时发现页面底部的热门文章模块每次请求都查询数据库SQL 完全一样。日活 2 万的情况下这条 SQL 一天被执行了 47 万次每个页面约 25 次。数据库 CPU 利用率中42% 的时间在处理这条查询。问题不是 SQL 慢——它执行只需要 8ms。问题是从浏览器到数据库每一层都没有做缓存。如果每一层承担它该承担的缓存责任这条 SQL 的重复执行可以降到每天 200 次0.04% 的原频率。二、全链路缓存的三层架构flowchart TD U[用户浏览器] -- L1[L1: 浏览器缓存] L1 --|Cache Miss| CDN[L2: CDN 边缘节点] CDN --|Cache Miss| APP[L3: 应用服务器] APP -- L3A[L3a: 应用内存缓存] L3A --|Cache Miss| L3B[L3b: 分布式缓存 Redis] L3B --|Cache Miss| DB[数据库] L1 -.-|Cache Hit| U CDN -.-|Cache Hit| U L3A -.-|Cache Hit| APP L3B -.-|Cache Hit| APP每层的缓存命中都在为下一层减负。L1 命中时不需要网络请求CDN 命中时不需要到达源站应用缓存命中时不需要查询数据库。2.1 浏览器缓存策略HTTP/1.1 200 OK # 强缓存1 小时内直接从浏览器缓存读取不发请求 Cache-Control: public, max-age3600, immutable # 图片等静态资源使用更长的缓存时间 Cache-Control: public, max-age31536000, immutable # 协商缓存带上 ETag服务器判断资源是否修改 ETag: abc123关键区分max-age期间浏览器完全不发请求强缓存ETag/Last-Modified是协商缓存——浏览器发请求但服务器可能返回 304 Not Modified无需传输 body。对于带有 content hash 的静态资源如app.a1b2c3.js使用immutable 1 年 max-age 是最优策略——文件内容变了 hash 就变URL 跟着变不需要担心缓存更新。2.2 CDN 缓存的分层策略# Nginx 作为源站时对不同类型的响应设置不同的 CDN 缓存规则 # 静态资源CDN 缓存 7 天 location /assets/ { add_header Cache-Control public, max-age604800, s-maxage604800; } # API 响应CDN 缓存 30 秒公共数据接口用户不敏感 location /api/public/ { add_header Cache-Control public, max-age30, s-maxage30; } # 用户私有数据CDN 不缓存 location /api/user/ { add_header Cache-Control private, no-cache, no-store; } # HTML 页面CDN 缓存 5 分钟 location / { add_header Cache-Control public, max-age300, s-maxage300; }s-maxage是专门给共享缓存CDN的指令优先级高于max-age。用它可以在浏览器和 CDN 上设置不同的缓存时长。2.3 应用层缓存实现// 应用内存缓存LRU 策略固定大小热点数据常驻 type AppCache struct { lru *lru.Cache metrics *CacheMetrics } func (c *AppCache) GetOrFetch( ctx context.Context, key string, ttl time.Duration, fetcher func(context.Context) (interface{}, error), ) (interface{}, error) { // 内存缓存命中 if val, ok : c.lru.Get(key); ok { c.metrics.hit(lru) return val, nil } c.metrics.miss(lru) // 等待数据源 val, err : fetcher(ctx) if err ! nil { return nil, fmt.Errorf(获取数据失败: %w, err) } c.lru.Add(key, val) return val, nil } // Redis 分布式缓存带防击穿的互斥锁 func (c *RedisCache) GetOrFetch( ctx context.Context, key string, ttl time.Duration, fetcher func(context.Context) (string, error), ) (string, error) { val, err : c.client.Get(ctx, key).Result() if err nil { return val, nil } if !errors.Is(err, redis.Nil) { return , fmt.Errorf(Redis 查询失败: %w, err) } // 缓存击穿防护拿不到锁的请求等待结果而非重复查询 lockKey : key :lock locked, err : c.client.SetNX(ctx, lockKey, 1, 5*time.Second).Result() if err ! nil { return , err } if !locked { // 自旋等待锁持有者完成数据加载 for i : 0; i 10; i { time.Sleep(100 * time.Millisecond) val, err c.client.Get(ctx, key).Result() if err nil { return val, nil } } return , fmt.Errorf(等待缓存加载超时) } defer c.client.Del(ctx, lockKey) val, err fetcher(ctx) if err ! nil { return , err } // 随机化 TTL 避免缓存雪崩 jitter : time.Duration(rand.Int63n(int64(ttl) / 10)) c.client.Set(ctx, key, val, ttljitter) return val, nil }随机化 TTL增加 ±10% 的抖动是防止缓存雪崩的关键技巧——不要让所有 Key 在同一时刻过期。三、缓存失效策略的渐进式设计不是所有场景都需要精确的缓存失效。按数据实时性要求分层数据类型缓存时长失效策略示例静态配置1 小时定时刷新站点名称、功能开关准实时数据30 秒时间窗口过期文章列表、热门排行用户数据5 分钟主动失效用户信息、订单状态实时数据不缓存-库存余量、支付状态主动失效的成本最高需要知道何时失效但数据一致性最好。对用户数据使用写穿透Write-Through写入数据库的同时更新缓存。func (s *UserService) UpdateUser(ctx context.Context, user *User) error { // 1. 更新数据库 if err : s.db.Update(ctx, user); err ! nil { return fmt.Errorf(数据库更新失败: %w, err) } // 2. 更新缓存写穿透 cacheKey : fmt.Sprintf(user:%d, user.ID) data, _ : json.Marshal(user) if err : s.cache.Set(ctx, cacheKey, data, 10*time.Minute); err ! nil { // 缓存更新失败不影响主流程记录告警 log.Printf(缓存更新失败(key%s): %v, cacheKey, err) } return nil }四、边界与权衡缓存穿透查询不存在的数据恶意请求查询不存在的用户 ID每次都穿透到数据库。用布隆过滤器或缓存空值设置较短的 TTL。内存缓存的 GC 压力Go 的内存缓存如 freecache、bigcache使用预分配的字节数组存储避免 GC 扫描。LRU 缓存中的指针对象会被 GC 扫描在大量缓存条目场景下 100 万可能导致 GC 暂停增加。一致性的最终答案缓存带来的数据不一致窗口是经常被高估的风险。对于用户个人数据5 分钟缓存5 分钟内用户看到旧的用户名其实不会造成实际损害。精确权衡不一致的后果 数据库查询的成本时才值得投入主动失效。不适合缓存的场景支付流程、实时库存扣减、竞拍出价——数据的实时性要求压倒一切。在这些场景中缓存是 bug 的来源而非性能优化的手段。五、总结全链路缓存的核心思想是让每层干好自己的事。浏览器缓存管用户重复访问CDN 缓存管地域分发的延迟应用缓存管数据库保护每层命中都在为下一层减负。落地方案从最低成本、最高收益开始先配置 CDN 缓存静态资源Nginx 加几行Cache-Control头→ 再为高频查询加应用内存缓存代码改动量小→ 然后在流量增长后加入 Redis 分布式缓存 → 最后配置浏览器缓存策略。缓存的 TTL 设置不是一次性完成的——上线后观察缓存命中率如 Redis 的keyspace_hits/keyspace_misses比率逐步调优。