Skip to content

Redis 缓存

概念

缓存就是数据交换的缓冲区(称作 Cache[kae]),是存贮数据的临时地方,一般读写性能较高。

缓存作用与问题

作用

  • 降低后端负载
  • 提高读写效率,降低响应时间

成本

  • 数据一致性成本
  • 代码维护成本
  • 运维成本

缓存模型

- 添加缓存策略

- 缓存更新策略

内存淘汰超时剔除主动更新
说明不用自己维护,利用 Redis 的内存淘汰机制,当内存不足时自动淘汰部分数据。下次查询时更新缓存给缓存数据添加 TTL 时间,到期后自动删除缓存。下次查询时更新缓存。编写业务逻辑,在修改数据库的同时,更新缓存。
一致性一般
维护成本

业务场景:

  • 低一致性需求:使用内存淘汰机制。例如店铺类型的查询缓存
  • 高一致性需求:主动更新,并以超时剔除作为兜底方案。例如店铺详情查询的缓存

- 主动更新策略

  • 方案一: Cache Aside Pattern(使用的最多)

由缓存的调用者,在更新数据库的同时更新缓存

  • 方案二:Read/Write Through Pattern

缓存与数据库整合为一个服务,由服务来维护一致性。调用者调用该服务,无需关心缓存一致性问题。

  • 方案三:Write Behind Caching Pattern

调用者只操作缓存,由其它线程异步的将缓存数据持久化到数据库,保证最终一致。

操作缓存和数据库时有三个问题需要考虑:

1.删除缓存还是更新缓存?

  • 更新缓存:每次更新数据库都更新缓存,无效写操作较多
  • 删除缓存(推荐):更新数据库时让缓存失效,查询时再更新缓存

2.如何保证缓存与数据库的操作的同时成功或失败?

  • 单体系统,将缓存与数据库操作放在一个事务
  • 分布式系统,利用 TCC 等分布式事务方案

3.先操作缓存还是先操作数据库?

  • 先删除缓存,再操作数据库
  • 先操作数据库,再删除缓存(更安全)

出现缓存与数据库数据不一致的原因

缓存更新策略的最佳实践方案:

  1. 低一致性需求:使用 Redis 自带的内存淘汰机制
  2. 高一致性需求:主动更新,并以超时剔除作为兜底方案
  • 读操作:

    • 缓存命中则直接返回
    • 缓存未命中则查询数据库,并写入缓存,设定超时时间
  • 写操作

    • 先写数据库,然后再删除缓存
    • 要确保数据库与缓存操作的原子性

案例

给查询商铺的缓存添加超时剔除和主动更新的策略:

  • ① 根据 id 查询店铺时,如果缓存未命中,则查询数据库,将数据库结果写入缓存,并设置超时时间
  • ② 根据 id 修改店铺时,先修改数据库,再删除缓存

缓存穿透

缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。

例如:请求一个压根就不存在的 id,就会导致请求穿透 redis 直接到达数据库,若是来了一批这样的请求,且数据库数据较多,会导致数据库压力增大造成宕机

常见的解决方案:

  • 缓存空对象:查询数据库后,将不存在的数据缓存到 redis,并设置合理的过期时间

    • 优点:实现简单,维护方便
    • 缺点:
      • 额外的内存消耗
      • 可能造成短期的不一致
  • 布隆过滤:概率判断,不一定准确

    • 优点:内存占用较少,没有多余 key
    • 缺点:
      • 实现复杂
      • 存在误判可能

缓存雪崩

缓存雪崩是指在同一时段大量的缓存 ky 同时失效或者 Redis 服务宕机,导致大量请求到达数据库,带来巨大压力。

解决方案:

  • 给不同的 Key 的 TTL 添加随机值

例如:缓存预热时(提前给 redis 里面导入一批数据库数据),由于时同一时间并发操作的,可以给 redis 的不同 key 的过期时间后面加一个随机数,可以避免所有 key 的过期时间都为同一个值

解决方案:

  • 利用 Redis 集群提高服务的可用性

例如: 利用 redis 哨兵集群保证高可用

  • 给缓存业务添加降级限流策略

例如:当发现 redis 出现故障时,应该及时返回错误,不应该将请求继续往后传递给数据库

  • 给业务添加多级缓存

例如:在一条业务线中,在不同的位置设置不同的缓存,保证高可用

缓存击穿

缓存击穿问题也叫热点 Key 问题,就是一个被高并发访问并且缓存重建业务较复杂的 key 突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。

常见解决方案:

互斥锁:

当多个请求请求进来,查了 redis 没有这个 key 的数据,会去查询数据库来填充 reids,可利用互斥锁,保证只能有一个线程在查库和填充缓存

互斥锁的问题:会造成很多请求或线程于等待状态

逻辑过期:

存入 redis 的 key 不设置 redis 的过期时间,而是在 value 里面加一个逻辑到期的时间,当下一次查到这个数据,发现过期,才会重新查询并更新缓存,并且更新缓存是异步执行的,在更新缓存之前,会先返回旧数据,避免的长时间等待

txt
key:user:1 ==> value {"expire":152141223,"name":"Jack","age":21} ==> expire是逻辑过期时间

对比

解决方案优点缺点
互斥锁没有额外的内存消耗、保证一致性、实现简单需要等待,性能受影响、可能有死锁风险
逻辑过期线程无需等待,性能较好不保证一致性、有额外内存消耗、实现复杂

TIP

具体怎么选择,需要根据业务场景来做权衡