常见的缓存和db同步方式

  • 缓存的入库由读操作控制,也就是读操作先判断缓存有则读,缓存没有再读数据库,再将数据库查询结果刷新到缓存。写操作先更新数据库,再删除缓存。这种模式也叫cache aside,也是日常我们最常用的方式,
    • 但是因为读操作会主动查数据库,所以就会存在缓存穿透以及缓存失效后的一系列问题(见下面的缓存失效三种场景)。
  • 缓存的入库由写操作控制,读操作只查询缓存,不管有没有都直接返回,而缓存数据完全由写操作控制,即先更新数据库,再同步更新缓存。

    • 优点显然就是读操作流量绝对不会到数据库,直接避免了很多上述的问题。
    • 缺点也很明显,所有数据都要导入缓存,包括存量数据也需要在初始化时预热进去。
    • 如果是冷数据,本方式其实会浪费很多空间,而cache aside本质上是一种延迟入库的方式,不会有资源浪费。
    • 所以这个方式比较适合读多写少的情况,尤其适合少量的、静态的全热点数据查询系统。

      分析缓存和db的数据一致性问题

  • 上述两种方式,都存在一步【先更新数据库,再更新缓存】的操作,一般有如下几种方式:

    • 手动同步:一般就是指在代码中显式调用这两步,mysql.update,redis.del。
    • 手动异步:第二步通过异步来处理提高写操作性能,异步线程池、mq等等。
    • 自动异步:基于mysql的binlog数据流+消息队列,好处是业务代码完全不用操作缓存组件。
  • 但不管是哪一种方式,原子性的,所以存在后者失败的情况,如果失败那么缓存一直存储的还是旧数据,那么读操作也就一直是旧数据。一般有两种方式解决:
    • 设置过期时间,也就是依赖失效后读操作去刷新数据,很显然在过期时间内依然不一致,那这就适用于数据短暂延迟不会带来很大业务影响的场景。
    • 先删除缓存,再更新数据库,这样如果第二步更新数据库失败了,后续的读操作会继续从数据库读取到正确的数据。但存在的问题就是,如果第一步之后,到第二步之前,又有读操作将旧值刷入缓存,就还是会导致不一致。
    • 先删缓存,再更新数据库,再删除缓存,也就是所谓的【延迟双删】。这个方式既解决了【先db后cache】的缓存失败问题(因为有第一次cache删除),又解决了【先cache后db】的中间刷入问题(因为有第二次cache删除),当然你可能会问如果上述两种问题同时出现怎么办?这其实就是概览问题了,因为首先我们要认识到这两个问题的出现概览都是极低的,现在有了极低x极低的方案,那这种方案就可以认为不会出问题了。

      常见的三种缓存失效场景及其处理方式

  1. 穿透:cache aside模式下,对于无效key会漏到数据库,一旦使用这种无效key进行攻击就会导致数据库负载增加,这里有两种攻击方式:
    1. 单个无效key的大量请求,这种可以通过在读操作如果查不到时,缓存设置NULL值进行处理。
    2. 大量无效key的请求,因为上述方式依然需要在首次去查询数据库,虽然后续会被NULL拦截,但如果发起大量这种无效key的请求,依然会流量会打到数据库,可以用布隆过滤器进行处理,也就是将数据库所有数据key初始化到redis的布隆过滤器中,查询数据前先到布隆过滤器判断是否存在,如果不存在就不用查了,或许你会问,我都把所有数据导入布隆过滤器了,那我为什么不直接把所有数据导入缓存,后续直接判断缓存不就得了(也就是上述第二种方式),这里就可以去思考一下布隆过滤器的意义了(低空间成本)。
  2. 击穿:热点key失效,系统中其实很多key对应的数据查询全靠缓存撑着,一旦该key失效(这里还先不考虑redis宕机的问题,只是正常的key过期),流量就会全部漏到数据库,导致数据库宕机。
    1. 我们要分析在服务器没挂掉的情况下,redis的key失效有哪几种原因
      1. 要么key被主动删除:如果该key有更新,不要通过删除的方式去触发刷新,而应该在写操作时进行直接进行更新。
      2. 要么key过期被删除:简单,不要设置过期时间,或设置不可达过期时间。
      3. 要么redis因为内存不够发起数据淘汰,这里要分哪几种淘汰策略
        1. 因为是热点key,所以lru模式(allkeys-lru,volatile-lru)的几种方式肯定轮不到这个key。
        2. 不设置过期时间,那么volatile模式也不会volatile-random、volatile-ttl。
        3. 所以就剩下noeviction和allkeys-random,前者不淘汰肯定没问题,后者随机淘汰就有可能。
        4. 综上所述,只要淘汰策略不设置成allkeys-random即可,在配置文件redis.conf 中,通过设置 maxmemory-policy。
    2. 热点key本身优化方案:
      1. 二级缓存:通过jvm级别的缓存,如ehcache、guava的Cache工具等等。
      2. 如何检测热点key,通过统计和监控查询频率判断即可,具体实现的话可能要通过类似于滑动窗口的方式来统计,业内也有一些开源组件可以复用,如有赞的TMC阿里的Tair
  3. 雪崩:大量key失效,一般有两种情况会导致:
    1. 某个系统启动或页面打开时,同一时间加载了大量数据到缓存,并且都是设置的差不多相同过期时间。那么到了该时间点,就会出现大量key失效,根据热点key同理,导致数据库负载增高。
    2. 缓存节点直接宕机了(不管是部分还是全量),那么在缓存恢复之前就所有的请求就会到mysql。
    3. 针对第一条的解决方案就是,设置过期值时加个随机值(建议做在底层框架上)。针对第二条的解决方案就是没办法要么出事前保证缓存服务器的高可用,要么出事后尽快恢复缓存服务器!!!
    4. 那这里还有个问题,如果缓存起来之后,如何一致性,好像也没啥办法,要么全量清空,要么人工捞取(从日志数据,流数据等)宕机阶段的数据变更再更新缓存。