旅游網(wǎng)站模板源碼媒體發(fā)稿網(wǎng)
面試題:
- 緩存預(yù)熱、雪萌、穿透、擊穿分別是什么?你遇到過(guò)那幾個(gè)情況?
- 緩存預(yù)熱你是怎么做的?
- 如何造免或者減少緩存雪崩?
- 穿透和擊穿有什么區(qū)別?他兩是一個(gè)意思還是載然不同?
- 穿適和擊穿你有什么解決方案?如何避免?
- 假如出現(xiàn)了緩存不一致,你有哪些修補(bǔ)方案?
- 。。。。。。
緩存預(yù)熱
@PostConstruct初始化白名單數(shù)據(jù)
詳情地址可查看代碼:Redis BitMap/HyperLogLog/GEO/布隆過(guò)濾器案例_Please Sit Down的博客-CSDN博客
緩存雪崩
出現(xiàn)原因
- redis主機(jī)掛了,redis全盤(pán)崩潰,偏硬件運(yùn)維
- redis中有大量key同時(shí)過(guò)期大面積失效,偏軟件開(kāi)發(fā)
緩存+解決
1、redis中key設(shè)置為永不過(guò)期 or 過(guò)期時(shí)間錯(cuò)開(kāi)
2、redis緩存集群實(shí)現(xiàn)高可用
a、主從+哨兵
b、使用Redis集群
c、開(kāi)啟redis持久化機(jī)制aof/rdb,盡快恢復(fù)緩存集群
3、多緩存結(jié)合預(yù)防雪崩
ehcache本地緩存 + redis緩存
4、服務(wù)降級(jí)
Hystrix或者阿里sentinel限流&降級(jí)
緩存穿透
是什么
????????請(qǐng)求去查詢(xún)一條記錄,先查redis無(wú),后查mysq無(wú),都查詢(xún)不到該條記錄,但是清求每次都會(huì)打到數(shù)據(jù)庫(kù)上面去,導(dǎo)致后臺(tái)數(shù)據(jù)庫(kù)壓力暴增。這種現(xiàn)象我們稱(chēng)為緩存穿適,這個(gè)redis變成了一個(gè)擺設(shè)。
? ? ? ? 簡(jiǎn)單說(shuō)就是:本來(lái)無(wú)物,兩庫(kù)都沒(méi)有。既不在Redis緩存庫(kù),也不在mysql,數(shù)據(jù)車(chē)存在被多次暴擊風(fēng)險(xiǎn)。
解決
主要是防止惡意攻擊,解決方法:空對(duì)象緩存、bloomfilteri過(guò)濾器
方案一
空對(duì)象緩存或者缺省值。
????????第一種解決方案,回寫(xiě)增強(qiáng)。如果發(fā)生了緩存穿透,我們可以針對(duì)要查詢(xún)的數(shù)據(jù),在Redis里存一個(gè)和業(yè)務(wù)部門(mén)商量后確定的缺省值(比如,零、負(fù)數(shù)、defaultNull等)。
????????比如,鍵uid:abcdxxx,值defaultNull作為案例的key和value。先去redis查鍵uid:abcdxxx沒(méi)有,再去mysql查沒(méi)有獲得 ,這就發(fā)生了一次穿透現(xiàn)象。but,可以增強(qiáng)回寫(xiě)機(jī)制。mysql也查不到的話(huà)也讓redis存入剛剛查不到的key并保護(hù)mysql。第一次來(lái)查詢(xún)uid:abcdxxx,redis和mysql都沒(méi)有,返回null給調(diào)用者,但是增強(qiáng)回寫(xiě)后第二次來(lái)查uid:abcdxxx,此時(shí)redis就有值了??梢灾苯訌腞edis中讀取default缺省值返回給業(yè)務(wù)應(yīng)用程序,避免了把大量請(qǐng)求發(fā)送給mysql處理,打爆mysql。但是,此方法架不住黑客的惡意攻擊,有缺陷......,只能解決key相同的情況。
????????黑客或者惡意攻擊:黑客會(huì)對(duì)你的系統(tǒng)進(jìn)行攻擊,拿一個(gè)不存在的id去查詢(xún)數(shù)據(jù),會(huì)產(chǎn)生大量的情求到數(shù)據(jù)庫(kù)去查詢(xún)。可能會(huì)導(dǎo)數(shù)你的數(shù)據(jù)庫(kù)由于壓力過(guò)大而宕掉。
? ? ? ? 1、key相同打你系統(tǒng):第一次打到mysql,空對(duì)象緩存后第二次就返回defaultNull缺省值,避免mysql被攻擊,不用再到數(shù)據(jù)車(chē)中去走一圈了。
? ? ? ? 2、key不同打你系統(tǒng):由于存在空對(duì)象緩存和緩存回寫(xiě)(看自己業(yè)務(wù)不限死),redis中的無(wú)關(guān)緊要的key也會(huì)越寫(xiě)越多(記得設(shè)置redisi過(guò)期時(shí)間)
方案二
使用Google布隆過(guò)器Guava解決緩存穿透。
Guava中布隆過(guò)濾器的實(shí)現(xiàn)算是比較權(quán)威的,所以實(shí)際項(xiàng)目中我們可以直接使用Guava布隆過(guò)濾器。
Guava's BloomFilter源碼出處:https://github.com/google/guava/blob/master/guava/src/com/google/common/hash/BloomFilter.java
白名單過(guò)濾器案例:
說(shuō)明:會(huì)出現(xiàn)誤判問(wèn)題,但是概率小可以接受,不能從布隆過(guò)濾器刪除;全部合法的key都需要放入Guava版布隆過(guò)濾器+redis里面,不然數(shù)據(jù)就是返回null。
代碼實(shí)現(xiàn):
pom.xml
<!--guava Google 開(kāi)源的 Guava 中自帶的布隆過(guò)濾器-->
<dependency><groupId>com.google.guava</groupId><artifactId>guava</artifactId><version>23.0</version>
</dependency>
yml
server.port=7777
spring.application.name=redis7# ========================redis單機(jī)=====================
spring.redis.database=0
# 修改為自己真實(shí)IP
spring.redis.host=192.168.111.185
spring.redis.port=6379
spring.redis.password=111111
spring.redis.lettuce.pool.max-active=8
spring.redis.lettuce.pool.max-wait=-1ms
spring.redis.lettuce.pool.max-idle=8
spring.redis.lettuce.pool.min-idle=0
測(cè)試1:
@Test
public void testGuavaWithBloomFilter(){// 創(chuàng)建布隆過(guò)濾器對(duì)象BloomFilter<Integer> filter = BloomFilter.create(Funnels.integerFunnel(), 100);// 判斷指定元素是否存在System.out.println(filter.mightContain(1));System.out.println(filter.mightContain(2));// 將元素添加進(jìn)布隆過(guò)濾器filter.put(1);filter.put(2);System.out.println(filter.mightContain(1));System.out.println(filter.mightContain(2));
}// 結(jié)果
// false false // true true
測(cè)試2:取樣本100W數(shù)據(jù),查查不在100W范圍內(nèi),其它10W數(shù)據(jù)是否存在
controller
import com.atguigu.redis7.service.GuavaBloomFilterService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;import javax.annotation.Resource;@Api(tags = "google工具Guava處理布隆過(guò)濾器")
@RestController
@Slf4j
public class GuavaBloomFilterController{@Resourceprivate GuavaBloomFilterService guavaBloomFilterService;@ApiOperation("guava布隆過(guò)濾器插入100萬(wàn)樣本數(shù)據(jù)并額外10W測(cè)試是否存在")@RequestMapping(value = "/guavafilter",method = RequestMethod.GET)public void guavaBloomFilter() {guavaBloomFilterService.guavaBloomFilter();}
}
service
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;import java.util.ArrayList;
import java.util.List;@Service
@Slf4j
public class GuavaBloomFilterService{public static final int _1W = 10000;//布隆過(guò)濾器里預(yù)計(jì)要插入多少數(shù)據(jù)public static int size = 100 * _1W;//誤判率,它越小誤判的個(gè)數(shù)也就越少(思考,是不是可以設(shè)置的無(wú)限小,沒(méi)有誤判豈不更好)//fpp the desired false positive probabilitypublic static double fpp = 0.03;// 構(gòu)建布隆過(guò)濾器private static BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), size,fpp);public void guavaBloomFilter(){//1 先往布隆過(guò)濾器里面插入100萬(wàn)的樣本數(shù)據(jù)for (int i = 1; i <=size; i++) {bloomFilter.put(i);}//故意取10萬(wàn)個(gè)不在過(guò)濾器里的值,看看有多少個(gè)會(huì)被認(rèn)為在過(guò)濾器里L(fēng)ist<Integer> list = new ArrayList<>(10 * _1W);for (int i = size+1; i <= size + (10 *_1W); i++) {if (bloomFilter.mightContain(i)) {log.info("被誤判了:{}",i);list.add(i);}}log.info("誤判的總數(shù)量::{}",list.size());}
}
結(jié)果:
現(xiàn)在總共有10萬(wàn)數(shù)據(jù)是不存在的,誤判了3033次,原始樣本:100W
不存在數(shù)據(jù):1000000W---1100000W ????
誤判率:3033 / 100000 = 0.03033
深刻分析代碼:核心BloomFilter.create
方法
@VisibleForTestingstatic <T> BloomFilter<T> create(Funnel<? super T> funnel, long expectedInsertions, double fpp, Strategy strategy) {。。。。
}
這里有四個(gè)參數(shù):
-
funnel
:數(shù)據(jù)類(lèi)型(通常是調(diào)用Funnels工具類(lèi)中的) -
expectedInsertions
:指望插入的值的個(gè)數(shù) -
fpp
:誤判率(默認(rèn)值為0.03) -
strategy
:哈希算法
問(wèn)題:為什么fpp設(shè)置成0.03?
情景一:fpp = 0.01
- 誤判個(gè)數(shù):947
- 占內(nèi)存大小:9585058位數(shù)
- 解決的hash沖突函數(shù):7個(gè)
情景二:fpp = 0.03
(默認(rèn)參數(shù))
- 誤判個(gè)數(shù):3033
- 占內(nèi)存大小:7298440位數(shù)
- 解決的hash沖突函數(shù):5個(gè)
情景三:fpp=0.000000000000001
- 占用內(nèi)存大小:67095408位數(shù)
- 解決的hash沖突函數(shù):47個(gè)
情景總結(jié):
- 誤判率能夠經(jīng)過(guò)fpp參數(shù)進(jìn)行調(diào)節(jié)
- fpp越小,須要的內(nèi)存空間就越大:0.01須要900多萬(wàn)位數(shù),0.03須要700多萬(wàn)位數(shù)。
- fpp越小,集合添加數(shù)據(jù)時(shí),就須要更多的hash函數(shù)運(yùn)算更多的hash值,去存儲(chǔ)到對(duì)應(yīng)的數(shù)組下標(biāo)里。(忘了去看上面的布隆過(guò)濾存入數(shù)據(jù)的過(guò)程)
上面的numBits,表示存一百萬(wàn)個(gè)int類(lèi)型數(shù)字,須要的位數(shù)為7298440,700多萬(wàn)位。理論上存一百萬(wàn)個(gè)數(shù),一個(gè)int是4字節(jié)32位,須要481000000=3200萬(wàn)位。若是使用HashMap去存,按HashMap50%的存儲(chǔ)效率,須要6400萬(wàn)位。能夠看出BloomFilter的存儲(chǔ)空間很小,只有HashMap的1/10左右。
上面的numHashFunctions
表示須要幾個(gè)hash函數(shù)運(yùn)算,去映射不一樣的下標(biāo)存這些數(shù)字是否存在(0 or 1)。
布隆過(guò)濾器說(shuō)明:?
黑名單過(guò)濾器案例:
緩存擊穿
是什么
????????大量的請(qǐng)求同時(shí)查詢(xún)一個(gè)key時(shí),此時(shí)這個(gè)key正好失效了,就會(huì)導(dǎo)致大量的請(qǐng)求都打到數(shù)據(jù)庫(kù)上面去。簡(jiǎn)單說(shuō)就是熱點(diǎn)key突然失效了,暴打mysql
備注:穿透和擊穿,截然不同。
危害
會(huì)造成某一時(shí)刻數(shù)據(jù)庫(kù)請(qǐng)求量過(guò)大,壓力劇增。
一般技術(shù)部門(mén)需要知道熱點(diǎn)key是那些個(gè)?做到心里有數(shù)防止擊穿
解決
互斥更新、隨機(jī)退避、差異失效時(shí)間
熱點(diǎn)key失效問(wèn)題:時(shí)間到了自然清除但還波訪(fǎng)問(wèn)到;delete掉的key,剛I巧又被訪(fǎng)問(wèn)
方案1:差異失效時(shí)間,對(duì)于訪(fǎng)問(wèn)須繁的熱點(diǎn)key,干脆就不設(shè)置過(guò)期時(shí)間
方案2:互斥跟新,采用雙檢加鎖策略
????????多個(gè)線(xiàn)程同時(shí)去查詢(xún)數(shù)據(jù)庫(kù)的這條數(shù)據(jù),那么我們可以在第一個(gè)查詢(xún)數(shù)據(jù)的請(qǐng)求上使用一個(gè) 互斥鎖來(lái)鎖住它。其他的線(xiàn)程走到這一步拿不到鎖就等著,等第一個(gè)線(xiàn)程查詢(xún)到了數(shù)據(jù),然后做緩存。后面的線(xiàn)程進(jìn)來(lái)發(fā)現(xiàn)已經(jīng)有緩存了,就直接走緩存。
案例
天貓聚劃算功能實(shí)現(xiàn)+防止緩存擊穿(熱點(diǎn)key突然失效導(dǎo)致了緩存擊穿)
定時(shí)任務(wù)每次取20條記錄,取的過(guò)程中,突然失效,大量數(shù)據(jù)打到mysql
redis數(shù)據(jù)類(lèi)型選型:list
常規(guī)代碼
entity
import io.swagger.annotations.ApiModel;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;@Data
@AllArgsConstructor
@NoArgsConstructor
@ApiModel(value = "聚劃算活動(dòng)producet信息")
public class Product {//產(chǎn)品IDprivate Long id;//產(chǎn)品名稱(chēng)private String name;//產(chǎn)品價(jià)格private Integer price;//產(chǎn)品詳情private String detail;
}
service:采用定時(shí)器將參與聚劃算活動(dòng)的特價(jià)商品新增進(jìn)入redis中
import cn.hutool.core.date.DateUtil;
import com.atguigu.redis7.entities.Product;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;import javax.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.TimeUnit;@Service
@Slf4j
public class JHSTaskService {public static final String JHS_KEY="jhs";public static final String JHS_KEY_A="jhs:a";public static final String JHS_KEY_B="jhs:b";@Autowiredprivate RedisTemplate redisTemplate;/*** 偷個(gè)懶不加mybatis了,模擬從數(shù)據(jù)庫(kù)讀取100件特價(jià)商品,用于加載到聚劃算的頁(yè)面中* @return*/private List<Product> getProductsFromMysql() {List<Product> list=new ArrayList<>();for (int i = 1; i <=20; i++) {Random rand = new Random();int id= rand.nextInt(10000);Product obj=new Product((long) id,"product"+i,i,"detail");list.add(obj);}return list;}@PostConstructpublic void initJHS(){log.info("啟動(dòng)定時(shí)器淘寶聚劃算功能模擬.........."+ DateUtil.now());new Thread(() -> {//模擬定時(shí)器一個(gè)后臺(tái)任務(wù),定時(shí)把數(shù)據(jù)庫(kù)的特價(jià)商品,刷新到redis中while (true){//模擬從數(shù)據(jù)庫(kù)讀取100件特價(jià)商品,用于加載到聚劃算的頁(yè)面中List<Product> list=this.getProductsFromMysql();//采用redis list數(shù)據(jù)結(jié)構(gòu)的lpush來(lái)實(shí)現(xiàn)存儲(chǔ)this.redisTemplate.delete(JHS_KEY);//lpush命令this.redisTemplate.opsForList().leftPushAll(JHS_KEY,list);//間隔一分鐘 執(zhí)行一遍,模擬聚劃算每3天刷新一批次參加活動(dòng)try { TimeUnit.MINUTES.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }log.info("runJhs定時(shí)刷新..............");}},"t1").start();}
}
controller
import com.atguigu.redis7.entities.Product;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;@RestController
@Slf4j
@Api(tags = "聚劃算商品列表接口")
public class JHSProductController {public static final String JHS_KEY="jhs";@Autowiredprivate RedisTemplate redisTemplate;/*** 分頁(yè)查詢(xún):在高并發(fā)的情況下,只能走redis查詢(xún),走db的話(huà)必定會(huì)把db打垮* @param page* @param size* @return*/@RequestMapping(value = "/pruduct/find",method = RequestMethod.GET)@ApiOperation("按照分頁(yè)和每頁(yè)顯示容量,點(diǎn)擊查看")public List<Product> find(int page, int size) {List<Product> list=null;long start = (page - 1) * size;long end = start + size - 1;try {//采用redis list數(shù)據(jù)結(jié)構(gòu)的lrange命令實(shí)現(xiàn)分頁(yè)查詢(xún)list = this.redisTemplate.opsForList().range(JHS_KEY, start, end);if (CollectionUtils.isEmpty(list)) {//TODO 走DB查詢(xún)}log.info("查詢(xún)結(jié)果:{}", list);} catch (Exception ex) {//這里的異常,一般是redis癱瘓 ,或 redis網(wǎng)絡(luò)timeoutlog.error("exception:", ex);//TODO 走DB查詢(xún)}return list;}
}
至此步驟,上述聚劃算的功能算是完成,請(qǐng)思考在高并發(fā)下有什么經(jīng)典生產(chǎn)問(wèn)題?
答案:熱點(diǎn)k突然失效導(dǎo)致可怕的緩存擊穿,delete命令執(zhí)行的一瞬間有空隙,其它請(qǐng)求線(xiàn)程繼續(xù)找Redis為null,打到了mysql,暴擊…
最終目的:2條命令原子性還是其次,主要是防止熱key突然失效暴擊mysq打爆系統(tǒng)
加固代碼
采用差異失效時(shí)間
sevice
import cn.hutool.core.date.DateUtil;
import com.atguigu.redis7.entities.Product;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.TimeUnit;@Service
@Slf4j
public class JHSTaskService {public static final String JHS_KEY_A="jhs:a";public static final String JHS_KEY_B="jhs:b";@Autowiredprivate RedisTemplate redisTemplate;/*** 偷個(gè)懶不加mybatis了,模擬從數(shù)據(jù)庫(kù)讀取100件特價(jià)商品,用于加載到聚劃算的頁(yè)面中* @return*/private List<Product> getProductsFromMysql() {List<Product> list=new ArrayList<>();for (int i = 1; i <=20; i++) {Random rand = new Random();int id= rand.nextInt(10000);Product obj=new Product((long) id,"product"+i,i,"detail");list.add(obj);}return list;}@PostConstructpublic void initJHSAB(){log.info("啟動(dòng)AB定時(shí)器計(jì)劃任務(wù)淘寶聚劃算功能模擬.........."+DateUtil.now());new Thread(() -> {//模擬定時(shí)器,定時(shí)把數(shù)據(jù)庫(kù)的特價(jià)商品,刷新到redis中while (true){//模擬從數(shù)據(jù)庫(kù)讀取100件特價(jià)商品,用于加載到聚劃算的頁(yè)面中List<Product> list=this.getProductsFromMysql();//先更新B緩存this.redisTemplate.delete(JHS_KEY_B);this.redisTemplate.opsForList().leftPushAll(JHS_KEY_B,list);this.redisTemplate.expire(JHS_KEY_B,20L,TimeUnit.DAYS);//再更新A緩存this.redisTemplate.delete(JHS_KEY_A);this.redisTemplate.opsForList().leftPushAll(JHS_KEY_A,list);this.redisTemplate.expire(JHS_KEY_A,15L,TimeUnit.DAYS);//間隔一分鐘 執(zhí)行一遍try { TimeUnit.MINUTES.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }log.info("runJhs定時(shí)刷新雙緩存AB兩層..............");}},"t1").start();}
}
controller
import com.atguigu.redis7.entities.Product;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;@RestController
@Slf4j
@Api(tags = "聚劃算商品列表接口")
public class JHSProductController {public static final String JHS_KEY_A="jhs:a";public static final String JHS_KEY_B="jhs:b";@Autowiredprivate RedisTemplate redisTemplate;@RequestMapping(value = "/pruduct/findab",method = RequestMethod.GET)@ApiOperation("防止熱點(diǎn)key突然失效,AB雙緩存架構(gòu)")public List<Product> findAB(int page, int size) {List<Product> list=null;long start = (page - 1) * size;long end = start + size - 1;try {//采用redis list數(shù)據(jù)結(jié)構(gòu)的lrange命令實(shí)現(xiàn)分頁(yè)查詢(xún)list = this.redisTemplate.opsForList().range(JHS_KEY_A, start, end);if (CollectionUtils.isEmpty(list)) {log.info("=========A緩存已經(jīng)失效了,記得人工修補(bǔ),B緩存自動(dòng)延續(xù)5天");//用戶(hù)先查詢(xún)緩存A(上面的代碼),如果緩存A查詢(xún)不到(例如,更新緩存的時(shí)候刪除了),再查詢(xún)緩存Bthis.redisTemplate.opsForList().range(JHS_KEY_B, start, end);//TODO 走DB查詢(xún)}log.info("查詢(xún)結(jié)果:{}", list);} catch (Exception ex) {//這里的異常,一般是redis癱瘓 ,或 redis網(wǎng)絡(luò)timeoutlog.error("exception:", ex);//TODO 走DB查詢(xún)}return list;}
}