缓存更新策略

- 对于低一致性需求:使用内存淘汰机制。例如店铺类型的查询缓存。
- 对于高一致性需求:主动更新 ,并以超时剔除作为兜底方案。例如店铺详情查询的缓存。
主动更新策略(缓存读写策略)
1. Cache Aside Pattern(旁路缓存模式)------ 一致性良好、难度一般
-
由缓存的调用者,在更新数据库的同时更新缓存。
-
写:先更新 db,然后直接删除 cache 。
Q:为什么是删除cache?
A:如果每次更新数据库都更新缓存,但是又不读,则会导致无效写操作较多,所以应该查询时再更新。
Q:为什么不先删除 cache ,后更新 db ?A:会导致数据不一致:请求 1 先把 cache 中的 A 数据删除;请求 2 从 cache 中读不到就从 db 中读取数据->请求 1 再把 db 中的 A 数据更新。
Q:那这样就不会出现问题了?A: 理论上还是可能会出现数据不一致性的问题,不过概率非常小,因为缓存的写入速度是比数据库的写入速度快很多。如:缓存失效时请求 1 从 db 读数据 A;请求 2 更新 db 中的数据 A,删除缓存(此时为空);请求 1 将数据 A 写入 cache。
-
读:从 cache 中读取数据,读取到就直接返回;cache 中读不到就从 db 中读取数据返回;再把数据放到 cache 中。
2. Read/Write Through Pattern(读写穿透)------ 一致性优秀、难度复杂、性能一般
- 缓存与数据库集成为一个服务,服务保证两者的一致性,对外暴露API接口。
- Read/Write Through Pattern 中服务端把 cache 视为主要数据存储,从中读取数据并将数据写入其中。cache 服务负责将此数据读取和写入 db,从而减轻了应用程序的职责。
- 写:cache 中存在,则先更新 cache ,然后 cache 服务自己更新 db;cache 中不存在,直接更新 db。
- 读:从 cache 中读取数据,读取到就直接返回;cache 中读不到就从 db 中读取数据返回;再把数据放到 cache 中。
- Read-Through Pattern 实际只是在 Cache-Aside Pattern 之上进行了封装。在 Cache-Aside Pattern 下,发生读请求的时候,如果 cache 中不存在对应的数据,是由客户端自己负责把数据写入 cache,而 Read Through Pattern 则是 cache 服务自己来写入缓存的,这对客户端是透明的。
3. Write Behind Pattern(异步缓存写入)------ 一致性差、难度复杂、性能高
- Write Behind Pattern 和 Read/Write Through Pattern 很相似,两者都是由 cache 服务来负责 cache 和 db 的读写。
- 但是,两个又有很大的不同:Read/Write Through 是同步更新 cache 和 db,而 Write Behind 则是只更新缓存,不直接更新 db,而是改为异步批量的方式来更新 db。
Redis 生产问题
1. 缓存穿透
- 缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。对数据库造成了巨大的压力,可能直接就被这么多请求弄宕机了。常见的解决方案有两种:
- 1)缓存无效 key
- 如果缓存和数据库中都查不到某个 key 的数据,就把他写到 Redis 中去并设置过期时间。这种方案并不能从根本上解决此问题,如果非要用这种方式来解决穿透问题的话,尽量将无效的 key 的过期时间设置短一点,比如 1 分钟。
- 优点:实现简单,维护方便。
- 缺点:额外的内存消耗、可能造成短期的不一致。
- 2)布隆过滤
- 布隆过滤器:可以看作由二进制向量(或者说位数组)和一系列随机映射函数(哈希函数)两部分组成的数据结构。相比于我们平时常用的List、Map、Set等数据结构,它占用空间更少并且效率更高,但是缺点是其返回的结果是概率性的,而不是非常准确的。理论情况下添加到集合中的元素越多,误报的可能性就越大。并且,存放在布隆过滤器的数据不容易删除。
- 把所有可能存在的请求的值都存放在布隆过滤器中,当用户请求过来,先判断用户发来的请求的值是否存在于布隆过滤器中。不存在的话,直接返回请求参数错误信息给客户端,存在的话才会走下面的流程。
- 优点:内存占用较少,没有多余key。
- 缺点:实现复杂、存在误判可能。
- 布隆过滤器:可以看作由二进制向量(或者说位数组)和一系列随机映射函数(哈希函数)两部分组成的数据结构。相比于我们平时常用的List、Map、Set等数据结构,它占用空间更少并且效率更高,但是缺点是其返回的结果是概率性的,而不是非常准确的。理论情况下添加到集合中的元素越多,误报的可能性就越大。并且,存放在布隆过滤器的数据不容易删除。
2. 缓存雪崩
- 缓存雪崩是指在同一时段大量的缓存key同时失效 或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。可采用以下解决方案:
- 针对 Redis 服务不可用的情况:
- Redis 集群:采用 Redis 集群,避免单机出现问题整个缓存服务都没办法使用。Redis Cluster 和 Redis Sentinel 是两种最常用的 Redis 集群实现方案。
- 多级缓存:设置多级缓存,例如本地缓存+Redis 缓存的二级缓存组合,当 Redis 缓存出现问题时,还可以从本地缓存中获取到部分数据。
- 针对大量缓存同时失效的情况:
- 设置随机失效时间(可选):为缓存设置随机的失效时间,例如在固定过期时间的基础上加上一个随机值,这样可以避免大量缓存同时到期,从而减少缓存雪崩的风险。
- 提前预热(推荐):针对热点数据提前预热,将其存入缓存中并设置合理的过期时间,比如秒杀场景下的数据在秒杀结束之前不过期。
- 持久缓存策略(看情况):虽然一般不推荐设置缓存永不过期,但对于某些关键性和变化不频繁的数据,可以考虑这种策略。
3. 缓存击穿
- 缓存击穿也叫热点Key问题,就是一个被高并发访问 并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。可采用以下解决方案:
- 1)互斥锁
- 在缓存失效后,通过设置互斥锁确保只有一个线程查询数据库并重建缓存,其他线程等待或重试。
- 优点:实现简单、保证一致性、没有额外的内存消耗。
- 缺点:性能受影响、可能有死锁风险。
- 2)逻辑过期
- 不依赖缓存TTL,将过期时间写入缓存值 中。当数据过期时,由发现过期的线程异步更新缓存,其他线程继续返回旧数据。
- 优点:性能较好、无阻塞。
- 缺点:不能保证一致性、实现复杂、有额外内存消耗。
- 3)提前预热
- 针对热点数据提前预热,将其存入缓存中并设置合理的过期时间比如秒杀场景下的数据在秒杀结束之前不过期。
- 4)永不过期
- 设置热点数据永不过期或者过期时间比较长。