缓存可以加快速度,极大提高系统性能,但也会产生许多问题,使用前需要了解清楚。
首先一个问题:为什么不能用本地缓存而要用分布式缓存呢?
(1)多个机器的本地缓存可能不一致。比如第一次负载均衡A机器,A机器查数据库,存在缓存中了,第二次负载均衡到B机器,又没有缓存,还得再查一遍数据库;更严重的情况是,一般写操作在更新数据库之后都会更新缓存,或者先使缓存失效再更新数据库,无论怎么做,如果使用本地缓存,都只能保证当前机器的混存一致性,而其他机器则会继续缓存错误的数据。
(2)像京东、淘宝等可能缓存量很大,让本应专注于业务处理的服务器承担了不必要的压力。而放在redis中,就完全可以通过集群等方式分散压力。
缓存常见的问题:
(1)缓存穿透:指查询一个数据库中不存在的数据,这样每次查到都是null,都没存入缓存,都走数据库,等于说缓存没用上。而且恶意攻击者可以专门构造这样的请求,就查你不存在的数据。
解决办法:
(A)一开始就做一些基础校验,不合法的数据,比如price<0,id<0,这样的请求不放进来
(B)把查出来的null也存入redis,这个的问题是可能导致redis中存入大量的null值。可以设置一个过期时间,比如30s,这样即使你攻击,30s内页最多查数据库一次。并且30s这些null都会删除,不会过多占用redis的内存。
(C)布隆过滤器 其实就是设置多个hash函数,然后每一个hash的结果是一个很大的bitmap。事先把每个可能出现的key值通过哈希函数哈希后将对应的bitmap点位设为1,这样每个存在的key值其实对应一组bitmap的点位。然后一个查询进来的时候,先用布隆过滤器过滤一遍,如果有一个bitmap为0,则不存在。如果都为1,则极大概率是存在的,这时候再到redis中查。优点是性能好,缺点是有极小概率误判
(A)B)(C)其实可以结合起来
(2)缓存击穿
比如混存了一个iphone15的数据,这是个热点数据,早上9点这个缓存ttl到期了,然后大量请求涌进来,由于没有缓存,一下子都来查数据库了,很容易就会造成数据库服务器的崩溃。
解决办法:
(A)加分布式锁。每次只放一个进去查库,查到后放入redis缓存才解锁。可以用双重检测锁(DCL)提高效率。
(B)设置热点数据永不过期。
(3)缓存雪崩
如果每个服务都在同一时间设置缓存,并且ttl设置得相同,就会有某个时间点所有的缓存都失效了,都去db查,这显然不行。
解决办法:设置ttl时增加一个随机量。
如何保证缓存和数据库的一致性?
(1)双写模式。即先改数据库,再改缓存。这里存在一个问题,就是比如A机器修改数据库数据为1,他刚要改缓存,可能因为机器卡顿、网络卡顿等各种原因,改的比较慢,他还没改,B机器把数据库数据改为2,并且把缓存改为2了,这时候A机器又把缓存改为1了。数据库中是2,缓存中是1,这就出现了数据不一致的问题。能不能先写缓存再写数据库呢?首先,显然这也数据不一致的问题,其次,先写数据库,后写缓存,缓存跟着数据库走,稍微有点延迟,这叫最终一致。如果缓存走在数据库前面,这叫什么?这叫错误数据。
(2)失效模式,即先修改数据库,再把缓存直接删掉。这样也有问题。可能发生这样的情况:
注:本文内容、图片参考谷粒商城项目。
总之,上述两种方法不能保证一致性。
解决办法,无论哪一种模式,更新缓存的时候都加一个过期时间,这样每隔一段时间一定会去查一次数据库,保证最终一致性。
那有的业务就是要求强一致呢,可以加分布式读写锁。读读可以并发,读写互斥,写写当然也互斥,这样保证在一个服务更新数据库数据、更新缓存的整个过程没人能进来,就保证二楼强一致。
但是频繁的加锁、解锁本身也会给系统增加很多负担,特别对于一些经常修改的数据,如果一致性还高,那可能也不应该用缓存了,就应该让他直接走数据库。
还有一个办法,使用canal。canal会伪装成一个数据库的从服务器,订阅主库的binlog,主库有什么更新,从库一定会收到,再来更新redis。binlog一定是按照数据库的更新顺序排好的,所以不会有一致性的问题。当然,从数据库更新到redis缓存更新当然会有一个很小的延迟,但这个可不是像上面两幅图的情况那样,不干预甚至永远都不一致。如果用canal,更新数据库的时候也不用再管缓存了,后台自动完成。缺点是又增加了一个组件,增加了系统的复杂度,而且多一个组件就又多一个可能的故障点。
所以还是那句话,对修改不不频繁,并且实时性要求不高的,可以用缓存,保证最终一致性即可;对于一些经常修改的数据,如果一致性还高,那可能也不应该用缓存了,就应该让他直接走数据库。