缓存雪崩、击穿、穿透、发生的背景
作者最近在写一个社区论坛项目,初始设想将论坛里用户发布的帖子内容存到数据库中,当用户访问论坛里的帖子时,帖子信息都从数据库中查。众所周知数据库的帖子数据是存在磁盘中的,而磁盘读写数据是计算机里最慢的(读取数度:寄存器>cpu高速缓存>内存>磁盘),所以为了避免用户直接访问数据库,我们会用redis做缓存层,将热门帖子存起来;当用户访问论坛时优先看到时存在redis缓存层的热门帖子(这似乎就是缓存预热)。
因为 Redis 是内存数据库,我们可以将数据库的数据缓存在 Redis 里,相当于数据缓存在内存,内存的读写速度比硬盘快好几个数量级,这样大大提高了系统性能
引入了缓存层,就会有缓存异常的三个问题,分别是缓存雪崩、缓存击穿、缓存穿透。
缓存雪崩、击穿、穿透会导致什么结果?
缓存雪崩、击穿、穿透发生后数据库很容易被高并发的请求冲垮
一般来说,MySQL能够处理大约每秒数百到数千个并发请求,如果超过就可能会崩溃
什么是雪崩、击穿、穿透?
缓存雪崩
通常我们为了保证缓存中的数据与数据库中的数据一致性,会给 Redis 里的数据设置过期时间,当缓存数据过期后,用户访问的数据如果不在缓存里,业务系统需要重新生成缓存,因此就会访问数据库,并将数据更新到 Redis 里,这样后续请求都可以直接命中缓存。
那么,当大量缓存数据在同一时间过期(失效)或者 Redis 故障宕机时,如果此时有大量的用户请求,都无法在 Redis 中处理,于是全部请求都直接访问数据库,从而导致数据库的压力骤增,严重的会造成数据库宕机,从而形成一系列连锁反应,造成整个系统崩溃,这就是缓存雪崩的问题。
拿项目场景举例, 当Redis中缓存的大量帖子信息在同一时间过期失效或者redis故障宕机,如果有数以万计的用户访问论坛社区,在发现页给用户推荐的帖子无法从redis中读取,只能直接访问数据库,从而导致数据库压力骤增,严重时会导致数据库宕机,整个系统崩溃,
缓存雪崩的解决方案
发生缓存雪崩的原因有两个
- 大量数据同时过期;
- Redis 故障宕机;
大量数据同时过期
针对大量数据同时过期而引发的缓存雪崩问题,常见的应对方法有下面这几种:
- 均匀设置过期时间;
- 互斥锁;
- 后台更新缓存;
1. 均匀设置过期时间
如果要给缓存数据设置过期时间,应该避免将大量的数据设置成同一个过期时间。我们可以在对缓存数据设置过期时间时,给这些数据的过期时间加上一个随机数,这样就保证数据不会在同一时间过期。
2. 互斥锁
当业务线程在处理用户请求时,如果发现访问的数据不在 Redis 里,就加个互斥锁,保证同一时间内只有一个请求来构建缓存(从数据库读取数据,再将数据更新到 Redis 里),当缓存构建完成后,再释放锁。未能获取互斥锁的请求,要么等待锁释放后重新读取缓存,要么就返回空值或者默认值。
实现互斥锁的时候,最好设置超时时间,不然第一个请求拿到了锁,然后这个请求发生了某种意外而一直阻塞,一直不释放锁,这时其他请求也一直拿不到锁,整个系统就会出现无响应的现象。
3. 后台更新缓存
业务线程不再负责更新缓存,缓存也不设置有效期,而是让缓存“永久有效”,并将更新缓存的工作交由后台线程定时更新。
事实上,缓存数据不设置有效期,并不是意味着数据一直能在内存里,因为当系统内存紧张的时候,有些缓存数据会被“淘汰”,而在缓存被“淘汰”到下一次后台定时更新缓存的这段时间内,业务线程读取缓存失败就返回空值,业务的视角就以为是数据丢失了。
解决上面的问题的方式有两种。
第一种方式,后台线程不仅负责定时更新缓存,而且也负责频繁地检测缓存是否有效,检测到缓存失效了,原因可能是系统紧张而被淘汰的,于是就要马上从数据库读取数据,并更新到缓存。
这种方式的检测时间间隔不能太长,太长也导致用户获取的数据是一个空值而不是真正的数据,所以检测的间隔最好是毫秒级的,但是总归是有个间隔时间,用户体验一般。
第二种方式,在业务线程发现缓存数据失效后(缓存数据被淘汰),通过消息队列发送一条消息通知后台线程更新缓存,后台线程收到消息后,在更新缓存前可以判断缓存是否存在,存在就不执行更新缓存操作;不存在就读取数据库数据,并将数据加载到缓存。这种方式相比第一种方式缓存的更新会更及时,用户体验也比较好。
ps业务线程不再负责更新缓存,缓存也不设置有效期,而是让缓存“永久有效”,并将更新缓存的工作交由后台线程定时更新。
事实上,缓存数据不设置有效期,并不是意味着数据一直能在内存里,因为当系统内存紧张的时候,有些缓存数据会被“淘汰”,而在缓存被“淘汰”到下一次后台定时更新缓存的这段时间内,业务线程读取缓存失败就返回空值,业务的视角就以为是数据丢失了。
解决上面的问题的方式有两种。
第一种方式,后台线程不仅负责定时更新缓存,而且也负责频繁地检测缓存是否有效,检测到缓存失效了,原因可能是系统紧张而被淘汰的,于是就要马上从数据库读取数据,并更新到缓存。
这种方式的检测时间间隔不能太长,太长也导致用户获取的数据是一个空值而不是真正的数据,所以检测的间隔最好是毫秒级的,但是总归是有个间隔时间,用户体验一般。
第二种方式,在业务线程发现缓存数据失效后(缓存数据被淘汰),通过消息队列发送一条消息通知后台线程更新缓存,后台线程收到消息后,在更新缓存前可以判断缓存是否存在,存在就不执行更新缓存操作;不存在就读取数据库数据,并将数据加载到缓存。这种方式相比第一种方式缓存的更新会更及时,用户体验也比较好。
ps:上面两种方法的缓存数据失效包含两种情况
- 缓存淘汰
- 缓存从未存在
Redis 故障宕机
针对 Redis 故障宕机而引发的缓存雪崩问题,常见的应对方法有下面这几种:
- 服务熔断或请求限流机制;
- 构建 Redis 缓存高可靠集群;
缓存击穿
如果缓存中的某个热点数据过期了,此时大量的请求访问了该热点数据,就无法从缓存中读取,直接访问数据库,数据库很容易就被高并发的请求冲垮,这就是缓存击穿的问题。
比如:社区论坛里的关于重庆胖猫的帖子热度极高,经常被用户频繁搜索访问,但该类帖子在某时间过期,就会导致该时间内大量用户直接从数据库中查询帖子信息,使数据库崩溃
应对缓存击穿可以采取前面说到两种方案:
- 互斥锁方案,保证同一时间只有一个业务线程更新缓存,未能获取互斥锁的请求,要么等待锁释放后重新读取缓存,要么就返回空值或者默认值。
- 不给热点数据设置过期时间,由后台异步更新缓存,或者在热点数据准备要过期前,提前通知后台线程更新缓存以及重新设置过期时
缓存穿透
当用户访问的数据,既不在缓存中,也不在数据库中,导致请求在访问缓存时,发现缓存缺失,再去访问数据库时,发现数据库中也没有要访问的数据,没办法构建缓存数据,来服务后续的请求。那么当有大量这样的请求到来时,数据库的压力骤增,这就是缓存穿透的问题。
缓存雪崩和击穿和认为是缓存过期时间设置过于集中和没有对热点数据缓存过期失效做预防。但都可以通过恢复缓存(即将数据库中的数据刷新到缓存)来减少对数据库的访问。发生这两个缓存问题可以把锅甩到开发者头上。
发生缓存穿透可能有两种情况
-
由于业务误操作,缓存中的数据和数据库中的数据都被误删除了,所以导致缓存和数据库中都没有数据,当用户访问该误删数据库时发生缓存击穿
-
由于黑客恶意攻击,故意大量访问某些读取不存在数据的业务,导致数据库崩溃发生缓存穿透
第一种是开发者的锅,第二种是用户的锅
应对缓存穿透的方案,常见的方案有三种。
- 第一种方案,非法请求的限制
设置中间件判断每个请求参数是否存在
-
第二种方案,缓存空值或者默认值
如果发现缓存穿透现象,就在缓存设置空值或默认值,下次同样请求来时直接通过缓存返回
-
第三种方案,使用布隆过滤器快速判断数据是否存在,避免通过查询数据库来判断数据是否存在
缓存穿透相比较与缓存雪崩、击穿对数据库的危害较小
缓存穿透对数据库的影响总体来说没有缓存雪崩和击穿的大,如果是由于大量恶意攻击,会产生较大的影响和缓存击穿的影响有点类似
接通过缓存返回
- 第三种方案,使用布隆过滤器快速判断数据是否存在,避免通过查询数据库来判断数据是否存在
缓存穿透相比较与缓存雪崩、击穿对数据库的危害较小
缓存穿透对数据库的影响总体来说没有缓存雪崩和击穿的大,如果是由于大量恶意攻击,会产生较大的影响和缓存击穿的影响有点类似
但在普通情况下的误删数据库或用户误访问非法数据,由于这种请求数据通常是少数,因此它对系统的影响相比其他类型的缓存问题较小。