缓存(Cache)
缓存(Cache)
缓存就是将数据存入内存中,利用内存的读取速度
为什么需要缓存
由于数据库一般将数据存储于硬盘,硬盘的读取速度远小于内存的读取速度,所以能够提高访问速度,缓存支持的并发量也要更大,有了缓存之后,数据库的压力也会随之变小。
缓存实现方式
实现方式可以分为本地缓存和分布式缓存
本地缓存
什么是本地缓存
直接存储于本地内存,不用进行网络传输
实现方式有哪些
使用Java本身的ConcurrentHashMap作为本地缓存,缺点是只提供了缓存的功能,并没有提供其他诸如
过期时间之类的功能。一个稍微完善一点的缓存框架至少要提供:过期时间、淘汰机制、命中率统计这三点。
利用第三方缓存框架:Ehcache 、 Guava Cache 、 Spring Cache、Caffeine本地缓存的好处
提高数据的读取效率(内存>硬盘),比起分布式缓存(远程)少了网络传输带来的时间开销,还有低依赖、轻量、简单、成本低
本地缓存的痛点和解决方案
痛点:
本地缓存应用耦合,对分布式架构支持不友好,比如同一个相同的服务部署在多台机器上的时候, 各个服务之间的缓存是无法共享的,因为本地缓存只在当前机器上有。
本地缓存容量受服务部署所在的机器限制明显。 如果当前系统服务所耗费的内存多,那么本地缓存 可用的容量就很少。
解决方案:
引入分布式缓存
分布式缓存
什么是分布式缓存
我们可以把分布式缓存(Distributed Cache) 看作是一种内存数据库的服务,它的最终作用就是提供缓存数据的服务。
分布式缓存脱离于应用独立存在,多个应用可直接的共同使用同一个分布式缓存服务。为什么要有分布式缓存
为了解决以下单独使用本地缓存的痛点
本地缓存应用耦合,对分布式架构支持不友好
本地缓存容量受服务部署所在的机器限制明显。分布式缓存有哪些
Memcached
引入分布式缓存的导致的问题
系统复杂性增加 :引入缓存之后,你要维护缓存和数据库的数据一致性、维护热点缓存、保证缓存
服务的高可用等等。
系统开发成本往往会增加 :引入缓存意味着系统需要一个单独的缓存服务,这是需要花费相应的成
本的,并且这个成本还是很贵的,毕竟耗费的是宝贵的内存。
缓存更新策略
缓存旁路模式
Cache Aside Pattern 是我们平时使用比较多的一个缓存读写模式,比较适合读请求比较多的场景。
写
先更新 db
直接删除cache读
- 从 cache 中读取数据,读取到就直接返回;
- cache 中读取不到的话,就从 db 中读取数据返回;
- 再把 db 中读取到的数据放到 cache 中
为什么先为什么删除 cache,而不是更新 cache?
主要原因有两点:
- 对服务端资源造成浪费 :删除 cache 更加直接,这是因为 cache 中存放的一些数据需要服务端经
过大量的计算才能得出,会消耗服务端的资源,是一笔不晓得开销。如果频繁修改 db,就能会导
致需要频繁更新 cache,而 cache 中的数据可能都没有被访问到。 - 产生数据不一致问题 :并发场景下,更新 cache 产生数据不一致性问题的概率会更大(后文会解
释原因)。
- 对服务端资源造成浪费 :删除 cache 更加直接,这是因为 cache 中存放的一些数据需要服务端经
在写数据的过程中,可以先删除 cache ,后更新 db 么
那肯定是不行的!因为这样可能会造成 数据库(db)和缓存(Cache)数据不一致的问题。
举例:请求 1 先写数据 A,请求 2 随后读数据 A 的话,就很有可能产生数据不一致性的问题。这个过程
可以简单描述为:- 请求 1 先把 cache 中的 A 数据删除;
- 请求 2 从 db 中读取数据;3. 请求 1 再把 db 中的 A 数据更新。
这就会导致请求 2 读取到的是旧值。
在写数据的过程中,先更新 db,后删除 cache 就没有问题了么?”
理论上来说还是可能会出现数据不一致性的问题,不过概率非常小,因为缓存的写入速度是比数
据库的写入速度快很多。
举例:请求 1 先读数据 A,请求 2 随后写数据 A,并且数据 A 在请求 1 请求之前不在缓存中的话,也有
可能产生数据不一致性的问题。这个过程可以简单描述为:- 请求 1 从 db 读数据 A;
- 请求 2 更新 db 中的数据 A(此时缓存中无数据 A ,故不用执行删除缓存操作 );
- 请求 1 将数据 A 写入 cache。
这就会导致 cache 中存放的其实是旧值。
·缺陷
缺陷 1:首次请求数据一定不在 cache 的问题
解决办法:可以将热点数据可以提前放入 cache 中。
缺陷 2:写操作比较频繁的话导致 cache 中的数据会被频繁被删除,这样会影响缓存命中率 。
解决办法:
数据库和缓存数据强一致场景 :更新 db 的时候同样更新 cache,不过我们需要加一个锁/分布式
锁来保证更新 cache 的时候不存在线程安全问题。
可以短暂地允许数据库和缓存数据不一致的场景 :更新 db 的时候同样更新 cache,但是给缓存加
一个比较短的过期时间,这样的话就可以保证即使数据不一致的话影响也比较小。
读写穿透
Read/Write Through Pattern 中服务端把 cache 视为主要数据存储,从中读取数据并将数据写入其中。
cache 服务负责将此数据读取和写入 db,从而减轻了应用程序的职责
写
先查 cache,cache 中不存在,直接更新 db。
cache 中存在,则先更新 cache,然后 cache 服务自己更新 db(同步更新 cache 和 db)。读
从 cache 中读取数据,读取到就直接返回 。
读取不到的话,先从 db 加载,写入到 cache 后返回响应。与缓存旁路模式的区别
Read-Through Pattern 实际只是在 Cache-Aside Pattern 之上进行了封装。在 Cache-Aside Pattern 下,发生读请求的时候,如果 cache 中不存在对应的数据,是由客户端自己负责把数据写入 cache,而 Read Through Pattern 则是 cache 服务自己来写入缓存的,这对客户端是透明的。 和 Cache Aside Pattern 一样, Read-Through Pattern 也有首次请求数据一定不再 cache 的问题,对 于热点数据可以提前放入缓存中。
异步缓存写入
写
Read/Write Through 是同步更新 cache 和 db,而 Write Behind 则是
只更新缓存,不直接更新 db,而是改为异步批量的方式来更新 db。读
从 cache 中读取数据,读取到就直接返回 。
读取不到的话,先从 db 加载,写入到 cache 后返回响应。适用场景
这种方式对数据一致性带来了更大的挑战,比如 cache 数据可能还没异步更新 db 的话,
cache 服务可能就就挂掉了。
这种策略在我们平时开发过程中也非常非常少见,但是不代表它的应用场景少,比如消息队列中消息的
异步写入磁盘、MySQL 的 Innodb Buffer Pool 机制都用到了这种策略。
Write Behind Pattern 下 db 的写性能非常高,非常适合一些数据经常变化又对数据一致性要求没那么 高的场景,比如浏览量、点赞量。
生产问题
如何保证缓存和数据库数据的一致性
缓存穿透
什么是缓存穿透
缓存穿透说简单点就是大量请求的 key 是不合理的,根本不存在于缓存中,也不存在于数据库中 。这就导致这些请求直接到了数据库上,根本没有经过缓存这一层,对数据库造成了巨大的压力,可能直接就被这么多请求弄宕机了。
解决方式
1)缓存无效 key
2)布隆过滤器
3)接口限流
缓存击穿
什么是缓存击穿
缓存击穿中,请求的 key 对应的是 热点数据 ,该数据 存在于数据库中,但不存在于缓存中(通常是因为缓存中的那份数据已经过期) 。这就可能会导致瞬时大量的请求直接打到了数据库上,对数据库造成了巨大的压力,可能直接就被这么多请求弄宕机了。
解决方式
永不过期(不推荐):设置热点数据永不过期或者过期时间比较长。
提前预热(推荐):针对热点数据提前预热,将其存入缓存中并设置合理的过期时间比如秒杀场景下的数据在秒杀结束之前不过期。
加锁(看情况):在缓存失效后,通过设置互斥锁确保只有一个请求去查询数据库并更新缓存。
缓存雪崩
什么是缓存雪崩
:缓存在同一时间大面积的失效,导致大量的请求都直接落到了数据库上,对数据库造成了巨大的压力。 这就好比雪崩一样,摧枯拉朽之势,数据库的压力可想而知,可能直接就被这么多请求弄宕机了。
解决方式
针对 Redis 服务不可用的情况:
Redis 集群:采用 Redis 集群,避免单机出现问题整个缓存服务都没办法使用。Redis Cluster 和 Redis Sentinel 是两种最常用的 Redis 集群实现方案,
多级缓存:设置多级缓存,例如本地缓存+Redis 缓存的二级缓存组合,当 Redis 缓存出现问题时,还可以从本地缓存中获取到部分数据。
针对大量缓存同时失效的情况:
设置随机失效时间(可选):为缓存设置随机的失效时间,例如在固定过期时间的基础上加上一个随机值,这样可以避免大量缓存同时到期,从而减少缓存雪崩的风险。
提前预热(推荐):针对热点数据提前预热,将其存入缓存中并设置合理的过期时间比如秒杀场景下的数据在秒杀结束之前不过期。
持久缓存策略(看情况):虽然一般不推荐设置缓存永不过期,但对于某些关键性和变化不频繁的数据,可以考虑这种策略