缓存问题速记

106 阅读4分钟

这是我参与11月更文挑战的第8天,活动详情查看:2021最后一次更文挑战

  • 缓存雪崩

    • 数据库每秒 1000的并发是⼀个正常的指标,一旦缓存宕机,过期时间一致导致缓存同时失效。请求打到数据库,数据库就崩了。重启数据库,⽴⻢⼜会被新的请求打死了。

    • 解决

      • 事前:redis⾼可⽤,主从+哨兵,redis cluster,避免全盘崩溃

      • 事中:本地ehcache缓存 + hystrix限流&降级,避免MySQL被打死

      • 事后:redis持久化RDB+AOF,快速恢复缓存数据

      • 缓存的失效时间设置为随机值,避免同时失效

  • 缓存穿透

    • 黑客的恶意行为,大量的请求一个缓存不存在的数据,都打在服务器上。数据库很快被打死。

    • 解决

      • 对请求参数进⾏校验,不合理直接返回

      • 查询不到的数据也放到缓存,value为空,如 set -999 ""(不推荐----如果黑客使用的是随机数请求,那么无法方式,还占了内存引发缓存清空策略导致有用的缓存被清空)

      • 使⽤布隆过滤器,快速判断key是否在数据库中存在,不存在直接返回(常用)

  • 缓存击穿

    • 热点数据,并发量很高,在失效的时候,重新拉数据库到缓存的期间,大量的请求导致数据库打死。

    • 缓存雪崩是指⼤量缓存失效,缓存击穿是指热点数据的缓存失效

    • 解决

      • 设置key永远不过期,或者快过期时,通过另⼀个异步线程重新设置key

      • 当从缓存拿到的数据为null,重新从数据库加载数据的过程上锁,下⾯写个分布式锁实现的demo

  • 缓存数据库双写一致性

    • 除了加锁让双写操作处于一个事务中可以满足强一致性,其他的所有方案其实都是弱一致性,但是要求最后能达到最终一致性。

    • 我们一般就是采用cache-aside模式来缓存操作的:即 先更新数据库,然后删除缓存。(简单并且出错概率小)。读:先读缓存,再读数据库。

      • 为什么不先删除缓存

        • 如果删除了缓存,还没写数据库,那么其他线程请求数据就会将历史数据再次刷新到缓存内。
      • 为什么不更新缓存

        • 所有的方案都是直接删缓存的!因为更新缓存在高并发的场景下很可能会出现脏读的情况。

        • 2个线程A和B。A先更新数据库,但是最后更新的缓存。此时缓存应该是B的值,但是因为网络原因会导致最终是A的。

        • 如图img

    • cache-aside依然存在的一些问题

      • 数据库做了读写分离

        • 写库操作完成了,删除了缓存,但是此时还没有同步到读库,被另外线程去读取了,就会导致读到脏数据

        • 解决:延迟再删除!设置延迟时间大于主从同步时间即可。(但是同步进行延迟删除,可能会浪费系统资源,所以有必要的话做异步处理:1. 可以将删除操作丢到队列,然后消费队列进行删除,如果删除失败就再次丢到队列。2. 也可以监听binlog来异步删除)

      • 数据库更新成功,缓存更新失败!

        • 通过重试机制,如果重试多次都失败,说明redis可能故障,此时应该将该key记录下来,并及时报警排查处理。
    • 如果一定要先删缓存再更新DB呢?(这指定是有毛病)

      • 直接这么做肯定是不行的,但是这个可以通过延迟双删解决!(注意延迟双删只是为了非要先删缓存场景设计的)

        • 延迟双删,就是先删除缓存,然后更新数据库后,等待一段时间后再删除缓存。

        • 如果嫌等待时间长会降低系统吞吐量,我们延迟操作异步去做(开一个异步线程,或者消息队列,或者binlog方式监听延迟删除等方式)。

    • 严格要求缓存和数据库双写一致性,只能加锁来串行化,写的时候不让其他的线程来读!