高性能
- 简洁的通讯协议,快速连接
- 操作基于内存,快速读写
- 成熟的数据结构,快速定位
- 基于牛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 ]
- allkeys-[ lru | random ]
一致性
缓存与数据库保持一致
- 低效做法:使用事务,数据库与缓存的更新
- 符合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;
}
防穿透
被故意不命中
用户伪造大量请求,故意不命中缓存,当这些请求集中转到数据库时,导致数据库挂掉,并最终导致整个系统挂掉。
如何防止缓存穿透的发生?
- 缓存空值
- 缓存数据范围
- 预先使用布隆过滤(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; // 返回空数据,或即将失效的数据,或最新的数据
}