在咱们平常的开发中,无不都是使用数据库来进行数据的存储,因为通常的系统任务中一般不会存在高并发的状况,因此这样看起来并无什么问题,但是一旦涉及大数据量的需求,好比一些商品抢购的情景,或者是主页访问量瞬间较大的时候,单一使用数据库来保存数据的系统会由于面向磁盘,磁盘读/写速度比较慢的问题而存在严重的性能弊端,一瞬间成千上万的请求到来,须要系统在极短的时间内完成成千上万次的读/写操做,这个时候每每不是数据库可以承受的,极其容易形成数据库系统瘫痪,最终致使服务宕机的严重生产问题。java
为了克服上述的问题,项目一般会引入NoSQL技术,这是一种基于内存的数据库,而且提供必定的持久化功能。redis
redis技术就是NoSQL技术中的一种,可是引入redis又有可能出现缓存穿透,缓存击穿,缓存雪崩等问题。本文就对这三种问题进行较深刻剖析。sql
一个必定不存在缓存及查询不到的数据,因为缓存是不命中时被动写的,而且出于容错考虑,若是从存储层查不到数据则不写入缓存,这将致使这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。数据库
有不少种方法能够有效地解决缓存穿透问题,最多见的则是采用布隆过滤器,将全部可能存在的数据哈希到一个足够大的bitmap中,一个必定不存在的数据会被 这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。另外也有一个更为简单粗暴的方法(咱们采用的就是这种),若是一个查询返回的数据为空(不论是数据不存在,仍是系统故障),咱们仍然把这个空结果进行缓存,但它的过时时间会很短,最长不超过五分钟。后端
粗暴方式伪代码:缓存
//伪代码 public object GetProductListNew() { int cacheTime = 30; String cacheKey = "product_list"; String cacheValue = CacheHelper.Get(cacheKey); if (cacheValue != null) { return cacheValue; } cacheValue = CacheHelper.Get(cacheKey); if (cacheValue != null) { return cacheValue; } else { //数据库查询不到,为空 cacheValue = GetProductListFromDB(); if (cacheValue == null) { //若是发现为空,设置个默认值,也缓存起来 cacheValue = string.Empty; } CacheHelper.Add(cacheKey, cacheValue, cacheTime); return cacheValue; } }
key可能会在某些时间点被超高并发地访问,是一种很是“热点”的数据。这个时候,须要考虑一个问题:缓存被“击穿”的问题。安全
使用互斥锁(mutex key)服务器
业界比较经常使用的作法,是使用mutex。简单地来讲,就是在缓存失效的时候(判断拿出来的值为空),不是当即去load db,而是先使用缓存工具的某些带成功操做返回值的操做(好比Redis的SETNX或者Memcache的ADD)去set一个mutex key,当操做返回成功时,再进行load db的操做并回设缓存;不然,就重试整个get缓存的方法。并发
SETNX,是「SET if Not eXists」的缩写,也就是只有不存在的时候才设置,能够利用它来实现锁的效果。分布式
public String get(key) { String value = redis.get(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; } }
memcache代码:
if (memcache.get(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(); } }
其它方案:待各位补充。
与缓存击穿的区别在于这里针对不少key缓存,前者则是某一个key。
缓存正常从Redis中获取,示意图以下:
缓存失效瞬间示意图以下:
缓存失效时的雪崩效应对底层系统的冲击很是可怕!大多数系统设计者考虑用加锁或者队列的方式保证来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上。还有一个简单方案就时讲缓存失效时间分散开,好比咱们能够在原有的失效时间基础上增长一个随机值,好比1-5分钟随机,这样每个缓存的过时时间的重复率就会下降,就很难引起集体失效的事件。
加锁排队,伪代码以下:
//伪代码 public object GetProductListNew() { int cacheTime = 30; String cacheKey = "product_list"; String lockKey = cacheKey; String cacheValue = CacheHelper.get(cacheKey); if (cacheValue != null) { return cacheValue; } else { synchronized(lockKey) { cacheValue = CacheHelper.get(cacheKey); if (cacheValue != null) { return cacheValue; } else { //这里通常是sql查询数据 cacheValue = GetProductListFromDB(); CacheHelper.Add(cacheKey, cacheValue, cacheTime); } } return cacheValue; } }
加锁排队只是为了减轻数据库的压力,并无提升系统吞吐量。假设在高并发下,缓存重建期间key是锁着的,这是过来1000个请求999个都在阻塞的。一样会致使用户等待超时,这是个治标不治本的方法!
注意:加锁排队的解决方式分布式环境的并发问题,有可能还要解决分布式锁的问题;线程还会被阻塞,用户体验不好!所以,在真正的高并发场景下不多使用!
随机值伪代码:
//伪代码 public object GetProductListNew() { int cacheTime = 30; String cacheKey = "product_list"; //缓存标记 String cacheSign = cacheKey + "_sign"; String sign = CacheHelper.Get(cacheSign); //获取缓存值 String cacheValue = CacheHelper.Get(cacheKey); if (sign != null) { return cacheValue; //未过时,直接返回 } else { CacheHelper.Add(cacheSign, "1", cacheTime); ThreadPool.QueueUserWorkItem((arg) -> { //这里通常是 sql查询数据 cacheValue = GetProductListFromDB(); //日期设缓存时间的2倍,用于脏读 CacheHelper.Add(cacheKey, cacheValue, cacheTime * 2); }); return cacheValue; } }
解释说明:
关于缓存崩溃的解决方法,这里提出了三种方案:使用锁或队列、设置过时标志更新缓存、为key设置不一样的缓存失效时间,还有一种被称为“二级缓存”的解决方法。
针对业务系统,永远都是具体状况具体分析,没有最好,只有最合适。
于缓存其它问题,缓存满了和数据丢失等问题,大伙可自行学习。最后也提一下三个词LRU、RDB、AOF,一般咱们采用LRU策略处理溢出,Redis的RDB和AOF持久化策略来保证必定状况下的数据安全。
参考相关连接:
https://blog.csdn.net/zeb_per...
https://blog.csdn.net/fanrenx...
https://baijiahao.baidu.com/s...
https://blog.csdn.net/xlgen15...
视频资源获取,可直进百度云群:
https://pan.baidu.com/mbox/ho...
本文在米兜公众号连接
https://mp.weixin.qq.com/s/ks...
欢迎关注米兜Java,一个注在共享、交流的Java学习平台。