Cache
缓存
缓存雪崩
由于缓存一般是会设置过期时间,所以,缓存大量同时失效就会出现问题。
即,缓存血崩是指缓存大量失效或者Redis故障,导致大量请求到了数据库上。
缓存雪崩原因
- 缓存大量同时失效
- Redis宕机
针对不同原因,有不同的解决方式
缓存大量同时失效
- 分散设置过期时间,尽量均匀在时间线上。
- 加互斥锁,保证只有一个请求来构建缓存,记得最好设置过期时间。
- 双key策略:主key设置过期时间,备用key永久,过期时,返回备用key的值。
- 后台更新缓存
- 定时更新
- 业务通过消息队列通知失效。
Redis宕机
由于请求量过大导致的宕机,可以通过
- 请求熔断或请求限制机制
- 构建Redis缓存高可用集群
缓存击穿
当缓存的某些热点数据过期时,大量请求打到了数据库上,导致的数据库压力过大。
- 互斥锁,保证一个只有请求构建缓存。
- 不给热点数据设置过期时间,由后台异步更新缓存,或者在热点数据准备要过期前,提前通知后台线程更新缓存以及重新设置过期时间。
缓存穿透
缓存穿透的发生一般有这两种情况:
- 业务误操作,缓存中的数据和数据库中的数据都被误删除了,所以导致缓存和数据库中都没有数据;
- 黑客恶意攻击,故意大量访问某些读取不存在数据的业务;
针对上述原因,有对应处理。
- 针对非法请求,校验请求参数来限制。
- 针对大量不存在的key,
- 使用布隆过滤器快速判断数据是否存在,避免通过查询数据库来判断数据是否存在;
- 当查询到缓存和db都不存在时,给缓存建立一个空或者错误值,防止db大量访问。
一致性
两次更新
一致性方面
无论是先更新数据库还是先更新缓存,都有可能导致数据不一致。因为无法保证两个请求分别在缓存和数据库处的更新顺序一致。
有效方面
由于是直接更新,而不是删除了再更新,缓存会一直存在,缓存可以一直命中,只不过可能有些过时。
优化
如果是一致性要求高,则可以通过一定的控制手段来解决这个问题。
- 在更新缓存前先加一个分布式锁,保证更新缓存和更新数据库的顺序。
- 更新缓存后,给缓存添加过期时间,减少数据不一致的时间。
先更新还是先删除?
先删除缓存,再更新数据库
也会发生不一致的情况,如下例。且这种情况发生概率不低。
解决方案
针对「先删除缓存,再更新数据库」方案在「读 + 写」并发请求而造成缓存不一致的解决办法是「延迟双删」。
延迟双删实现的伪代码如下:
#删除缓存
redis.delKey(X)
#更新数据库
db.update(X)
#睡眠
Thread.sleep(N)
#再删除缓存
redis.delKey(X)加了个睡眠时间,主要是为了确保请求 A 在睡眠的时候,请求 B 能够在这这一段时间完成「从数据库读取数据,再把缺失的缓存写入缓存」的操作,然后请求 A 睡眠完,再删除缓存。
所以,请求 A 的睡眠时间就需要大于请求 B 「从数据库读取数据 + 写入缓存」的时间。
但是具体睡眠多久其实是个玄学,很难评估出来,所以这个方案也只是尽可能保证一致性而已,极端情况下,依然也会出现缓存不一致的现象。
先更新数据库,再删除缓存
缓存存在时,读直接返回结果。写操作,先更新数据库,再删除缓存。
缓存不存在时,读会请求数据库。
只有在极端情况下,才会发生不一致的情况,如下例。
不难发现,这个例子的情况是比较难发生的,需要满足两个条件。
- 缓存不存在,此时读写并发。
- 读请求回写缓存,延时很长,直到写请求删除完缓存之后。
一般来说,缓存不存在,同时读写发生了并发,这个情况还是有一定的发生条件的。
此外,更新数据库的时间一般比回写缓存的时间长很多,所以这个条件很难发生。
所以,「先更新数据库 + 再删除缓存」的方案,是可以保证数据一致性的。
删除失败
但也存在一定问题,特殊情况下,在删除缓存的时候失败了,会导致缓存中的数据是旧值。
删除失败原因可能是:
- Redis网络断开
- Redis性能抖动导致删除命令超时。
针对缓存失败的情况,可以通过
- 设置过期时间,最多维持一小段不一致的时间。
- 将操作加入消息队列,由消费者来操作数据,如果应用删除缓存失败,可以从消息队列中重新读取数据,然后再次删除缓存,这个就是重试机制。
- 订阅 MySQL binlog,再操作缓存。比如,Cancel中间件,伪装成一个MySQL节点,向 MySQL 主节点发送 dump 请求,MySQL 收到请求后,就会开始推送 Binlog 给 Canal,Canal 解析 Binlog 字节流之后,转换为便于读取的结构化数据,供下游程序订阅使用。