Redis缓存延迟指的是:Redis与数据库发生同步过程中Redis数据状态滞后的现象。缓存延迟涉及到数据一致性与缓存更新策略的选择。

Redis缓存失效指的是:因为缓存穿透、缓存击穿、缓存雪崩等原因导致Redis缓存查询不到的情况。

下面将分别讨论Redis缓存延迟中的数据一致性与缓存更新策略,以及缓存失效中的缓存穿透、缓存击穿、缓存雪崩问题。

一、缓存延迟

基于业务数据的时效性要求,Redis缓存延迟的容忍度也会不同,如,对于商品库存与商品详情两种数据来说,商品库存随时变化,而商品详情在新建之后一般不怎么发生变化。因此,商品库存数据的时效性要求就会更高,缓存延迟的时间应当越短越好,商品详情数据则反之。

业务数据的时效性要求,决定了会选择哪种缓存更新策略来保证数据一致性的强度。

(一)数据一致性

数据一致性(Consistency)是分布式理论CAP的一部分。

出现数据不一致,是因为多个组件在同步时出现延迟,这一过程可以发生在在Redis集群内部中的Redis节点之间,也可以发生在Redis集群与数据库之间。后文的Redis更新策略考虑的是后者,即,Redis集群作为一个整体与数据库之间的数据一致性。

下图演示了在数据库发生变动后,用户查询的数据出现不一致的过程:

@startuml Redis数据同步顺序图
title Redis数据同步顺序图

autonumber

actor User
participant Tomcat
participant Redis
participant MySQL
actor Admin

activate User
activate Admin


== 用户首次查询 ==
User ->> Tomcat ++: 数据查询请求
Tomcat ->> Redis ++: 数据查询指令
return 数据在缓存中不存在
Tomcat ->> MySQL ++: 数据查询命令
return 数据库中的数据
Tomcat ->> Redis ++: 写入Redis
return 写入结果
return 最终数据
note right of User
此时的最终数据是最新的
endnote

== 数据发生变化 ==
Admin ->> Tomcat ++: 数据更新请求
Tomcat ->> MySQL ++: 数据更新指令
return 更新结果
Tomcat ->> Redis ++: Redis同步
note right of Tomcat
根据不同的数据一致性要求,选择不同的同步策略
endnote
return 同步结果
return 数据更新结果


== 用户第二次查询 ==
User ->> Tomcat ++: 数据查询请求
Tomcat ->> Redis ++: 数据查询指令
return Redis中的数据
note right of Tomcat
此时,Redis中的数据可能存在**数据不一致**的情况,
因为数据库的数据发生了变化而Redis没有来得及更新
endnote
return 最终数据
note right of User
此时的最终数据可能是旧的
endnote

@enduml

由于在数据发生变化时,Redis同步这一步存在延迟,导致用户第二次查询的数据是数据库更新前的Redis中的旧数据。用户什么时候获取新的数据,取决于业务上数据时效性的要求,数据时效性要求越高,Redis的更新时机就应当越早。

(二)缓存更新策略

根据数据一致性的强度从低到高,Redis的更新策略有:内存淘汰、超时过期、主动更新。

  1. 内存淘汰

内存淘汰是指,由Redis根据自身内存使用状态来淘汰数据的过程。这种缓存更新机制不需要开发人员在代码层面做任何操作,开发成本较低,但数据一致性较差,一般适用于不经常变动的缓存数据。

可以在Redis交互界面设置maxmemory的值,来改变Redis最大内存:

redis set config maxmemory 2048m

也可以在redis.conf配置文件中修改对应值:

maxmemory 2048m
  1. 超时过期

超时过期是指,在创建缓存记录时为其指定一个有效期,达到有效期后Redis自动删除对应记录的过程。这种缓存更新机制只需要开发人员在新建缓存对象时指定其过期时间即可,开发成本较低,数据一致性一般,一般适用于偶尔变动的缓存数据。

在Redis控制台中,有三种方式设置过期时间:expiresetsetex,它们的效果都是一样的,同时,可以使用ttl查询剩余存活时间。如下所示:

set key value # 写入缓存
expire key 100 # 设置存活时间

set key value EX 100 # 写入缓存的同时设置过期时间
setex key 100 value # 同上

ttl key # 查看剩余存活时间
  1. 主动更新

主动更新是指,数据库发生变动时由代码主动调用Redis服务同步修改缓存数据的过程。这种缓存更新策略需要开发人员主动开发部分代码,开发成本适中,数据一致性较强,一般适用于经常变动的缓存数据,并将超时过期作为兜底方案一起使用。

更新缓存时,一般采取“先写数据库、再删缓存”的步骤。先写数据库的原因是:缓存的写速度比数据库要快很多,更耗时的操作先做可以确保在并发过程中减小数据冲突的概率。同时,一般使用del删除缓存,下次查询时会自动将数据库中将最新的记录重新写入Redis,比起重复setdel更快。

最后,数据库与Redis的更新要保证原子性,即,要么同时成功,要么同时失败。解决方式是将数据库和Redis的更新操作放到同一个事务中,分布式环境则需要考虑TCC分布式事务。

二、缓存失效

根据严重程度从低到高,缓存失效包括以下场景:缓存穿透、缓存击穿、缓存雪崩。

(一)缓存穿透

缓存穿透是指,用户查询了缓存和数据库中都不存在的数据,导致缓存永不生效,请求直接发往数据库的现象。

解决缓存穿透的常用方法是:缓存空对象与布隆过滤。

缓存空对象是指,在数据库未查到记录时,将一个空值存入缓存,这样下次再次查询这个数据时,将直接由Redis返回空,以减轻数据库的压力。

布隆过滤是指,查询Redis之前,优先在布隆过滤器中判断数据是否不存在,布隆过滤器根据一定的Hash规则认为数据不存在,则该数据就一定不存在,直接返回空即可。该方案实现简单,但会造成额外的内存消耗与短期的数据不一致。

一般缓存穿透发生的原因是恶意用户试图自行组装参数对系统发起攻击,因此,除了上述两种方式外,也可以在查询数据之前,做好用户权限及基础数据格式的校验,避免无效的请求继续往后走。该方案实现复杂,但内存占用较少。

(二)缓存击穿

缓存击穿是指,被高并发访问的热点数据过期导致大量请求直接发往数据库的现象。

如果不巧的是该热点数据缓存的重建过程又比较缓慢,那么问题会更严重,在Redis记录写入之前,若干进程将会同时执行重复的重建操作,这给数据库的压力将是巨大的。

解决缓存击穿的常用方法是:增加互斥锁、热点数据逻辑过期。

互斥锁是指,在重建缓存记录查询数据库操作只交给一个线程去做,其它线程等待,直到缓存中有记录为止。该方案实现简单但性能较差。

逻辑过期则是指,热点数据不再设置Redis的自带过期时间,而是在数据中增加一个自定义的过期时间,过期删除操作交给代码去执行。该方案实现复杂但性能较好。

(三)缓存雪崩

缓存雪崩是指,Redis出现大批量不可用的现象。

出现大批量不可用的原因有两点,一是Redis服务宕机,二是大量缓存记录同时超时过期。针对服务宕机,可以采取Redis集群来提高Redis的可用性。针对大量Redis同时超时过期,可以在设置超时时间时偏移一个随机值,避免同时过期。

同时,为了避免大量请求直接发往数据库,可以为缓存业务增加降级限流策略。在分布式系统中,多级缓存也可以减轻缓存雪崩带来的影响。

参考文档

  1. Redis官方网站
  2. 黑马程序员Redis入门到实战教程