Redis(2) 缓存
1. 缓存更新
缓存更新策略
缓存更新是Redis为了节约内存而设计出来的,当我们向Redis插入太多数据,会导致内存占用过多,所以Redis会对部分数据进行更新,或者将其淘汰
内存淘汰 | 超时剔除 | 主动更新 | |
---|---|---|---|
说明 | 不用自己维护,利用Redis的内存淘汰机制,当内存不足时自动淘汰部分数据。下次查询时更新缓存。 | 给缓存数据添加TTL时间, 到期后自动删除缓存。下次查询时更新缓存。 | 编写业务逻辑,在修改数据库的同时更新缓存。 |
一致性 | 差 | 一般 | 好 |
维护成本 | 无 | 低 | 高 |
业务场景
- 低一致性需求:使用内存淘汰机制,例如店铺类型的查询缓存(因为这个很长一段时间都不需要更新)
- 高一致性需求:主动更新,并以超时剔除作为兜底方案,例如店铺详情查询的缓存
主动更新策略
- Cache Aside Pattern (人工编码方式):缓存调用者在更新数据库的同时更新缓存,也称之为双写方案
- Read/Write Through Pattern:缓存与数据库整合为一个服务,由服务来维护一致性。调用者调用该服务,无需关心缓存一致性问题。
- 缺点:但是维护这样一个服务很复杂,市面上也不容易找到这样的一个现成的服务,开发成本高
- Write Behind Caching Pattern (写回法):调用者只操作缓存,由其他线程异步地将缓存数据持久化到数据库,保证最终一致。
- 优点:即使多次更新缓存,最终只需要更新一次到数据库
- 缺点:维护异步的任务比较复杂,需要实时监控缓存中的数据更新,其他线程去异步更新数据库也可能不太及时,而且缓存服务器如果宕机,那么缓存的数据也就丢失了
综上所述,在企业的实际应用中,方案一更加可靠
Cache Aside Pattern
问题1:删除缓存还是更新缓存(删除)
- 更新缓存:每次更新数据库都需要更新缓存,无效写操作较多
- 删除缓存:更新数据库时让缓存失效,再次查询时更新缓存
问题2:如何保证缓存与数据库操作的同时成功或失败
- 单体系统:将缓存与数据库操作放在同一个事务
- 分布式系统:利用TCC等分布式事务方案
问题3:先操作缓存还是先操作数据库(数据库)
先删除缓存,再操作数据库
- 删除缓存的操作很快,但是更新数据库的操作相对较慢,如果此时有一个线程2刚好进来查询缓存,由于我们刚刚才删除缓存,所以线程2需要查询数据库,并写入缓存,但是我们更新数据库的操作还未完成,所以线程2查询到的数据是脏数据,出现线程安全问题
先操作数据库,再删除缓存
- 假设线程1在查询缓存的时候,缓存TTL刚好失效,需要查询数据库并写入缓存,这个操作耗时相对较短,但是就在这么短的时间内,线程2进来了,更新数据库,删除缓存,但是线程1虽然查询完了数据(更新前的旧数据),但是还没来得及写入缓存,所以线程2的更新数据库与删除缓存,并没有影响到线程1的查询旧数据,写入缓存,造成线程安全问题
分析可得,先操作数据库,再删除缓存,出现线程安全问题的概率更低
2. 缓存穿透
缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远都不会生效,会频繁的去访问数据库。
常见的解决方案:
缓存空对象
- 实现简单、维护方便
- 额外的内存消耗、可能造成短期的不一致
布隆过滤器
布隆过滤器其实采用的是哈希思想来解决这个问题,它实际上是一个初始值都为 0 的位图数组和一系列随机映射函数。布隆过滤器可以用于检索一个元素一定不存在或者可能存在一个集合中。
布隆过滤器会通过 3 个操作完成标记:
- 第一步,使用 N 个哈希函数分别对数据做哈希计算,得到 N 个哈希值;
- 第二步,将第一步得到的 N 个哈希值对位图数组的长度取模,得到每个哈希值在位图数组的对应位置。
- 第三步,将每个哈希值在位图数组的对应位置的值设置为 1;
布隆过滤器由于是基于哈希函数实现查找的,高效查找的同时存在哈希冲突的可能性,比如数据 x 和数据 y 可能都落在第 1、4、6 位置,而事实上,可能数据库中并不存在数据 y,存在误判的情况。
优点:内存占用较少,没有多余key
缺点:实现复杂、存在误判可能
以上两种都属于被动的解决方案
可以通过增加id的复杂度,避免被猜测id规律,做好数据的基础格式校验
加强用户权限校验
做好热点参数的限流
3. 缓存雪崩
- 缓存雪崩是指在同一时间段,大量缓存的key同时失效,或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力
- 解决方案
- 给不同的Key的TTL添加随机值,让其在不同时间段分批失效
- 利用Redis集群提高服务的可用性
- 给缓存业务添加降级限流策略
- 给业务添加多级缓存
4. 缓存击穿
- 缓存击穿也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,那么无数请求访问就会在瞬间给数据库带来巨大的冲击
- 举个不太恰当的例子:一件秒杀中的商品的key突然失效了,大家都在疯狂抢购,那么这个瞬间就会有无数的请求访问去直接抵达数据库,从而造成缓存击穿
- 常见的解决方案有两种
- 互斥锁:性能较差
- 逻辑过期
互斥锁
逻辑过期
解决方案 | 优点 | 缺点 |
---|---|---|
互斥锁 | 没有额外的内存消耗 保证一致性 实现简单 |
线程需要等待,性能受影响 可能有死锁风险 |
逻辑过期 | 线程无需等待,性能较好 | 不保证一致性 有额外内存消耗 实现复杂 |