目录
什么是缓存预热、缓存雪崩、缓存击穿、缓存穿透?
缓存预热
缓存雪崩
解决方案
针对Redis故障宕机
针对大量key同时过期
缓存击穿
解决方案
缓存穿透
解决方案
总结
数据库和缓存如何保证一致性?
先更新缓存还是先更新数据库?
异常情况下引发的数据一致性问题
1.先更新数据库,再更新缓存
2.先更新缓存,再更新数据库
并发情况下引发的一致性问题
1.先更新数据库,再更新缓存
解决方案
2.先更新缓存,再更新数据库
先删除缓存再更新数据库还是先更新数据库再删除缓存?
并发情况下引发的一致性问题
1.先删除缓存,再更新数据库
解决方案
2.先更新数据库,再删除缓存
如何保证两步都成功?
1.重试机制
2.订阅数据库变更日志:
总结
什么是缓存预热、缓存雪崩、缓存击穿、缓存穿透?
缓存预热
缓存预热就是系统上线后,提前将相关的缓存数据直接加载到缓存系统。避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据!
缓存雪崩
当大量key同时过期或Redis故障宕机时,如果有大量的用户请求,都无法再Redis中处理,于是全部请求都直接访问数据库,从而导致数据库压力骤增,严重会导致数据库宕机,从而形成一系列连锁反应,造成整个数据库崩溃,这就是缓存雪崩。
解决方案
针对Redis故障宕机
1.采用Redis集群,避免单机出现问题导致整个缓存服务都没法使用
2.服务熔断或请求限流机制:因为Redis宕机而导致的缓存雪崩问题时,可以启动服务熔断机制,暂停业务应用对缓存服务的访问,直接返回错误,不再继续访问数据库,从而降低对数据库的压力,保证数据库系统的正常运行,然后等到 Redis 恢复正常后,再允许业务应用访问缓存服务。
服务熔断机制是保护数据库的正常允许,但是暂停了业务应用访问缓存服系统,全部业务都无法正常工作,为了减少对业务的影响,我们可以启用请求限流机制,只是将少部分请求发送到数据库进行处理,再多的请求就在入口处直接拒绝服务,等到Redis恢复正常并把缓存预热完后,在解除请求的限流机制。
针对大量key同时过期
1.均匀key过期时间,应避免大量key同一时间过期。
2.互斥锁:在线程处理业务请求时,如果发现访问的数据不在Redis数据库中,就加个互斥锁,保证同一时间只有一个请求从数据库中读取数据,再将缓存更新到Redis中。(设置互斥锁时,最好设置超时时间,避免当请求拿到锁后,这个请求发生某种情况一直阻塞,无法释放锁,导致其他请求也拿不到锁,系统发生无响应现象)。
3.后台更新缓存:业务线程不再负责更新缓存,缓存也不设置有效期,而是让缓存“永久有效”,并让更新缓存的工作交由后台线程定时更新。
缓存击穿
如果缓存中的某个热点数据过期了,此时大量请求访问了该热点数据,就无法从缓存中读取,直接访问数据库,数据库就很容易被高并发的请求冲垮,这就是缓存击穿。
解决方案
1.互斥锁方案,保证同一时间只有一个业务线程在更新缓存,未获取互斥锁的请求,要么等待锁释放后重新读取缓存,要么就返回空值或者默认值。
2.不给热点数据设置过期时间,由后台异步更新缓存,或者在热点数据准备要过期前,提前通知后台线程更新缓存以及重新设置过期时间;
3.差异失效时间:新建两块缓存,主A从B,先更新B在更新A,严格按照这个顺序。当查询缓存时,先查询主缓存A,如果A中没有所需数据,在查询从缓存B。
缓存穿透
当用户访问数据时,数据既不在缓存中,也不在数据库中,导致请求在访问缓存时,发现缓存缺失,再去访问数据库时,发现数据库中也没有要访问的数据,没法构建缓存数据,来服务后续的请求。那么当有大量这样的请求到来时,数据库压力骤增,这就是缓存穿透。
缓存穿透的发生一般有两种情况:
1.业务操作失误,缓存中的数据和数据库中的数据都被删除了,所以导致缓存和数据库中都没有数据;
2.黑客恶意攻击。故意大量访问不存在的数据的业务。
解决方案
1.非法请求的限制:当有大量恶意请求访问不存在的数据的时候,也会发生缓存穿透,因此在 API 入口处我们要判断求请求参数是否合理,请求参数是否含有非法值、请求字段是否存在,如果判断出是恶意请求就直接返回错误,避免进一步访问缓存和数据库。
2.设置空值或默认值:当我们线上业务发现缓存穿透的现象时,可以针对查询的数据,在缓存中设置一个空值或者默认值,这样后续请求就可以从缓存中读取到空值或者默认值,返回给应用,而不会继续查询数据库。
3.使用布隆过滤器快速判断数据是否存在,避免通过查询数据库来判断数据是否存在:我们可以在写入数据库数据时,使用布隆过滤器做个标记,然后在用户请求到来时,业务线程确认缓存失效后,可以通过查询布隆过滤器快速判断数据是否存在,如果不存在,就不用通过查询数据库来判断数据是否存在,即使发生了缓存穿透,大量请求只会查询 Redis 和布隆过滤器,而不会查询数据库,保证了数据库能正常运行,Redis 自身也是支持布隆过滤器的。
需要注意的是布隆过滤器可能会存在误判的情况。总结来说就是:布隆过滤器说某个元素存在,小概率会误判。布隆过滤器说某个元素不在,那么这个元素一定不在。
总结
数据库和缓存如何保证一致性?
先更新缓存还是先更新数据库?
先不考虑并发问题,正常情况下,无论谁先谁后,都会更新成功,但异常情况下可能会发生【第一步操作成功、第二步操作失败的情况】
异常情况下引发的数据一致性问题
1.先更新数据库,再更新缓存
更新数据库成功,更新缓存失败,那么此时数据库中是最新值,缓存中是旧值,之后读到的就是旧值,只有当缓存失效后,才能从数据库中得到正确得值。
2.先更新缓存,再更新数据库
更新缓存成功,更新数据库失败,那么此时缓存中是最新值,数据库中是旧值,之后读到的是缓存中的新值,但当缓存失效后,从数据库中又会读到旧值。
并发情况下引发的一致性问题
1.先更新数据库,再更新缓存
假如有两个请求,请求A和请求B,同时更新同一条数据可能会出现这样的情况:
请求A先将数据库中数据更新为1,然后在更新缓存前,请求B将数据库数据更新为2,紧接着也把缓存更新为2,然后 请求A将缓存更新为1,此时数据库中数据为1,缓存中数据为2,出现了缓存和数据库中数据不一致情况。
解决方案
1.采用分布式锁:在更新缓存前先加个分布式锁,保证同一时间只运行一个请求更新缓存,就会不会产生并发问题了,当然引入了锁后,对于写入的性能就会带来影响。
2.设置过期时间:在更新完缓存时,给缓存加上较短的过期时间,这样即时出现缓存不一致的情况,缓存的数据也会很快过期,对业务还是能接受的。
2.先更新缓存,再更新数据库
假如有两个请求,请求A和请求B,同时更新同一条数据可能会出现这样的情况:
A 请求先将缓存的数据更新为 1,然后在更新数据库前,B 请求来了, 将缓存的数据更新为 2,紧接着把数据库更新为 2,然后 A 请求将数据库的数据更新为 1。
此时,数据库中的数据是 1,而缓存中的数据却是 2,出现了缓存和数据库中的数据不一致的现象。
先删除缓存再更新数据库还是先更新数据库再删除缓存?
所以,无论是「先更新数据库,再更新缓存」,还是「先更新缓存,再更新数据库」,这两个方案都存在并发问题,当两个请求并发更新同一条数据的时候,可能会出现缓存和数据库中的数据不一致的现象。
此时需要考虑另一种方案:删除缓存。在更新数据时,不更新缓存,而是删除缓存中的数据。然后,到读取数据时,发现缓存中没了数据之后,再从数据库中读取数据,更新到缓存中。即Cache Aside(旁路缓存)策略。
对于异常情况,但凡第二步操作失败,肯应会引起数据不一致,这里重点介绍并发问题带来的数据不一致情况。
并发情况下引发的一致性问题
1.先删除缓存,再更新数据库
有两个请求,请求A进行写操作,请求B进行读操作
(1)请求A进行写操作,删除redis缓存后,工作正在进行中,更新mysql......A还么有彻底更新完mysql,还没commit
(2)请求B开工查询,查询redis发现缓存不存在(被A从redis中删除了)
(3)请求B继续,去数据库查询得到了mysql中的旧值(A还没有更新完)
(4)请求B将旧值写回redis缓存
(5)请求A将新值写入mysql数据库
上述情况会出现数据不一致情况
解决方案
1.延迟双删:在线程 A 删除缓存、更新完数据库之后,先「休眠一会」,再「删除」一次缓存。
#删除缓存
redis.delKey(X)
#更新数据库
db.update(X)
#睡眠
Thread.sleep(N)
#再删除缓存
redis.delKey(X)
加了个睡眠时间,主要是为了确保请求 A 在睡眠的时候,请求 B 能够在这这一段时间完成「从数据库读取数据,再把缺失的缓存写入缓存」的操作,然后请求 A 睡眠完,再删除缓存。
所以,请求 A 的睡眠时间就需要大于请求 B 「从数据库读取数据 + 写入缓存」的时间。
2.先更新数据库,再删除缓存
继续用「读 + 写」请求的并发的场景来分析。
假如某个用户数据在缓存中不存在,请求 A 读取数据时从数据库中查询到年龄为 20,在未写入缓存中时另一个请求 B 更新数据。它更新数据库中的年龄为 21,并且清空缓存。这时请求 A 把从数据库中读到的年龄为 20 的数据写入到缓存中。
最终,该用户年龄在缓存中是 20(旧值),在数据库中是 21(新值),缓存和数据库数据不一致。
从上面的理论上分析,先更新数据库,再删除缓存也是会出现数据不一致性的问题,但是在实际中,这个问题出现的概率并不高。
因为缓存的写入通常要远远快于数据库的写入,所以在实际中很难出现请求 B 已经更新了数据库并且删除了缓存,请求 A 才更新完缓存的情况。
而一旦请求 A 早于请求 B 删除缓存之前更新了缓存,那么接下来的请求就会因为缓存不命中而从数据库中重新读取数据,所以不会出现这种不一致的情况。
如何保证两步都成功?
前面我们分析到,无论是更新缓存还是删除缓存,只要第二步发生失败,那么就会导致数据库和缓存不一致。
1.重试机制
可以引入消息队列,将第二个操作(删除缓存)要操作的数据加入到消息队列,由消费者来操作数据。
- 如果应用删除缓存失败,可以从消息队列中重新读取数据,然后再次删除缓存,这个就是重试机制。当然,如果重试超过的一定次数,还是没有成功,我们就需要向业务层发送报错信息了。
- 如果删除缓存成功,就要把数据从消息队列中移除,避免重复操作,否则就继续重试。
2.订阅数据库变更日志:
「先更新数据库,再删缓存」的策略的第一步是更新数据库,那么更新数据库成功,就会产生一条变更日志,记录在 binlog 里。
于是我们就可以通过订阅 binlog 日志,拿到具体要操作的数据,然后再执行缓存删除。
总结
文章参考:
2.小林coding:图解Redis介绍 | 小林coding
3. 尚硅谷Redis7实战:尚硅谷Redis零基础到进阶,最强redis7教程,阳哥亲自带练(附redis面试题)_哔哩哔哩_bilibili
4.javaguide:Java 面试指南 | JavaGuide(Java面试 + 学习指南)
5.水滴与银弹公众号:缓存和数据库一致性问题,看这篇就够了 (qq.com)