HiHuo
首页
博客
手册
工具
关于
首页
博客
手册
工具
关于
  • 系统设计实战

    • 系统设计面试教程
    • 系统设计方法论
    • 01-短链系统设计
    • 02 - 秒杀系统设计
    • 03 - IM 即时通讯系统设计
    • 04 - Feed 流系统设计
    • 05 - 分布式 ID 生成器设计
    • 06 - 限流系统设计
    • 第7章:搜索引擎设计
    • 08 - 推荐系统设计
    • 09 - 支付系统设计
    • 10 - 电商系统设计
    • 11 - 直播系统设计
    • 第12章:缓存系统设计
    • 第13章:消息队列设计
    • 第14章:分布式事务
    • 15 - 监控系统设计

第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 → 异步更新缓存

如何设计一个本地缓存?

答案:

核心要素:

  1. 淘汰策略:LRU/LFU/FIFO
  2. 过期机制:定时清理 + 惰性删除
  3. 并发安全:sync.RWMutex
  4. 统计信息:命中率、大小
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

解决方案:

  1. 拆分大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
  1. 压缩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()
}
  1. 异步删除
// 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、多级缓存

形象比喻:

  • 击穿:城堡的一面墙被击穿
  • 雪崩:整个城堡倒塌

如何评估缓存的性能?

答案:

核心指标:

  1. 命中率(Hit Rate)
命中率 = 命中次数 / 总请求次数
目标:> 80%
  1. 延迟(Latency)
P50: 50%请求的延迟
P99: 99%请求的延迟
P999: 99.9%请求的延迟

Redis目标:
P99 < 5ms
P999 < 10ms
  1. QPS(Queries Per Second)
单实例Redis:10万+ QPS
Redis Cluster:100万+ QPS
  1. 内存使用率
Memory Used / Memory Total
目标:< 80%(避免swap)
  1. 连接数
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

Prev
11 - 直播系统设计
Next
第13章:消息队列设计