何为缓存?

将常用的数据,放入内存中,使得查询该数据的效率提升,这个常用的数据通常被称之为缓存

最简单的缓存,用java中的集合HashMap即可实现,从数据库中查询的数据放入map中,下次查询先判断该map中是否存在该数据,有就直接返回,没有再去查询数据库

在上述介绍中的缓存,有着非常多的隐患和问题,且不说缓存在读写时本身存在的问题,上述介绍的硬伤是单机性,这个HashMap是存在于当前应用的内存中,若我们的项目是分布式集群项目,拥有多个业务相同的服务,这种简易缓存就会导致service1中有缓存,而负载机制会均衡调用请求到其他服务,service2中便没有缓存

分布式缓存

基于单机版缓存的问题,希望有一款应用独立在所有的服务之外来帮我们存储缓存,从而解决分布式缓存问题

这就引出了Redis,可以将Redis看作一个数据库,不过他是一个NoSQL(Not Only SQL)数据库,Redis使用Key-Value存储数据,从种种迹象表明Redis可以说是解决分布式缓存的极好人选

缓存失效问题

缓存穿透

缓存穿透指的是查询一个不存在的数据,首先缓存中肯定没有数据,系统回去数据库中查询,发现数据库中也没有这条数据,我们就不会将这次的空结果放入缓存。这样就导致只要查询不存在的数据就会越过缓存,来查询数据库,缓存就失去了作用

针对缓存穿透问题,解决方案有很多也比较简单,例如:没查询到数据我们也保存进入缓存,这样查询空数据就不会直接越过缓存

缓存雪崩

在我们设置缓存时一般会设置它的过期时间,而当这个过期时间到的时候我们的数据库就会同时被很多次访问,从而导致数据库瞬间压力过高而雪崩,这种情况就称之为缓存雪崩

解决方案也比较简单,在设置过期时间的时候将这个时间加上个随机值,使其过期不在同一时间内发生,就减少了数据库的压力

缓存击穿

在一个时间段,因为缓存过期了,同时有非常大的并发请求请求这个过期了缓存的接口,它们在判断没有缓存成立后,就都会去查询数据库,这种缓存失效的情况称之为缓存击穿

解决方案为加锁,在同一时间锁定查数据库的操作,只能有一个线程来查询数据库查完保存缓存,其他的请求就可以走缓存了

这里的加锁,又会牵扯到分布式锁的问题,预计日后会单写一期 浅析分布式锁

缓存数据一致性问题

CAP理论简介

这个问题其实有牵扯到分布式的CAP基础理论
C 数据一致性
A 可用性
P 分区容错性
这三点不能兼得,只能三取其二
分布式理论就是建立在P分区容错性的基础之上的,所以P必不可少
剩下的组合方案就有CP和AP
也就是代表系统的强一致性或者高可用性

这里我们如果强制要求缓存数据必须和数据库数据一致,数据库改了缓存就必须改,必须一致,这就偏向于CP强一致性,这样势必会带来性能损耗

如果我们可以允许一定情况下的错误数据,来保证系统的流畅和可用,这就偏向于AP高可用

双写模式

双写模式指的是更新数据库时也会去更新缓存
双写模式的问题如下图
cache
线程一写数据库和写缓存之间有一段时间的空隙,而线程而的写数据库和写缓存操作就在这个空隙间完成,之后线程一的写缓存执行,就导致缓存的数据并非数据库中的真实数据,这种数据被称之为脏数据

针对于这个问题,有两种方案,一种是cp不允许脏数据的出现,解决办法就是加锁。加锁固然会带来性能损耗,但是可以保证cp
还有一种就是ap对数据准确性要求不是那么高,就可以忽略这个问题,允许用户拿到脏数据,来保证高可用性

失效模式

失效模式指的是在更新数据库后删除掉缓存,这样下次用户去读数据从数据库查出来就会再次放入缓存中
这种方案同样会出现一致性问题如下图
cache02

  1. 线程一修改数据库后删除缓存
  2. 线程二在一删缓存之前修改数据库
  3. 线程三在二修改完数据库后和删除缓存之前读取缓存和读取数据库
    因为缓存被一删除了,所以线程三此时必须查询数据库,若是此时二没有写好数据库,那么三获取的就是一写的数据库,然后更新缓存,这里的缓存就是脏数据

这里和双写模式相同解决思路仍然是ap和cp两种方案,加锁或者是不管
这里其实使用分布式的读写锁是比较合适的,既不太多占用资源又完成了数据的强一致性

Q.E.D.


深至缄默,如云漂泊