为什么需要缓存?

原因:

用缓存,主要有两个用途:高性能、高并发

高性能

非实时变化的数据-查询mysql耗时需要300ms,存到缓存redis,每次查询仅仅1ms,性能瞬间提升百倍。

高并发

mysql 单机支撑到2K QPS就容易报警了,如果系统中高峰时期1s请求1万,仅单机mysql是支撑不了的,但是使用缓存的话,单机支撑的并发量轻松1s几万~十几万。

原因是缓存位于内存,内存对高并发的良好支持。

常见的缓存问题:

1、缓存与数据库双写不一致
2、缓存雪崩、缓存穿透
3、缓存并发竞争

1、如何保证缓存与数据库的双写一致性?

最经典的缓存+数据库读写的模式,就是 Cache Aside Pattern。

  • 读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应。
  • 更新的时候,先更新数据库,然后再删除缓存。

为什么是删除缓存,而不是更新缓存?-> 用到缓存才去算缓存-lazy加载思想。

非高并发场景数据不一致问题:

先修改数据库,再删除缓存。如果缓存删除失败,导致缓存中是旧数据。

解决方法:

先删除缓存,再修改数据库。如果缓存删除失败,则整个操作失败,如果修改数据库失败,缓存已为空,则请求数据时,会重新加载数据库的数据,
虽然都是旧数据,但保持了数据一致性。

高并发场景数据不一致问题:

先删除了缓存,然后要去修改数据库,此时还没修改。(定义为步骤A

一个请求过来,去读缓存,发现缓存空了,去查询数据库(定义为步骤B1)。查到了修改前的旧数据,放到了缓存中。(定义为步骤B2

随后数据变更的程序完成了数据库的修改。此时数据库和缓存数据不一致了。

解决方法:

定义一个FIFO的阻塞队列,例如:LinkedBlockingQueue,将步骤A和步骤B放入同一个队列中。步骤A必然在步骤B的前面。

当场景发生了上述步骤B1时,只有2个情况:缓存已删除,数据库已修改或者数据库还未修改。不考虑已修改的正常情况,则步骤A必然已发生。
则可以在,步骤A步骤B1发生时均按照数据唯一标识(ID)同一个队列步骤A先于步骤B1入队,按照FIFO的方式,步骤A会先完成,则问题
理论上得以解决。

注意事项:

  • 高并发场景下肯定会有同一个数据多个步骤B的出现,可以过滤去重。即:队列中已存在则不用再入队了。
  • 由于步骤B已变成异步读请求,基于我们的高并发场景,需要考虑读超时的问题。如果请求还在等待时间范围内,不断轮询发现可以取到值了,那么就直接返回;如果请求等待的时间超过一定时长,那么这一次直接从数据库中读取当前的旧值。
  • 如果大量的数据更新频繁,导致队列中堆积大量的更新操作,然后大量的读请求超时,最后导致大量的请求直接走数据库。则需要根据具体业务模拟测试峰值,部署多个应用分摊更新操作。

2、缓存雪崩、缓存穿透-> 请移步这篇文章 缓存雪崩、缓存穿透

3、Redis的并发竞争问题

场景:

  • redis的并发竞争问题,主要是发生在并发写竞争
  • redis本身是单线程,不存在并发问题,但我们在使用过程中会存在并发问题:更新操作分成了3步骤,读取数据,数据操作,设新值回去。

例如:redis有一个key=“product_num”,value=10, 此时有2个客户端同时对这个key做加1操作,预期结果是value=12。
但有这样的情况:第一个客户端还未设新值回去的时候第2个客户端获取到值,为10,则2个客户端最终操作结果value=11,与预期不符!

解决方案:

  • 利用redis自带的 incr 命令。
  • CAS乐观锁

某个时刻,多个系统实例都去更新某个 key。可以基于 zookeeper 实现分布式锁。每个系统通过 zookeeper 获取分布式锁,确保同一时间,只能有一个系统实例在操作某个 key,别人都不允许读和写。

zookeeper-distributed-lock

:写mysql数据库时保存一个时间戳(或者version),从 mysql 查询的时候,时间戳也带出来。

每次要写DB前,先判断一下当前这个 value 的时间戳是否比缓存里的 value 的时间戳要新。如果是的话,那么可以写,否则,就不能用旧的数据覆盖新的数据。

原文地址:https://www.cnblogs.com/charm-j/p/10375413.html