前言
缓存穿透,并发以及雪崩是分布式系统中比较典型的缓存类问题,本文将针对三种典型问题,分别说说其发生的原因以及比较通用的解决方案。
1.缓存穿透
缓存穿透指的是使用不存在的 key 进行大量高并发的查询,导致无法命中缓存,从而穿透去查询数据库,最终使数据库的压力过大,甚至会导致数据库宕机的情况。
其发生原因一般来自于恶意攻击,亦或者是业务层的无意代码(bug)造成的。
解决方案:
对于不存在的 key, 无法命中缓存后,走数据库查询返回的空值同样缓存起来,这样再次接受到同样的查询时,就能命中缓存并返回空值。如此可以保证只存在少量的穿透到数据库。
如果说攻击者猜到了我们使用了这种方案,可能会每次使用不同的参数来查询,所以我们最好还需要对参数进行过滤。举个栗子,比如说我们使用的是 ID 进行查询,则可以对 ID 的格式进行分析,如果此 ID 不符合 ID 生成的规则,是不合法的 ID,则直接拒绝, 此方案可以拦截一定无效的请求。
2.缓存并发
缓存并发一般发生在高并发的场景下,针对于那些访问量非常高的 key, 如果说这个 key 过期了,并发量非常大的时候,则导致同一时间很多请求同时访问数据库来查询最新的数据,并且写会缓存,从而导致应用和数据库的负载增加,性能降低,严重的话可能会导致数据库宕机。
解决方案:
- 1.分布式锁
通过使用分布式锁,保证对于每个 key 同时只能有一个线程去查询后端服务,其他线程阻塞,直到获取锁为止。这种方案将高并发的压力转移给了分布式锁,因此对分布式锁的要求很高。
- 2.本地锁
我们还可以通过本地锁的方式来限制只有一个线程去数据库中查询数据,其他请求阻塞,等待前面的线程查询到数据后再访问缓存。但是此种方案弊端也很明显,它只能针对线上单个节点,当服务存在节点数量较多时,还是存在多个数据库查询操作,也就是说并没有完全的解决缓存并发的问题,只是一定程度上减轻。
- 3.软过期
软过期指的是对缓存中的数据设置失效时间,而不是使用缓存中间件自提供的过期时间功能。通过在业务层存储过期时间,由业务层来判断是否过期并更新数据。代码逻辑中,每次查询,我们需要对缓存的过期时间进行判断,如果说快要过期了,代码将缓存中的时效延长,同时异步去数据库中获取最新的数据进行更新。
还可以单独部署一个异步更新服务来更新设置软过期的缓存,这样应用层就不用关心缓存并发的问题了。
3.缓存雪崩
缓存雪崩指的是由于服务器重启或者大量缓存集中在某一个时间内失效,给后端数据库造成瞬间的负载增大,甚至压垮数据库。
解决方案:
对不同的数据使用不同的失效时间,甚至是对相同数据,也设置不同的失效时间。举个栗子,我们缓存了 user 数据,会对每个用户的数据设置不同的缓存过期时间,比如说是 60 s, 然后再加上 10 以内的随机数,则过期时间为 60s ~ 70s, 这样就可以避免缓存雪崩。