第12章:缓存系统设计
面试频率:
sum(rate(cache_hits_total[5m])) / (sum(rate(cache_hits_total[5m])) + sum(rate(cache_misses_total[5m]))) < 0.6 for: 5m labels: severity: warning annotations: summary: "缓存命中率低于60%"
# 延迟过高
- alert: HighCacheLatency
expr: |
histogram_quantile(0.99,
sum(rate(cache_operation_duration_seconds_bucket[5m])) by (le)
) > 0.01
for: 5m
labels:
severity: warning
annotations:
summary: "缓存P99延迟超过10ms"
# 热点key过多
- alert: TooManyHotKeys
expr: hot_key_count > 100
for: 5m
labels:
severity: info
annotations:
summary: "热点key数量过多"
---
## 面试问答
为什么Redis这么快?
**答案**:
1. **内存存储**:数据存储在内存,访问速度快(纳秒级)
2. **单线程**:避免线程切换和锁竞争开销
3. **I/O多路复用**:使用epoll,单线程处理多个客户端
4. **高效数据结构**:SDS、跳表、压缩列表等优化数据结构
5. **纯内存操作**:无磁盘I/O(除持久化)
**性能数据**:
GET/SET操作:100,000+ QPS Pipeline批量操作:1,000,000+ QPS
---
Redis和Memcached有什么区别?
| 特性 | Redis | Memcached |
|------|-------|-----------|
| **数据类型** | String、Hash、List、Set、ZSet | 只支持String |
| **持久化** | 支持RDB、AOF | 不支持 |
| **主从复制** | 支持 | 不支持 |
| **事务** | 支持 | 不支持 |
| **Lua脚本** | 支持 | 不支持 |
| **多线程** | 6.0后支持I/O多线程 | 多线程 |
| **性能** | 10万QPS | 100万QPS(纯KV) |
**选择建议**:
- 简单KV缓存 → Memcached
- 复杂数据结构 → Redis
---
缓存和数据库如何保证一致性?
**答案**:
**方案1:Cache-Aside(推荐)**
```go
// 写:先更新DB,再删除缓存
func Update(key string, value string) {
db.Update(key, value)
cache.Delete(key) // 删除而非更新
}
// 读:先读缓存,未命中再读DB
func Get(key string) string {
if value := cache.Get(key); value != "" {
return value
}
value := db.Get(key)
cache.Set(key, value)
return value
}
为什么删除而不是更新?
- 更新可能失败,导致脏数据
- 删除后,下次读取时自动加载最新数据
方案2:延迟双删
func Update(key string, value string) {
cache.Delete(key) // 第一次删除
db.Update(key, value)
time.Sleep(1*time.Second)
cache.Delete(key) // 第二次删除(延迟)
}
方案3:订阅binlog
DB更新 → binlog → Canal/Maxwell → MQ → 异步更新缓存
如何设计一个本地缓存?
答案:
核心要素:
- 淘汰策略:LRU/LFU/FIFO
- 过期机制:定时清理 + 惰性删除
- 并发安全:sync.RWMutex
- 统计信息:命中率、大小
type LocalCache struct {
data map[string]*cacheItem
lru *list.List
capacity int
mu sync.RWMutex
hits int64
misses int64
}
type cacheItem struct {
key string
value interface{}
expireAt time.Time
elem *list.Element
}
Redis集群如何实现?
答案:
Redis Cluster方案:
- 16384个哈希槽:slot = CRC16(key) % 16384
- 节点负责一段slot:Master1负责0-5460,Master2负责5461-10922...
- 自动故障转移:Master宕机,Slave自动提升
优点:
去中心化,无Proxy
自动分片
高可用
缺点:
✗ 不支持跨slot的multi-key操作
✗ 迁移复杂
客户端路由:
1. 客户端计算slot
2. 发送到对应节点
3. 如果MOVED,重定向到正确节点
如何处理大key问题?
答案:
识别大key:
# Redis自带工具
redis-cli --bigkeys
# 自定义脚本
redis-cli --scan --pattern "user:*" | xargs redis-cli STRLEN
解决方案:
- 拆分大key
// 不好:单个大Hash
HSET user:1000 field1 value1 field2 value2 ... field10000 value10000
// 好:拆分成多个小Hash
HSET user:1000:1 field1 value1 ... field100 value100
HSET user:1000:2 field101 value101 ... field200 value200
- 压缩value
import "compress/gzip"
func Compress(data []byte) []byte {
var buf bytes.Buffer
w := gzip.NewWriter(&buf)
w.Write(data)
w.Close()
return buf.Bytes()
}
- 异步删除
// Redis 4.0+
UNLINK big_key // 异步删除,不阻塞
缓存预热怎么做?
答案:
// 1. 系统启动时预热
func WarmUpCache() {
// 加载热点商品
products := db.GetHotProducts(100)
for _, p := range products {
cache.Set(fmt.Sprintf("product:%d", p.ID), p)
}
// 加载热门分类
categories := db.GetPopularCategories()
for _, c := range categories {
cache.Set(fmt.Sprintf("category:%d", c.ID), c)
}
}
// 2. 定时预热
func ScheduleWarmUp() {
ticker := time.NewTicker(1 * time.Hour)
for range ticker.C {
WarmUpCache()
}
}
// 3. 接入层预热
func PreloadCache(keys []string) {
for _, key := range keys {
go func(k string) {
value := db.Get(k)
cache.Set(k, value)
}(key)
}
}
如何实现分布式锁?
答案:
基于Redis的分布式锁:
// 加锁
func Lock(key string, expiration time.Duration) (string, error) {
// 生成唯一token
token := uuid.New().String()
// SET key token NX EX 10
ok, err := redis.SetNX(ctx, key, token, expiration).Result()
if err != nil || !ok {
return "", errors.New("lock failed")
}
return token, nil
}
// 解锁(Lua脚本保证原子性)
const unlockScript = `
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
`
func Unlock(key string, token string) error {
result, err := redis.Eval(ctx, unlockScript, []string{key}, token).Result()
if err != nil {
return err
}
if result == int64(0) {
return errors.New("unlock failed")
}
return nil
}
问题与改进:
- 死锁:加过期时间
- 误删锁:使用token验证
- 锁过期:Watch dog自动续期
- 不可重入:使用Hash记录重入次数
缓存击穿和缓存雪崩有什么区别?
| 维度 | 缓存击穿 | 缓存雪崩 |
|---|---|---|
| 定义 | 单个热点key过期 | 大量key同时过期 |
| 影响 | 单个热点数据的DB压力 | 整体DB压力暴增 |
| 原因 | 热点key过期 | 缓存集中过期/宕机 |
| 解决 | 互斥锁、永不过期 | 随机TTL、多级缓存 |
形象比喻:
- 击穿:城堡的一面墙被击穿
- 雪崩:整个城堡倒塌
如何评估缓存的性能?
答案:
核心指标:
- 命中率(Hit Rate)
命中率 = 命中次数 / 总请求次数
目标:> 80%
- 延迟(Latency)
P50: 50%请求的延迟
P99: 99%请求的延迟
P999: 99.9%请求的延迟
Redis目标:
P99 < 5ms
P999 < 10ms
- QPS(Queries Per Second)
单实例Redis:10万+ QPS
Redis Cluster:100万+ QPS
- 内存使用率
Memory Used / Memory Total
目标:< 80%(避免swap)
- 连接数
Connected Clients
建议:< 10000
性能测试工具:
# redis-benchmark
redis-benchmark -h 127.0.0.1 -p 6379 -c 100 -n 100000
# 结果示例
SET: 85470.09 requests per second
GET: 89525.52 requests per second