数据一般存储在数据库,数据库的数据落在磁盘中。如果用户的请求,都落到数据库中,如果大量请求一上来,数据库很容易崩溃,所以为了避免直接访问数据库,常常使用 Redis 作为缓存层。
引入缓存层,常常会有缓存异常的三个问题,分别是:缓存雪崩、缓存穿透、缓存击穿。
缓存雪崩
通常我们为了保证缓存中的数据与数据库中的数据一致性,会给 redis 设置过期时间,当数据过期后,用户访问的数据不在缓存中,就会访问数据库,并将数据库更新到 redis。
缓存雪崩是指在高并发的情况下,大量缓存同时失效,导致大量请求直接打到数据库或者后端服务,从而造成数据库或后端服务负载骤增,甚至崩溃的现象。它通常发生在缓存系统中的数据同时失效或者大量缓存数据在同一时间过期。
缓存雪崩的两大原因:
- 大量数据同时过期
- Redis 故障宕机
大量数据同时过期
-
均匀设置过期时间
避免将大量的数据设置成同一个过期时间,可以给数据过期加上一个随机数
-
互斥锁
如果访问的数据不在 redis 中,就加一个互斥锁,保证同一时间只有一个请求落到数据库中(从数据库中读取数据,再将数据更新到 redis 中)。当缓存构建完成后,再释放锁。未能获取互斥锁的请求,要么等待锁释放后重新读取缓存,要么就返回空值或者默认值。
实现互斥锁的时候,最好设置超时时间,不然第一个请求拿到了锁,然后这个请求发生了某种意外而一直阻塞,一直不释放锁,这时其他请求也一直拿不到锁,整个系统就会出现无响应的现象。
-
后台更新缓存
业务线程不再负责更新缓存,缓存也不设置有效期,而是让缓存“永久有效”,并将更新缓存的工作交由后台线程定时更新。
数据不设置有效期并不意味着数据能一直存在内存中:在内存不足的情况下,缓存系统需要释放一些缓存数据来腾出空间,以便存储新的数据。这种现象通常被称为缓存驱逐(Cache Eviction)
解决方法:
- 后台线程不仅负责定时更新缓存,而且也负责频繁地检测缓存是否有效
- 在业务线程发现缓存数据失效后(缓存数据被淘汰),通过消息队列发送一条消息通知后台线程更新缓存,后台线程收到消息后,在更新缓存前可以判断缓存是否存在,存在就不执行更新缓存操作;不存在就读取数据库数据,并将数据加载到缓存。这种方式相比第一种方式缓存的更新会更及时,用户体验也比较好。
redis 故障宕机
- 服务熔断或请求限流机制
- 构建 Redis 缓存高可靠集群
通过主从节点的方式构建 Redis 缓存高可靠集群
缓存击穿
是指在高并发访问的情况下,某个热点数据(缓存中的某个关键数据)失效后,大量并发请求直接访问数据库或后端服务,导致瞬时数据库或服务压力骤增,可能引起系统性能下降甚至崩溃的现象。
可以发现缓存击穿跟缓存雪崩很相似,你可以认为缓存击穿是缓存雪崩的一个子集。
应对缓存击穿可以采取前面说到两种方案:
- 互斥锁方案,保证同一时间只有一个业务线程更新缓存,未能获取互斥锁的请求,要么等待锁释放后重新读取缓存,要么就返回空值或者默认值。
- 不给热点数据设置过期时间,由后台异步更新缓存,或者在热点数据准备要过期前,提前通知后台线程更新缓存以及重新设置过期时间;
缓存穿透
当发生缓存雪崩或击穿时,数据库中还是保存了应用要访问的数据,一旦缓存恢复相对应的数据,就可以减轻数据库的压力,而缓存穿透就不一样了。
当用户访问的数据,既不在缓存中,也不在数据库中,导致请求在访问缓存时,发现缓存缺失,再去访问数据库时,发现数据库中也没有要访问的数据,没办法构建缓存数据,来服务后续的请求。那么当有大量这样的请求到来时,数据库的压力骤增,这就是缓存穿透的问题。
缓存穿透的发生一般有这两种情况:
- 业务误操作,缓存中的数据和数据库中的数据都被误删除了,所以导致缓存和数据库中都没有数据;
- 黑客恶意攻击,故意大量访问某些读取不存在数据的业务;
应对缓存穿透的方案,常见的方案有三种。
-
非法请求的限制;
当有大量恶意请求访问不存在的数据的时候,也会发生缓存穿透,因此在 API 入口处我们要判断求请求参数是否合理,请求参数是否含有非法值、请求字段是否存在,如果判断出是恶意请求就直接返回错误,避免进一步访问缓存和数据库。
-
缓存空值或者默认值;
当我们线上业务发现缓存穿透的现象时,可以针对查询的数据,在缓存中设置一个空值或者默认值,这样后续请求就可以从缓存中读取到空值或者默认值,返回给应用,而不会继续查询数据库。
-
使用布隆过滤器快速判断数据是否存在,避免通过查询数据库来判断数据是否存在;
我们可以在写入数据库数据时,使用布隆过滤器做个标记,然后在用户请求到来时,业务线程确认缓存失效后,可以通过查询布隆过滤器快速判断数据是否存在,如果不存在,就不用通过查询数据库来判断数据是否存在。
即使发生了缓存穿透,大量请求只会查询 Redis 和布隆过滤器,而不会查询数据库,保证了数据库能正常运行,Redis 自身也是支持布隆过滤器的。
缓存预热
缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统,这样就可以避免在用户请求的时候,先查询数据库,然后再将数据回写到缓存。
如果不进行预热, 那么 Redis 初始状态数据为空,系统上线初期,对于高并发的流量,都会访问到数据库中, 对数据库造成流量的压力。
缓存预热的操作方法
- 数据量不大的时候,工程启动的时候进行加载缓存动作;
- 数据量大的时候,设置一个定时任务脚本,进行缓存的刷新;
- 数据量太大的时候,优先保证热点数据进行提前加载到缓存。
总结