缓存设计中的要点

高性能

  • 简洁的通讯协议,快速连接
  • 操作基于内存,快速读写
  • 成熟的数据结构,快速定位
  • 基于牛X的语言实现,快速运行
  • 主从架构,读写分离

高可用

  • 异常监控,自动化应急
  • 数据灾备,快速恢复
  • 主从架构,从服务上位
  • 分布式、集群化
  • 服务自动降级
  • 多级缓存

存储

  • 成熟的数据结构,减少冗余
  • 数据压缩方案,减小数据大小
  • 置换算法:FIFO、LRU、LFU
  • 索引,以空间换时间
  • 数据分片,快捷扩容、无限扩容
  • 一定要设置过期时间!

常见缓存过期策略

策略 优点 缺点
定时过期
每个设置了过期时间的key,都携带一个定时器做倒计时,到期自动删除
立即清除,无空间浪费 定时器占用大量cpu,影响性能
惰性过期
只有当访问的时候,才会判断是否过期,过期则删除
最大化节省cpu 过期数据未被访问时,会占用存储,造成空间浪费
定期过期
每隔一定时间,随机扫描一定数量带有有效期的key,并清除其中已过期的key
前两种的这种方案 难点
需要合理设置 “时间”、 “数量”

常见存储置换策略:FIFO、LRU、LFU

  • FIFO(first in first out)
    • 淘汰最早的数据
  • LRU(least recently used)
    • 淘汰最长时间未被使用的数据
  • LFU(least frequently used)
    • 淘汰使用次数最少的数据

Redis置换策略配置(maxmemory-policy)

  • noeviction:不置换
  • volatile-[ lru | random | ttl ]
    • 对具备有效期的key,按lru/随机/最短置换
  • allkeys-[ lru | random ]
    • 对所有key,按lru/随机置换

一致性

缓存与数据库保持一致
- 低效做法:使用事务,数据库与缓存的更新
- 符合ACID
- 业务普遍做法:先更新数据库,再删除缓存
- 严谨但复杂的做法:消息队列、订阅binlog

旁路缓存原则:

读操作:
- 先读缓存
- 如果命中,直接返回
- 如果未命中,访问DB,并写入缓存

写操作:
- 删缓存,而不是更新缓存
- 先写DB,再删缓存

问题:
以下方式,存在什么隐患?
- 先更新缓存,再更新数据库
- 先删除缓存,再更新数据库
- 先更新数据库,再更新缓存

防雪崩

高并发时失效
雪崩:在用户高并发瞬间,如果缓存不可用(失效),用户的请求压力,都转到数据库,导致数据库挂掉,并最终导致整个系统挂掉。

如何防止缓存雪崩的发生?

  • 缓存服务高可用
    • 服务挂了,做什么都没用
  • 多个缓存不能同时过期
    • 随机失效,而不是同时或定点
  • 多级缓存
    • 多级缓存,尽可能把数据库挡在后面
  • 使用互斥锁
    • 只有拿到锁的请求,才能查库
  • 排队限流
    • 控制查库的并发峰值
  • 提前预热
    • 预先更新缓存
  • 公用性质缓存更应重视
    • 如:首页、推荐位
    • 个性化数据相对风险小
// 业务中改成这样是否有问题
function getDataBuyKey($strKey) {
    // 从缓存中取数据
    $arrData = getCacheData($strKey);
    // 如果未命中缓存,或缓存即将失效,抢占锁
    if (!$arrData || $arrData['ttl'] - time() < 10) {
        // 如果抢锁成功(带有效期的锁),可以查库
        if (redis::set('lock_' . $strKey, 1, array('nx', 'ex' => 10))) {
            $arrData = getDbData($strKey); // 查库
            setCacheData($arrData); // 更新缓存
        }
    }
    return $arrData;
}

防穿透

被故意不命中
用户伪造大量请求,故意不命中缓存,当这些请求集中转到数据库时,导致数据库挂掉,并最终导致整个系统挂掉。

如何防止缓存穿透的发生?

  • 缓存空值
    • 额外缓存一份空值,以防反复查库
  • 缓存数据范围
    • 如,应用ID范围、页码范围、时间段
  • 预先使用布隆过滤(bloom filter)
    • 一定不存在的数据,请求将被拦截
function getDataBuyKey($strKey) {
    $arrData = [];
    // 先执行key的有效验证
    if (checkKey($strKey)) {
        // 从缓存中取数据,未取到,返回null
        $arrData = getCacheData($strKey);
    }
    // 如果未命中缓存,或者缓存即将失效,抢占锁
    if (is_null($arrData) || $arrData['ttl'] - time() < 10) {
        // 如果抢锁成功(带有效期的锁),可以查库
        if (redis::set('lock_' . $strKey, 1, array('nx', 'ex' => 10))) {
            $arrData = (array)getDbData($strKey); // 查库
            setCacheData($arrData); // 更新缓存,若为空值,写入"[]"
        }
        is_null($arrData) && $arrData = []
    }
    return $arrData; // 返回空数据,或即将失效的数据,或最新的数据
}

发表评论