1. 缓存雪崩
1.1 是什么?
两种情况会导致缓存雪崩,一是缓存的过期时间同时失效,二是redis挂了。
redis不可能把所有数据都缓存起来(内存昂贵且有限),所以redis需要对数据设置过期时间,并采用惰性删除+定期删除两种策略搭配删除过期键。如果缓存数据设置的过期时间是相同的,并且redis正好删完了这部分数据,就会导致在这段时间,缓存同时失效,全部请求都打到数据库。
1.2 怎么解决?
(1) 针对第一种情况,可以在缓存的时候给过期时间加上一个随机值,这样就能大幅度的减少缓存在同一时间过期。
(2) 对于第二种情况:
- 事发前:实现redis的高可用(主从架构+哨兵 或 redis cluster),尽量避免redis挂掉;
- 事发中:万一redis不幸挂了,可以通过设置本地缓存+限流,尽量避免数据库被干掉(这样至少服务还可用);
- 事发后:redis持久化,重启后自动从磁盘上加载数据,快速恢复缓存数据。
2. 缓存穿透
2.1 是什么?
缓存穿透是指查询一个一定不存在的数据。由于缓存不命中,且出于容错考虑,从数据库查不到数据不写入缓存,这就导致这个不存在的数据每次请求都要查库,失去了缓存的意义。
请求的数据在缓存大量不命中,导致每次请求都走数据库,可能会拖垮数据库,导致服务不可用。
2.2 怎么解决?
两个方案:
- 当从数据库查不到数据时,也将这个空对象设置到缓存,下次再请求就可以直接从缓存获取。这种情况一般会将空对象设置较短的过期时间;
- 由于请求的参数是不合法的(每次都请求不存在的参数),可以考虑使用布隆过滤器或者压缩filter提前拦截,不合法就不让这个请求到DB层。
3. 缓存并发
3.1 是啥?
在高并发场景下,当一个缓存key过期时,由于访问这个缓存key的请求量较大,多个请求同时发现缓存过期,造成多个请求会同时查库,并且回写缓存,一样可能拖垮数据库。
3.2 怎么解决?
两个个方案:
- 分布式锁
使用分布式锁,保证对于每个key同时只有一个线程去查询数据库,其他线程只需要等缓存更新,这种方式将高并发的压力转到了分布式锁。 - 软过期
业务层在发现数据即将过期时将缓存的时间延长,同时新起一个线程去查库获取最新数据更新缓存,这时候其他线程访问数据就可以一直有数据可用。
4. 缓存架构
4.1 双写
对于读操作先读缓存,没有的话再去查库,然后将数据回写缓存,最后将数据返回;对于写操作,先写库再写缓存。
- 优点:实现读写顺序的逻辑简单;
- 缺点:当存在并发更新时,由于更新数据库后的更新缓存操作是网络远程调用,所以可能存在新值被旧值覆盖的问题,因此这种方法适合并发更新请求非常小的业务场景。
4.2 异步更新
应用层只读缓存,写操作针对数据库,由异步的更新服务将数据库的存量(也是首次同步时的增量)和增量的数据更新到缓存中,并且不设置缓存过期时间,最终全量数据都会保存在缓存中,而且异步更新服务需要将更新缓存的操作串行化,比如使用消息队列,这样就可以避免出现并发更新数据库后更新缓存操作的顺序问题。在设计异步服务时要充分保证异步服务的可用性,要有完善的监控和告警,否则可能出现缓存数据和数据库不一致的问题。
这里还有两个优化点,队列中针对同一个缓存的多个更新操作其实没意义,可以增加过滤层,如果缓存更新请求消息出队列的时候发现队列中还有针对该缓存的更新请求,就丢弃该次请求消息,避免频繁更新;另一个是如果更新缓存的操作失败,需要重试,重试的时候需要考虑该操作这时候是否还是最新的,避免新操作被旧操作覆盖。
- 优点:性能好,读操作只发生在缓存层,数据库只有写操作;
- 缺点:涉及组件略多,更新服务和消息队列中调度涉及稍微复杂,系统开发和运维成本较高。
5.数据一致性
既然是通过多级缓存节点来存储大量数据,难免产生脏数据,数据一致性是必须考虑的问题。根据应用场景和实现复杂度有所取舍,比如业务容忍一小段时间内的数据不一致,那么缓存过期淘汰和更新机制就能够满足最终一致性。
5.1 先数据库后缓存
对存入缓存的数据设置过期时间,所有的写操作以数据库为准,即如果数据库写成功,缓存更新失败,那么只要到达过期时间,则后面的读请求自然会从数据库中读取新值然后回写缓存。分为两类:
- 先更新数据库再更新缓存
如上面说的双写架构,同时有请求A和请求B进行更新操作,可能会出现脏数据; - 先更新数据库再删除缓存
惰性更新缓存的方式,可以一定程度上避免机器性能消耗。
5.2 先缓存后数据库
一般来说,数据最终以数据库为准,写缓存成功,其实并不算成功。但是操作缓存快,同样分为两类:
- 先更新缓存再更新数据库
对于特定多写多读的业务,先更新缓存是十分高效的,但在并发更新数据的时候,可能出现数据不一致。该场景下的脏数据可以使用消息队列将更新数据库的消息串行化等方式加以避免。当然也需要考虑业务场景,如果只是对数值型缓存数据进行加1或减1操作,这时操作顺序也不再需要考虑了。 - 先删除缓存再更新数据库
在读操作十分频繁的情况下,存在更新缓存不一致的风险,比如同时有一个更新请求,一个查询请求,更新请求A先删除缓存,查询请求B查询不到缓存转而查库并回写缓存,然后更新请求A才更新数据库,这种情况下,缓存中的数据永远都是脏数据,只有通过给缓存设置过期时间才可能解决。这里还有另一种方法可以规避:更新请求A先删除缓存再更新数据库后异步休眠x秒最后删除缓存,即异步双删。