前言:本节我们一起来探讨一下Redis常见的几个问题。

1、缓存穿透

现象:
一般的缓存系统,都是按照key去缓存查询,如果不存在对应的value,就应该去后端系统查找(比如 DB)。
缓存穿透是指在高并发下查询key不存在的数据,会穿过缓存查询数据库,导致数据库压力过大而宕机 。
关键词:不存在的Key
产生原因:
第一, 自身业务代码或者数据出现问题。
第二, 一些恶意攻击、 爬虫等造成大量空命中。
解决方案:
1、对查询结果为空的情况也进行缓存,缓存时间(ttl)设置短一点,或者该key对应的数据insert了 之后清理缓存。 问题:缓存太多空值占用了更多的空间
2、使用布隆过滤器(这里不做过多介绍)。在缓存之前在加一层布隆过滤器,在查询的时候先去布隆过滤器查询 key 是否 存在,如果不存在就直接返回,存在再查缓存和DB。

2、缓存击穿

现象:
缓存击穿,是指一个key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。
关键词: 一个key或一些key、热点key
产生原因:业务考虑不周全
解决方案:
1、用分布式锁控制访问的线程 使用redis的setnx互斥锁先进行判断,这样其他线程就处于等待状态,保证不会有大并发操作去操作数据库。
2、不设超时时间,volatile-lru 但会造成写一致问题 当数据库数据发生更新时,缓存中的数据不会及时更新,这样会造成数据库中的数据与缓存中的数据的不一致,应用会从缓存中读取到脏数据。可采用延时双删策略处理。

3、缓存雪崩

现象:缓存雪崩,是指在某一个时间段,缓存集中过期失效。
关键词:大批量Key同时失效
产生原因:产生雪崩的原因之一,比如在写本文的时候,马上就要到双十二零点,很快就会迎来一波抢购,这波商品时间比较集中的放入了缓存,假设缓存一个小时。那么到了凌晨一点钟的时候,这批商品的缓存就都过期了。而对这批商品的访问查询,都落到了数据库上,对于数据库而言,就会产生周期性的压力波峰。
解决方案:
1、 key的失效期分散开,不同的key设置不同的有效期 。

4、Redis删除策略和内存淘汰机制

删除策略

Redis的数据删除有定时删除、惰性删除和主动删除三种方式。 Redis目前采用惰性删除+主动删除的方式。

定时删除
在设置键的过期时间的同时,创建一个定时器,让定时器在键的过期时间来临时,立即执行对键的删除 操作。 需要创建定时器,而且消耗CPU,一般不推荐使用。
惰性删除
在key被访问时如果发现它已经失效,那么就删除它。 调用expireIfNeeded函数,该函数的意义是:读取数据之前先检查一下它有没有失效,如果失效了就删 除它。
主动删除
在redis.conf文件中可以配置主动删除策略,默认是no-enviction(不删除)

内存淘汰机制

主动清理策略在Redis 4.0 之前一共实现了 6 种内存淘汰策略,在 4.0 之后,又增加了 2 种策 略,总共8种:

针对设置了过期时间的key做处理:
1. volatile-ttl:在筛选时,会针对设置了过期时间的键值对,根据过期时间的先后进行删 除,越早过期的越先被删除。
2. volatile-random:就像它的名称一样,在设置了过期时间的键值对中,进行随机删除。
3. volatile-lru:会使用 LRU 算法筛选设置了过期时间的键值对删除。 4. volatile-lfu:会使用 LFU 算法筛选设置了过期时间的键值对删除。
针对所有的key做处理:
5. allkeys-random:从所有键值对中随机选择并删除数据。
6. allkeys-lru:使用 LRU 算法在所有数据中进行筛选删除。
7. allkeys-lfu:使用 LFU 算法在所有数据中进行筛选删除。
不处理:
8. noeviction:不会剔除任何数据,拒绝所有写入操作并返回客户端错误信息”(error) OOM command not allowed when used memory”,此时Redis只响应读操作。

扩展: LRU(Least recently used) 最近最少使用,算法根据数据的历史访问记录来进行淘汰数据,其核心思想 是“如果数据最近被访问过,那么将来被访问的几率也更高”。 最常见的实现是使用一个链表保存缓存数据,详细算法实现如下:
LFU(Least frequently used) 最不经常使用,如果一个数据在最近一段时间内使用次数很少,那么在将 来一段时间内被使用的可能性也很小。

5、Redis双写一致性问题

我们在高并发场景中,在遇到更新数据时候,怎样操作缓存和数据库?目前还没有很全面的方案,下面我们来梳理一下几种处理方法:

先更新缓存,再更新数据库

对于这种方式,是最不能接受的,道理显而易见,若更新了缓存后,更新数据库后回滚了事务怎么办?这里不再赘述。

先更新数据库,再更新缓存

这种方式,也是不能接受的,看下面的例子:

  • (1)线程A更新了数据库
  • (2)线程B更新了数据库
  • (3)线程B更新了缓存
  • (4)线程A更新了缓存

很明显,这种场景造成了脏数据,所以,这种方式也不考虑

先删除缓存,再更新数据库

这种方式也有弊端,但是可以弥补。先来看弊端:

  • (1)请求A要进行修改操作,根据策略,首先删除缓存。
  • (2)请求B要进行查询操作,发现缓存不存在,于是查询数据库,从数据库查到了旧值。
  • (4)请求B将旧值写入缓存。
  • (5)请求A将新值写入数据库。

    上述情况就会导致,数据库里存的值,和缓存值不一样。
    解决办法:延时双删策略

  • (1)先删除缓存

  • (2)再写数据库(这两步和原来一样)
  • (3)休眠N秒,
  • (4)再次删除缓存。

这么做,可以将N秒内所造成的缓存脏数据,再次删除。针对上面的情形,我们应该自行评估自己的项目的读数据业务逻辑的耗时。然后写数据的休眠时间则在读数据业务逻辑的耗时基础上,加几百ms即可。

先更新数据库,再删缓存

这种策略,几乎不产生垃圾数据,但是也有极端情况:假设A是读操作,B是写操作。
(1)此时缓存刚好失效
(2)请求A查询数据库,得一个旧值
(3)请求B将新值写入数据库
(4)请求B删除缓存
(5)请求A将查到的旧值写入缓存
如果发生上述情况,确实是会发生脏数据。但是这种情况非常少见,如何解决?首先,给缓存设有效时间是一种方案。其次,采用前面讲的异步延时删除策略也可以解决。

6、Redis实现分布式锁

  • 基于数据库悲观锁
  • 基于redis SETNX命令
  • 基于zookeeper