当请求比较多的时候,频繁地访问数据库会给数据库带来比较大的压力,所以此时我们会使用缓存作为缓解数据库压力的一种方式。
常见的缓存更新策略
-
Cache Aside(旁路缓存)策略
- 当修改数据时,由应用程序来更新数据库,然后同时由应用程序来更新或者删除缓存
- 低一致性需求:使用Redis的内存淘汰机制, 最多加上超时删除来兜底。
- 高一致性需求:主动更新,并以超时删除作为兜底方案。
-
Read/Write Through(读穿/写穿)策略
- Read/Write Through是应用程序只和缓存交互,不再和数据库交互。让缓存和数据库交互,相当于更新数据库的操作由缓存进行
- Read Through: 查询缓存中数据不存在时,由缓存中间件负责从数据库查询数据,并将结果写入到缓存组件,最后缓存组件将数据返回给应用
- Write Through: 当有数据更新的时候,先更新缓存中的数据,并且由缓存组件同步更新到数据库中,然后缓存组件告知应用程序更新完成
- 常用的缓存中间件,如Redis,往往不提供这种方式。所以业务场景中不常用
- Read/Write Through是应用程序只和缓存交互,不再和数据库交互。让缓存和数据库交互,相当于更新数据库的操作由缓存进行
-
Write Back(写回)策略
- Write Back策略在更新数据的时候,只更新缓存,同时将缓存数据设置为脏的,然后立马返回,并不会更新数据库。之后会通过批量异步更新的方式来更新数据库
- Write Back 策略特别适合写多的场景, 但是带来的问题是,数据不是强一致性的,而且会有数据丢失的风险
- Write Back常用于计算机底层。比如 CPU 的缓存、操作系统中文件系统的缓存都采用了Write Back策略. 当电脑在突然断电之后,之前写入的文件会有部分丢失,就是因为 Page Cache 还没有来得及刷盘造成的.
我们的写业务代码时通常使用旁路缓存策略,有可能出现缓存击穿、穿透、雪崩问题。
缓存雪崩
所谓缓存雪崩,一般有两种情况。
- 同一时间大量的key都过期了,导致访问这些key的大量请求访问数据库。
- redis宕机了,导致大量请求访问数据库。
解决方案
- 如果是大量key同一时间过期,则给key添加固定的过期时间的基础上, 再增加一个随机的过期时间。
- 如果是Redis宕机, 那么就使用主从或者集群保证Redis高可用。
- 最后,做好缓存雪崩的预防是一方面。另一方面,从服务保护的角度来说,要做好服务的限流,降级,熔断策略。即使万一发生了雪崩,也能让系统提供一定程度的服务,不至于整个宕机。
缓存穿透
缓存穿透: 请求的数据在数据库和缓存中都不存在, 那么请求就会直接访问数据库.
解决方案
- 可以缓存空对象.
- 优点是实现简单, 维护方便
- 缺点是空对象会带来额外的内存消耗, 有可能出现短暂的数据不一致问题, 当每次请求的key都不一样时失效
- 使用布隆过滤器。
- 优点是不会有多余key, 内存占用少
- 缺点是实现复杂, 且有误判的可能
缓存击穿
缓存击穿: 缓存击穿问题也叫热点Key问题,就是一个被大量请求访问的key突然失效了,大量请求打到了数据库上。
解决方案
- 互斥锁: 如果redis中没查到数据, 缓存未命中. 则获取互斥锁(setnx), 获取失败则休眠重试, 获取成功则再次查询缓存是否存在(双重检测), 没查到则查询数据库进行缓存重建
- 互斥锁注重一致性, 但是性能较差, 需要数据库与缓存强一致性选择互斥锁.
- 逻辑过期: 缓存数据时不设置过去时间,需要在将要缓存的数据中加上时间属性作为逻辑过期时间. 当缓存命中则判断是否过期, 未过期直接返回数据, 过期则获取互斥锁. 获取锁失败则说明已经有人进行缓存重建了, 那么直接返回旧数据. 获取锁成功(需要双重检测)则开启新线程进行缓存重建, 原线程直接返回保证效率.
- 逻辑过期性能好, 但是一致性差, 需要高性能选择逻辑过期.