前言

本文我们整理总结下Redis使用过程中一些常见的问题,并研究如何解决。

缓存穿透

一般的缓存系统,都是按照key去缓存查询,如果不存在对应的value,就应该去后端系统查找(比如 DB)。

简介

缓存穿透是指在高并发下查询key不存在的数据,会穿过缓存查询数据库。导致数据库压力过大而宕机 。

解决方案

解决方案:

  1. 对查询结果为空的情况也进行缓存,缓存时间(ttl)设置短一点,或者该key对应的数据insert了之后清理缓存。

    存在的问题:缓存太多空值占用了更多的空间

  2. 使用布隆过滤器。在缓存之前在加一层布隆过滤器,在查询的时候先去布隆过滤器查询 key 是否 存在,如果不存在就直接返回,存在再查缓存和DB。

关于布隆过滤器,是1970年布隆提出的,实际上是一个很长的二进制向量和一系列随机hash映射函数。

布隆过滤器是加在客户端和缓存层之间的一个过滤器,可以检索一个元素是否在集合当中,如下图所示:

布隆过滤器的基本思想是,当一个元素被加入集合时,通过K个Hash函数将这个元素映射成一个数组中的K 个点,把它们置为1。检索时,我们只要看看这些点是不是都是1就(大约)知道集合中有没有它了:如 果这些点有任何一个0,则被检元素一定不在;如果都是1,则被检元素很可能在。

示意图如下:

布隆过滤器有什么优势呢?

最大的优势就是快。布隆过滤器的空间效率和查询时间都远远超过一般的算法,因为都是hash值计算,时间复杂度都是O(1)。把字符串转换成位(0或1)进行存储,节省了大量空间。

缓存雪崩

简介

当缓存服务器重启或者大量缓存集中在某一个时间段失效,这样在失效的时候,也会给后端系统(比如 DB)带来很大压力。

我们把突然间大量的key失效了redis重启,大量访问数据库,导致数据库崩溃的这种现象称为缓存雪崩。

解决方案

对于缓存雪崩,常见的解决方案有以下几种:

  1. 将缓存中key的失效期分散开,对不同的key设置不同的有效期,从而避免大量key在同一时间段内失效;
  2. 设置二级缓存

​ 我们可以在Tomcat中设置jvm本地缓存,通常可以采用Ehcache和Guava Cache等缓存框架。

存在的问题:数据不能保持时时一致,因为从DB同步数据到本地缓存需要花费时间。这种方案可以适用在一些对于数据一致性要求不太高的场景下,往往对一些不经常变化的数据做二级缓存,效率比较高。

  1. 对于缓存服务器做一些高可用配置,比如集群、主从等。

​ 存在的问题:有可能造成数据脏读,因为各个缓存服务器之间同步数据需要花费时间。

缓存击穿

简介

缓存在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓 存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。 我们把这种现象称为缓存击穿。

缓存雪崩和缓存击穿的区别是什么呢

缓存击穿针对的是某一key的缓存(这些key可能会在某些时间点被超高并发地访问,是一种非常“热 点”的数据),而缓存雪崩针对的是大量的key。

解决方案

  1. 不设置超时时间

    存在的问题:当数据库数据发生更新时,缓存中的数据不会及时更新,这样会造成数据库中的数据与缓存中的数据的 不一致,应用会从缓存中读取到脏数据。可采用延时双删策略处理。

  2. 用分布式锁控制访问的线程

​ 使用redis的setnx互斥锁先进行判断,这样其他线程就处于等待状态,保证不会有大并发操作去操作数据库。

存在的问题:虽然解决了缓存击穿的问题,但是所有的请求都是串行访问,效率低。

数据不一致

简介

缓存和数据库中的数据不一致,这是常见的问题。

如何解决?

要保证强一致性很难,我们追求最终一致性。

要保证数据的最终一致性,在 缓存的读写模式 一文中介绍过延时双删策略。我们综合延时双删策略,再加上一些其他的措施来尽量保证数据的最终一致性:

  1. 先更新数据库同时删除缓存,等读的时候填充缓存;
  2. 2秒后再删除一次缓存;
  3. 设置缓存过期时间(expired time),比如10秒或者1小时;
  4. 将缓存删除失败记录到日志中,利用脚本提取失败记录再次删除(缓存失效期过长 7*24)

升级方案:通过数据库的binlog来异步淘汰key,利用工具(canal)将binlog日志采集发送到MQ中,然后通过ACK机 制确认处理删除缓存。

数据并发竞争

简介

多数据并发竞争问题指的是多个redis的client同时set 同一个key引起的并发问题。 例如:多客户端(Jedis)同时并发写一个key,一个key的值是1,本来按顺序修改为2,3,4,最后是4,但是顺序变成了4,3,2,最后变成了2。

解决方案

方案一 分布式锁+时间戳

主要思想是准备一个分布式锁,大家去抢锁,抢到锁就做set操作。 加锁的目的实际上就是把并行读写改成串行读写的方式,从而来避免资源竞争 。

主要用到的redis函数是setnx()。

由于上面举的例子,要求key的操作需要顺序执行,所以需要保存一个时间戳判断set顺序。

系统A key 1 {ValueA 7:00}

系统B key 1 { ValueB 7:05}

假设系统B先抢到锁,将key1设置为{ValueB 7:05}。接下来系统A抢到锁,发现自己的key1的时间戳早于缓存中的时间戳(7:00<7:05),那就不做set操作了。

方案二 利用消息队列

在并发量过大的情况下,可以通过消息中间件进行处理,把并行读写进行串行化。 把Redis的set操作放在队列中使其串行化,必须的一个一个执行。

Hot Key

简介

当有大量的请求(几十万)访问某个Redis某个key时,由于流量集中达到网络上限,从而导致这个redis的服务器宕机。造成缓存击穿,接下来对这个key的访问将直接访问数据库造成数据库崩溃,或者访问数据库回填Redis再访问Redis,继续崩溃。

解决方案

如何发现热key?

  1. 预估热key,比如秒杀的商品、火爆的新闻等
  2. 在客户端进行统计,实现简单,加一行代码即可
  3. 如果是Proxy,比如Codis,可以在Proxy端收集
  4. 利用Redis自带的命令,monitor、hotkeys。但是执行缓慢(不要用)
  5. 利用基于大数据领域的流式计算技术来进行实时数据访问次数的统计,比如 Storm、Spark Streaming、Flink等等这些技术,发现热点数据后可以写到zookeeper中

如何处理热Key

  1. 变分布式缓存为本地缓存 发现热key后,把缓存数据取出后,直接加载到本地缓存中。可以采用Ehcache、Guava Cache都可以,这样系统在访问热key数据时就可以直接访问自己的缓存了。(数据不要求时时一致)
  2. 在每个Redis主节点上备份热key数据,这样在读取时可以采用随机读取的方式,将访问压力负载到每个Redis上。
  3. 利用对热点数据访问的限流熔断保护措施 每个系统实例每秒最多请求缓存集群读操作不超过 400 次,一超过就可以熔断掉,不让请求缓存集群,直接返回一个空白信息,然后用户稍后会自行再次重新刷新页面之类的。(对于常用的首页这种方案不行,因为系统友好性太差)。通过系统层自己直接加限流熔断保护措施,可以很好的保护后面的缓存集群。

Big Key

简介

大key指的是存储的值(Value)非常大,常见场景有:热门话题下的讨论、大V的粉丝列表、序列化后的图片、没有及时处理的垃圾数据 。

造成的问题

  1. 大key会大量占用内存,在集群中无法均衡
  2. Redis的性能下降,主从复制异常
  3. 在主动删除或过期删除时会操作时间过长而引起服务阻塞

解决方案

如何发现Big Key?

  1. redis-cli --bigkeys命令。可以找到某个实例5种数据类型(String、hash、list、set、zset)的最大key。 存在的问题:但如果Redis 的key比较多,执行该命令会比较慢
  2. 获取生产Redis的rdb文件,通过rdbtools分析rdb生成csv文件,再导入MySQL或其他数据库中进行分析统计,根据size_in_bytes统计bigkey 。

如何处理Big Key?

优化big key的原则就是string减少字符串长度,list、hash、set、zset等减少成员数。

① string类型的big key,尽量不要存入Redis中,可以使用文档型数据库MongoDB或缓存到CDN上。 如果必须用Redis存储,最好单独存储,不要和其他的key一起存储。采用一主一从或多从。

② 单个简单的key存储的value很大,可以尝试将对象分拆成几个key-value, 使用mget获取值,这样分拆的意义在于分拆单次操作的压力,将操作压力平摊到多次操作中,降低对redis的IO影响。

常见的做法是对于hash, set,zset,list 中存储过多的元素,可以将这些元素分拆。

举例如下:

以hash类型举例来说,对于field过多的场景,可以根据field进行hash取模,生成一个新的key,例如原来的 hash_key:{filed1:value, filed2:value, filed3:value ...},可以hash取模后形成如下key:value形式:

hash_key:1:{filed1:value}

hash_key:2:{filed2:value}

hash_key:3:{filed3:value} ...

取模后,将原先单个key分成多个key,每个key filed个数为原先的1/N

③ 删除大key时不要使用del,因为del是阻塞命令,删除时会影响性能。

④ 使用 lazy delete (unlink命令)

此命令的作用是删除指定的key(s),若key不存在则该key被跳过。但是,相比DEL会产生阻塞,该命令会在另一个线程中回收内存,因此它是非阻塞的。 这也是该命令名字的由来:仅将keys从key空间中删除,真正的数据删除会在后续异步操作。

redis> SET key1 "Hello"

"OK"

redis> SET key2 "World"

"OK"

redis> UNLINK key1 key2 key3

(integer) 2

总结

我们主要介绍了Redis使用中常见的一些问题,包括缓存雪崩、缓存穿透、缓存击穿、Hot Key、Big key等,发生的场景,以及常用的解决方案,我们以后在实际开发使用Redis时能够对于这些问题有更好的解决。