redis 缓存问题:穿透、雪崩、污染、一致性
缓存穿透(缓存和数据库中都没有的数据)
这种情况,如果不加以处理,请求必然打在数据库,如果请求量过大,DB 就挂了。很容易被恶意攻击,比如频繁查询 id 是 -1 的数据
解决方案
- 接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截
- 从缓存取不到的数据,在数据库中也没有取到,这时也可以将 key-value 对写为 key-null,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击
- 布隆过滤器。bloomfilter就类似于一个hash set,用于快速判某个元素是否存在于集合中,其典型的应用场景就是快速判断一个key是否存在于某容器,不存在就直接返回。布隆过滤器的关键就在于hash算法和容器大小
缓存雪崩(缓存中没有但数据库中有的数据)
还有个概念是缓存击穿,击穿和雪崩的根本原因都是 redis 没有而数据库中有,二者区别就是:击穿只同一条数据高并发导致的访问量大;雪崩是多条数据累计的访问量大
原因可能是热点数据过期(既然是热点,访问就很频繁),或者大批量缓存同时过期。都会到 DB 去查询从而增加 DB 压力。
解决方案
- 热点数据永不过期
- 非热点数据,过期时间不要太集中,可以使过期时间在默认的过期时间基础上随机加上一点时间,分散过期
- 接口限流、熔断、降级处理,重要的接口特殊处理(比如秒杀,肯定并发访问高)
缓存污染(或满了)
指的是缓存中存在大量 不常访问 的数据,随着服务不断运行,很可能发生这种情况
解决方案也很简单,配置合适的淘汰策略,要么增加服务器内存
一致性
对于查询没什么好说的,都是先查缓存,有就返回;没有就查询数据库,再把数据库的写入缓存
数据不一致往往都是更新操作导致的,更新无非两种方式,要么先删除缓存再更新数据库,要么先更新数据库再删除缓存
- 先更新数据库再删缓存的问题
线程a | 线程b |
---|---|
更新数据库 | |
查询(这时查询的是缓存数据,还未更新) | |
删除缓存 |
- 先删缓存再更新数据库的问题
线程a | 线程b | 线程c |
---|---|---|
删除缓存 | ||
查询(缓存没数据,查数据库,然后写入缓存) | ||
更新数据库 | ||
查询(线程b已经写入了缓存,此时缓存和数据库就不一致了) |
解决方案:延时双删
- 先删除缓存
- 更新数据库
- 延迟 n 秒,再次删除缓存
线程a | 线程b | 线程c |
---|---|---|
删除缓存 | ||
查询(缓存没数据,查数据库,然后写入缓存) | ||
更新数据库 | ||
延迟几秒后再次删除缓存 | ||
查询(缓存没数据,查数据库,然后写入缓存) |
为什么要延时?延时多久合适?
- 数据库主从同步或多节点同步需要时间,先让数据库都一致
- 再次删除的目的是删除 线程b 写入的缓存数据,所以要等线程b写完,不然再次删除就没东西删
- 一般3-5秒足够了
redis 服务也可能不可靠,比如删除 redis 失败,解决方案可以是 队列+重试 或者订阅 binlog 两种
-
队列+重试
- 更新数据库数据
- 缓存因为种种问题删除失败
- 将需要删除的key发送至消息队列
- 自己消费消息,获得需要删除的key
- 继续重试删除操作,直到成功
-
订阅 binlog
队列+重试 会导致业务侧代码侵入比较严重,一个简单的缓存 key 操作,要使用消息队列还要不断重试。于是有了订阅 mysql 的 binlog 方式,就是类似 mysql 的主从同步思想,使用 binlog 来刷缓存