Redis缓存常见问题及解决⽅案
⽂章⽬录
Redis缓存
Redis是⼀种存在内存中的数据库,它的读写速度⾮常快,被⼴泛应⽤于缓存⽅向;另外,redis 也经常⽤来做分布式锁。redis 提供了多种数据类型来⽀持不同的业务场景。除此之外,redis ⽀持事务 、持久化、LUA脚本、LRU驱动事件、多种集⽅案。
Redis两⼤特性
⾼性能:
假如⽤户第⼀次访问数据库中的某些数据。这个过程会⽐较慢,因为是从硬盘上读取的。将该⽤户访问的数据存在数
缓存中,这样下⼀次再访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存,所以速度相当
快。如果数据库中的对应数据改变的之后,同步改变缓存中相应的数据即可!
⾼并发:
直接操作缓存能够承受的请求是远远⼤于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中
去,这样⽤户的⼀部分请求会直接到缓存这⾥⽽不⽤经过数据库。
Redis缓存常见问题及解决⽅案
缓存击穿
原理
缓存穿透是指查询⼀个⼀定不存在的数据,由于缓存是不命中时被动写的,并且出于容错考虑,如果从存储层查不到数据则不写⼊缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。在流量⼤时,可能DB就挂掉了,要是有⼈利⽤不存在的key频繁攻击我们的应⽤,这就是漏洞。
解决⽅案
有很多种⽅法可以有效地解决缓存穿透问题,最常见的则是采⽤布隆过滤器,将所有可能存在的数据哈希到⼀个⾜够⼤的bitmap中,⼀个⼀定不存在的数据会被 这个bitmap拦截掉,从⽽避免了对底层存储系统的查询压⼒。另外也有⼀个更为简单粗暴的⽅法(我们采⽤的就是这种),如果⼀个查询返回的数据为空(不管是数 据不存在,还是系统故障),我们仍然把这个空结果进⾏缓存,但它的过期时间会很短,最长不超过五分钟。
缓存雪崩
原理
缓存雪崩是指在我们设置缓存时采⽤了相同的过期时间,导致缓存在某⼀时刻同时失效,请求全部转发到DB,DB瞬时压⼒过重雪崩。
解决⽅案
缓存失效时的雪崩效应对底层系统的冲击⾮常可怕。⼤多数系统设计者考虑⽤加锁或者队列的⽅式保证缓存的单线 程(进程)写,从⽽避免失效时⼤量的并发请求落到底层存储系统上。这⾥分享⼀个简单⽅案就时讲缓存失效时间分散开,⽐如我们可以在原有的失效时间基础上增加⼀个随机值,⽐如1-5分钟随机,这样每⼀个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
缓存击穿
原理
对于⼀些设置了过期时间的key,如果这些key可能会在某些时间点被超⾼并发地访问,是⼀种⾮常“热点”的数据。这个时候,需要考虑⼀个问题:缓存被“击穿”的问题,这个和缓存雪崩的区别在于这⾥针对某⼀key缓存,前者则是很多key。
缓存在某个时间点过期的时候,恰好在这个时间点对这个Key有⼤量的并发请求过来,这些请求发现缓存过期⼀般都会从后端DB加载数据并回设到缓存,这个时候⼤并发的请求可能会瞬间把后端DB压
垮。解决⽅案
使⽤互斥锁(mutex key)
业界⽐较常⽤的做法,是使⽤mutex。简单地来说,就是在缓存失效的时候(判断拿出来的值为空),不是⽴即去load db,⽽是先使⽤缓存⼯具的某些带成功操作返回值的操作(⽐如Redis的SETNX或者Memcache的ADD)去set⼀个mutex key,当操作返回成功时,再进⾏load db的操作并回设缓存;否则,就重试整个get缓存的⽅法。
SETNX,是「SET if Not eXists」的缩写,也就是只有不存在的时候才设置,可以利⽤它来实现锁的效果。在redis2.6.1之前版本未实现setnx的过期时间,所以这⾥给出两种版本代码参考:
最新版本代码:
memcache代码://2.6.1前单机版本锁String get(String key) {      String value = (key);      if (value  == null) {        if (redis.setnx(key_mutex, "1")) {            // 3 min timeout to avoid mutex holder crash            pire(key_mutex, 3 * 60)            value = db.get(key);            redis.set(key, value);            redis.delete(key_mutex);        } else {              //其他线程休息50毫秒后重试              Thread.sleep(50);              get(key);        }    }  }
1
2
3
4
5
678
9
10
11
12
13
14
15
16
17public String get(key) {  String value = (key);  if (value == null) { //代表缓存值过期      //设置3min 的超时,防⽌del 操作失败的时候,下次缓存过期⼀直不能load db    if (redis.setnx(key_mutex, 1, 3 * 60) == 1) {  //代表设置成功          value = db.get(key);                  redis.set(key, value, expire_secs);                  redis.del(key_mutex);          } else {  //这个时候代表同时候的其他线程已经load db 并回设到缓存了,这时候重试获取缓存值即可                  sleep(50);                  get(key);  //重试          }      } else {          return value;            }}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
"提前"使⽤互斥锁(mutex key):
在value内部设置1个超时值(timeout1), timeout1⽐实际的memcache timeout(timeout2)⼩。当从cache读取到timeout1发现它已经过期时候,马上延长timeout1并重新设置到cache。然后再从数据库加载数据并设置到cache中。伪代码如下:
“永远不过期”:
这⾥的“永远不过期”包含两层意思:
(1) 从redis上看,确实没有设置过期时间,这就保证了,不会出现热点key过期问题,也就是“物理”不过期。
(2) 从功能上看,如果不过期,那不就成静态的了吗?所以我们把过期时间存在key对应的value⾥,如果发现要过期了,通过⼀个后台的异步线程进⾏缓存的构建,也就是“逻辑”过期
从实战看,这种⽅法对于性能⾮常友好,唯⼀不⾜的就是构建缓存时候,其余线程(⾮构建缓存的线程)可能访问的是⽼数据,但是对于⼀般的互联⽹功能来说这个还是可以忍受。if ((key) ==
null) {      // 3 min timeout to avoid mutex holder crash      if (memcache.add(key_mutex, 3 * 60 * 1000) == true) {          value = db.get(key);          memcache.set(key, value);          memcache.delete(key_mutex);      } else {          sleep(50);          retry();      }  }
1
2
3
4
5
6
7
8
9
10
11v = (key);  if (v == null) {      if (memcache.add(key_mutex, 3 * 60 * 1000) == true) {          value = db.get(key);          memcache.set(key, value);          memcache.delete(key_mutex);      } else {          sleep(50);          retry();      }  } else {      if (v.timeout <= now()) {          if (memcache.add(key_mutex, 3 * 60 * 1000) == true) {              // extend the timeout for other threads              v.timeout += 3 * 60 * 1000;              memcache.set(key, v, KEY_TIMEOUT * 2);              // load the latest value from db              v = db.get(key);              v.timeout = KEY_TIMEOUT;              memcache.set(key, value, KEY_TIMEOUT * 2);              memcache.delete(key_mutex);          } else {              sleep(50);              retry();          }      }  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
资源保护:
采⽤netflix的hystrix,可以做资源的隔离保护主线程池,如果把这个应⽤到缓存的构建也未尝不可。
四种解决⽅案:没有最佳只有最合适String get(final String key) {      V v = (key);      String value = v.getValue();      long timeout = v.getTimeout();      if (v.timeout <= System.currentTimeMillis()) {          // 异步更新后台异常执⾏          ute(new Runnable() {              public void run() {                  String keyMutex = "mutex:" + key;                  if (redis.setnx(keyMutex, "1")) {                   
  // 3 min timeout to avoid mutex holder crash                      pire(keyMutex, 3 * 60);                      String dbValue = db.get(key);                      redis.set(key, dbValue);                      redis.delete(keyMutex);                  }              }          });      }      return value;  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
redis支持的五种数据类型21

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。