《reids核心技术与实战》学习笔记

1. 数据不一致的发生情况

数据一致性的两种情况:

① 缓存中有数据,那么,缓存的数据值需要和数据库中的值相同。

② 缓存中本身没有数据,那么,数据库中的值必须是最新值。

不符合上述两种情况的,就属于缓存和数据库的数据不一致问题

两种缓存读写策略(发生增删改时):

① 同步直写策略:写缓存时,也同步写数据库,即同时更新缓存和数据库,缓存和数据库中的数据保持一致,这种情况一般要使用事务机制,来保证原子操作。

② 异步写回策略:写缓存时不同步写数据库,等到数据从缓存中淘汰时,再写回数据库。使用这种策略时,如果数据还没有写回数据库,缓存就发生了故障,那么此时数据库就没有最新的数据了,这种策略一般用于数据一致性要求不高的场景。

新增数据时:

如果是新增数据,数据会直接写到数据库中,不用对缓存做任何操作,此时,缓存中本身就没有新增数据,而数据库中是最新值,这种情况符合我们刚刚所说的一致性的第 2 种情况,所以,此时,缓存和数据库的数据是一致的。

删改数据时:

先删除缓存,后更新数据库:若数据库更新失败,则下次访问缓存时,发现缓存缺失会去数据库找,但是此时得到的是旧数据

先更新数据库,后删除缓存:若缓存删除失败,则缓存会命中,下次访问时直接读到了旧的数据

2. 数据不一致的解决方法

重试机制:可以把要删除的缓存值或者是要更新的数据库值暂存到消息队列中(例如使用Kafka 消息队列)。当应用没有能够成功地删除缓存值或者是更新数据库值时,可以从消息队列中重新读取这些值,然后再次进行删除或更新。如果能够成功地删除或更新,就要把这些值从消息队列中去除,以免重复操作,此时,也可以保证数据库和缓存的数据一致了。

情况一:先删除缓存,再更新数据库。

解决方法:延迟双删。当A还没有来得及更新数据库就有其他B线程读取数据库,但此时读到的为旧值,且会写回Redis缓存,导致数据不一致。方法是在线程 A 更新完数据库值以后,我们可以让它先 sleep 一小段时间,再进行一次缓存删除操作,这样下次其他线程来请求访问的时候,redis缓存为空,就会去数据库查最新的数据,然后写回到redis缓存,此时数据保持了一致性。

情况二:先更新数据库值,再删除缓存值。如果线程 A 删除了数据库中的值,但还没来得及删除缓存值,线程 B 就开始读取数据了,那么此时,线程 B 查询缓存时,发现缓存命中,就会直接从缓存中读取旧值。不过,在这种情况下,如果其他线程并发读缓存的请求不多,那么,就不会有很多请求读取到旧值。而且,线程 A 一般也会很快删除缓存值,这样一来,其他线程再次读取时,就会发生缓存缺失,进而从数据库中读取最新值。所以,这种情况对业务的影响较小。

3. 缓存雪崩

定义:缓存雪崩是指大量的应用请求无法在 Redis 缓存中进行处理,于是应用将大量请求发送到数据库层,导致数据库层的压力激增。

原因一:

缓存中有大量数据同时过期,导致大量请求无法得到处理,于是会把请求发给数据库,导致数据库压力突增,进一步影响到数据库其他业务的请求处理。

解决方法:

① 避免大量数据设置相同的过期时间:比如给过期时间增加一个较小的随机数(限制在1~3分钟),让不同数据在相近的时间失效,此时过期时间有所差别,但差别又不会太大。

② 服务降级:服务降级主要体现在对核心数据和非核心数据的处理。a. 当业务应用访问的是非核心数据时,暂时停止从缓存中查询这些数据,而是直接返回预定义信息、空值或是错误信息;b. 当业务应用访问的是核心数据时,仍然允许查询缓存,如果缓存缺失,也可以继续通过数据库读取。这样只有部分过期数据的请求会发送到数据库,数据库的压力就降低了。

原因二:

redis缓存实例发生故障宕机了,无法处理请求,这就会导致大量请求一下子积压到数据库层,从而发生缓存雪崩。一般一个redis实例可以支持数万级别的请求吞吐量,而单个数据库一般支持数千级别的请求吞吐量,相差近十倍,因此redis实例宕机后数据库要承受近十倍的请求压力。

解决方法:

① 实现服务熔断或请求限流机制:a. 所谓服务熔断,指在发生缓存雪崩时,为了防止引发连锁的数据库雪崩,甚至是整个系统的崩溃,暂停业务应用对缓存系统的接口访问,即缓存客户端并不把请求发给 Redis 缓存实例,而是直接返回,等到 Redis 缓存实例重新恢复服务后,再允许应用请求发送到缓存系统。服务熔断虽然可以保证数据库的正常运行,但是暂停了整个缓存系统的访问,对业务应用的影响范围大。b. 所谓请求限流,即当发生redis宕机而导致的缓存雪崩时,限定进入系统的请求数,避免大量并发请求压力传递到数据库层,同时也不会暂停整个业务系统的访问,对业务应用的影响范围小。

② 事前预防:通过主从节点的方式构建 redis 缓存高可靠集群。如果 redis 缓存的主节点故障宕机了,从节点还可以切换成为主节点,继续提供缓存服务,避免了由于缓存实例宕机而导致的缓存雪崩问题。

4. 缓存击穿

发生场景:当某个访问非常频繁的热点数据的请求无法在缓存中得到处理时(比如热点数据过期失效时),会导致访问该热点数据的大量请求发送到后端数据库,导致数据库压力激增,即表示发生了缓存击穿。

解决方法:

① 对于访问非常频繁的热点数据,就不设置过期时间了,这样一来,对热点数据的访问请求,都可以在缓存中进行处理,而 redis 数万级别的高吞吐量可以很好地应对大量的并发请求访问。

② 检查更新:在缓存快要过期时,提前主动更新缓存。具体操作例如:在每次执行get操作时,同时把缓存key的过期时间(绝对时间)取出来,然后将get出来的缓存过期时间和当前系统时间做一个对比,如果:缓存过期时间 - 当前系统时间 <= 1分钟(可以自定义一个时间),则主动更新缓存,这样可以保证缓存中的数据始终是最新的,相当于让数据不过期。

③ 分级缓存:采用二级缓存L1和L2,L1缓存失效时间短,L2缓存失效时间长,请求优先从L1缓存获取数据,如果L1未命中则加锁,此时只有该请求的线程获取到该锁,这个线程再从数据库读取数据并将数据更新到L1缓存和L2缓存,而其他的线程依然从L2缓存获取数据并返回。

④ 后台刷新:后台定义一个定时任务专门主动更新缓存数据,比如一个缓存中的数据过期时间是30分钟,那么定时任务每隔29分钟定时把数据从数据库中更新到redis缓存中。

5. 缓存穿透

定义:缓存穿透是指要访问的数据既不在redis缓存中,也不在数据库中,这样请求在访问缓存时发生缓存缺失,再去访问数据库,发现数据库也没有要访问的数据,这样就会同时给缓存和数据库带来巨大的压力。

原因:① 业务层误操作:即缓存和数据库中的数据被误删除了;② 恶意攻击:有些恶意攻击专门访问数据库中没有的数据

解决方法:

① 缓存空值或缺省值:在缓存中存储一个空值或者默认值,当下次查询的时候就可以直接从缓存中读取到这些值,而不用把大量请求发送到数据库了。

② 布隆过滤器:布隆过滤器是由初值都为0的bit数组和N个哈希函数组成,用来判断某个数据是否存在。当把数据写入数据库时使用布隆过滤器做标记,当发生缓存缺失后,请求查询数据库时先查询布隆过滤器,如果标记了,即数据写入了数据库,此时可以查询数据库,如果没有标记,就不用再去数据库查询了,这样一来即使发生了缓存穿透,大量的请求只会查询redis和布隆过滤器,而不会积压到数据库。

③ 提前检测:提前防止某些恶意请求去查询数据库中不存在的数据。

6. 缓存满了怎么办

缓存满了需要使用缓存淘汰机制,第一是筛选出对应用访问“不重要”的数据,第二是把这些数据从缓存中删除给新数据腾出位置。

redis共有7种淘汰机制,如下图:

情况一:设置了过期时间的数据中进行淘汰

使用 expire 命令对一批键值对设置了过期时间后,无论是这些键值对的过期时间快到了,还是 Redis 的内存使用量达到了 maxmemory 阈值,Redis 都会进一步按照 volatile-ttl、volatile-random、volatile-lru、volatile-lfu 这四种策略的具体筛选规则进行淘汰。

  • volatile-ttl 根据过期时间的先后进行删除,越早过期的越先被删除。

  • volatile-random 在设置了过期时间的键值对中,进行随机删除。

  • volatile-lru 使用 LRU(Least recently used,最近最少使用) 算法筛选设置了过期时间的键值对。

  • volatile-lfu 使用 LFU(Least frequently used,最不经常使用) 算法选择设置了过期时间的键值对。

情况二:所有数据中进行淘汰

allkeys-lru、allkeys-random、allkeys-lfu 这三种淘汰策略的备选淘汰数据范围,就扩大到了所有键值对,无论这些键值对是否设置了过期时间。如果一个键值对被删除策略选中了,即使它的过期时间还没到,也需要被删除。当然,如果它的过期时间到了但未被策略选中,同样也会被删除。

  • allkeys-random 策略,从所有键值对中随机选择并删除数据。

  • allkeys-lru 策略,使用 LRU 算法在所有数据中进行筛选。

  • allkeys-lfu 策略,使用 LFU 算法在所有数据中进行筛选。

建议:

如果业务中有明显的冷热数据,优先使用 LRU 算法,把最近最常访问的数据留在缓存中。

如果业务数据访问频次相差不大,使用 random 策略,随机淘汰就行。

如何处理被淘汰的数据:

如果这个缓存数据没有被修改过(干净数据),则直接删除;如果被修改过(脏数据),则还需要把数据写会数据库后再删除。

7. 缓存被污染如何处理

有些数据被访问次数非常少,却依然留在缓存中,白白占用缓存大小,这种情况就是缓存被污染了。

解决方法分析:

  • random类的淘汰策略显然不合适,因为它不是根据访问频次来决定淘汰数据的,万一把高频访问的数据给删了就不合适了。
  • ttl策略也不合适,因为它是按照过期时间来决定淘汰数据的,而过期时间不能反应数据的访问频次。
  • lru策略确实可以筛选出最近频繁访问的数据,但在处理扫描式单次查询操作时,无法解决缓存污染。所谓的扫描式单次查询操作,就是指应用对大量的数据进行一次全体读取,每个数据都会被读取,而且只会被读取一次,所以无法判定最近频繁访问的数据。
  • lfu策略是在 lru 策略基础上,为每个数据增加了一个计数器,来统计这个数据的访问次数。当使用 lfu 策略筛选淘汰数据时,首先会根据数据的访问次数进行筛选,把访问次数最低的数据淘汰出缓存。如果两个数据的访问次数相同,lfu 策略再比较这两个数据的访问时效性,把距离上一次访问时间更久的数据淘汰出缓存。

在实际业务应用中,lru 和 lfu 两个策略都有应用,lru 和 lfu 两个策略关注的数据访问特征各有侧重,lru 策略更加关注数据的时效性,而 lfu 策略更加关注数据的访问频次。