长大后想做什么?做回小孩!

0%

缓存的一致性、穿透、击穿、雪崩

缓存读写策略

Cache Aside Pattern(旁路缓存模式)

db 和 cache 都是由应用服务进行维护,并且是以 db 的结果为准。 适合读请求比较多的场景。

  • 先更新 db
  • 然后直接删除 cache 。

:

  • 从 cache 中读取数据,读取到就直接返回
  • cache 中读取不到的话,就从 db 中读取数据返回
  • 再把数据放到 cache 中。

Read/Write Through Pattern(读写穿透)

db 和 cache 都是由一个单独的cache服务进行维护。

写(Write Through):

  • 先查 cache,cache 中不存在,直接更新 db。
  • cache 中存在,则先更新 cache,然后 cache 服务自己更新 db(同步更新 cache 和 db)。

读(Read Through):

  • 从 cache 中读取数据,读取到就直接返回 。
  • 读取不到的话,先从 db 加载,写入到 cache 后返回响应。

Write Behind Pattern(异步缓存写入)

读写穿透的升级版,同样是用一个单独的cache服务维护 db 和 cache,但是写操作只更新 cache,不同时更新 db,而是改为异步批量的方式更新db。

缺点是一致性更加难以维护。

优点是 db 下的写性能很好,非常适合一些更新频繁,但是一致性和时效性要求没那么高的业务,例如浏览量、点击量等等。

缓存和数据库的一致性

这里 一致性 通常指 旁路缓存模式 策略下的 更新数据库成功,但是缓存更新失败并发操作 导致的不一致问题。

保证最终一致性

  • 延迟双删:先删除缓存,再更新数据库,之后经过延迟(时间一般是预估一次业务的完成时间)再删除缓存。并发情况下可能会导致一次不一致。

    写数据库之后删除缓存失败:

    • 异步发送需要删除的 key 到 MQ ,监听 MQ 重试删除(代码耦合度较高)。
    • 使用 canal 监听 binlog 日志,相当于一个 MySQL 数据库的从节点,数据库发生数据变动,会通知 canal 客户端(可以是一个 SpringBoot 应用)。

强一致性(牺牲性能)

  • 2PC、3PC、Raft等强一致性协议。
  • 分布式读/写锁,写请求的时候只有一个线程可以更新数据库和缓存,直到操作完成。

缓存穿透

大量请求的数据不在缓存中,大量查数据库(也不在数据库里)导致数据库压力过大。通常是黑客伪造不存在的 key 。

  • 缓存无效 key :如果缓存和数据库都查不到某个 key 的数据就写一个到缓存中去并设置过期时间。可以解决请求的 key 变化不频繁的情况,如果黑客恶意攻击,每次构建不同的请求 key,会导致缓存大量无效的 key 。

  • 布隆过滤器:由二进制向量(或者说位数组,Redis 中的 bit map)和一系列随机映射函数(哈希函数)两部分组成的数据结构。

    ​ 把所有可能存在的请求的值都存放在布隆过滤器中,当用户请求过来,先判断用户发来的请求的值是否存在于布隆过滤器中。不存在的话,直接返回请求参数错误信息给客户端,存在的话才会走查数据库的业务流程。

    ​ 缺点是返回的结果是概率性的,而不是非常准确的。理论情况下添加到集合中的元素越多,误报的可能性就越大。并且,存放在布隆过滤器的数据不容易删除。

  • 接口限流:根据用户或者 IP 对接口进行限流,对于异常频繁的访问行为,还可以采取黑名单机制,例如将异常 IP 列入黑名单。

缓存击穿

请求的 key 对应的是热点数据,该数据存在于数据库中,但不存在于缓存中(通常是因为缓存中的那份数据已经过期)。

  • 永不过期:设置热点数据永不过期或者过期时间比较长。不推荐,因为非常浪费内存,但是如果数据非常关键且几乎都是读操作时,可以考虑。
  • 热点 key 预热:针对热点数据提前预热,例如,使用定时任务来定时触发缓存预热的逻辑,将数据库中的热点数据查询出来并存入缓存中,并设置合理的过期时间比如秒杀场景下的数据在秒杀结束之前不过期。
  • 互斥锁:在缓存失效后,抢到互斥锁的线程才能执行查数据库流程。获得锁先二次检查缓存能否命中,未命中才去查询数据库并更新缓存,否则直接返回二次查缓存的结果。

缓存雪崩

缓存在同一时间大面积的失效,导致大量的请求都直接落到了数据库上,对数据库造成了巨大的压力。

  • 针对服务宕机导致的雪崩
    • Redis 集群:采用 Redis 集群,避免单机出现问题整个缓存服务都没办法使用。
    • 多级缓存:设置多级缓存,例如本地缓存+Redis 缓存的二级缓存组合,当 Redis 缓存出现问题时,还可以从本地缓存中获取到部分数据。
  • 针对大量缓存同时失效
    • 随机失效时间:为缓存设置随机的失效时间,例如在固定过期时间的基础上加上一个随机值,这样可以避免大量缓存同时到期,从而减少缓存雪崩的风险。
    • 提前预热:同缓存击穿。
    • 永不过期:同缓存击穿。