把redis作為緩存使用已經(jīng)是司空見慣整袁,但是使用redis后也可能會(huì)碰到一系列的問題菠齿,尤其是數(shù)據(jù)量很大的時(shí)候,經(jīng)典的幾個(gè)問題如下:
(一)緩存和數(shù)據(jù)庫間數(shù)據(jù)一致性問題
分布式環(huán)境下(單機(jī)就不用說了)非常容易出現(xiàn)緩存和數(shù)據(jù)庫間的數(shù)據(jù)一致性問題坐昙,針對(duì)這一點(diǎn)的話绳匀,只能說,如果你的項(xiàng)目對(duì)緩存的要求是強(qiáng)一致性的炸客,那么請(qǐng)不要使用緩存疾棵。我們只能采取合適的策略來降低緩存和數(shù)據(jù)庫間數(shù)據(jù)不一致的概率,而無法保證兩者間的強(qiáng)一致性痹仙。合適的策略包括 合適的緩存更新策略是尔,更新數(shù)據(jù)庫后要及時(shí)更新緩存、緩存失敗時(shí)增加重試機(jī)制开仰,例如MQ模式的消息隊(duì)列拟枚。
(二)緩存擊穿問題
緩存擊穿表示惡意用戶模擬請(qǐng)求很多緩存中不存在的數(shù)據(jù),由于緩存中都沒有抖所,導(dǎo)致這些請(qǐng)求短時(shí)間內(nèi)直接落在了數(shù)據(jù)庫上梨州,導(dǎo)致數(shù)據(jù)庫異常。這個(gè)我們?cè)趯?shí)際項(xiàng)目就遇到了田轧,有些搶購活動(dòng)暴匠、秒殺活動(dòng)的接口API被大量的惡意用戶刷,導(dǎo)致短時(shí)間內(nèi)數(shù)據(jù)庫c超時(shí)了傻粘,好在數(shù)據(jù)庫是讀寫分離每窖,同時(shí)也有進(jìn)行接口限流,hold住了弦悉。
解決方案的話:
方案1窒典、使用互斥鎖排隊(duì)
業(yè)界比價(jià)普遍的一種做法,即根據(jù)key獲取value值為空時(shí)稽莉,鎖上瀑志,從數(shù)據(jù)庫中l(wèi)oad數(shù)據(jù)后再釋放鎖。若其它線程獲取鎖失敗,則等待一段時(shí)間后重試劈猪。這里要注意昧甘,分布式環(huán)境中要使用分布式鎖,單機(jī)的話用普通的鎖(synchronized战得、Lock)就夠了充边。
public String getWithLock(String key, Jedis jedis, String lockKey, String uniqueId, long expireTime) {
// 通過key獲取value
String value = redisService.get(key);
if (StringUtil.isEmpty(value)) {
// 分布式鎖,詳細(xì)可以參考https://blog.csdn.net/fanrenxiang/article/details/79803037
//封裝的tryDistributedLock包括setnx和expire兩個(gè)功能常侦,在低版本的redis中不支持
try {
boolean locked = redisService.tryDistributedLock(jedis, lockKey, uniqueId, expireTime);
if (locked) {
value = userService.getById(key);
redisService.set(key, value);
redisService.del(lockKey);
return value;
} else {
// 其它線程進(jìn)來了沒獲取到鎖便等待50ms后重試
Thread.sleep(50);
getWithLock(key, jedis, lockKey, uniqueId, expireTime);
}
} catch (Exception e) {
log.error("getWithLock exception=" + e);
return value;
} finally {
redisService.releaseDistributedLock(jedis, lockKey, uniqueId);
}
}
return value;
}
這樣做思路比較清晰浇冰,也從一定程度上減輕數(shù)據(jù)庫壓力,但是鎖機(jī)制使得邏輯的復(fù)雜度增加聋亡,吞吐量也降低了肘习,有點(diǎn)治標(biāo)不治本。
方案2杀捻、接口限流與熔斷井厌、降級(jí)
重要的接口一定要做好限流策略,防止用戶惡意刷接口致讥,同時(shí)要降級(jí)準(zhǔn)備仅仆,當(dāng)接口中的某些服務(wù)不可用時(shí)候,進(jìn)行熔斷垢袱,失敗快速返回機(jī)制墓拜。
方案3、布隆過濾器
bloomfilter就類似于一個(gè)hash set请契,用于快速判某個(gè)元素是否存在于集合中咳榜,其典型的應(yīng)用場景就是快速判斷一個(gè)key是否存在于某容器,不存在就直接返回爽锥。布隆過濾器的關(guān)鍵就在于hash算法和容器大小涌韩,下面先來簡單的實(shí)現(xiàn)下看看效果,我這里用guava實(shí)現(xiàn)的布隆過濾器:
<dependencies>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>23.0</version>
</dependency>
</dependencies>
public class BloomFilterTest {
private static final int capacity = 1000000;
private static final int key = 999998;
private static BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), capacity);
static {
for (int i = 0; i < capacity; i++) {
bloomFilter.put(i);
}
}
public static void main(String[] args) {
/*返回計(jì)算機(jī)最精確的時(shí)間氯夷,單位微妙*/
long start = System.nanoTime();
if (bloomFilter.mightContain(key)) {
System.out.println("成功過濾到" + key);
}
long end = System.nanoTime();
System.out.println("布隆過濾器消耗時(shí)間:" + (end - start));
int sum = 0;
for (int i = capacity + 20000; i < capacity + 30000; i++) {
if (bloomFilter.mightContain(i)) {
sum = sum + 1;
}
}
System.out.println("錯(cuò)判率為:" + sum);
}
}
成功過濾到999998
布隆過濾器消耗時(shí)間:215518
錯(cuò)判率為:318
可以看到臣樱,100w個(gè)數(shù)據(jù)中只消耗了約0.2毫秒就匹配到了key,速度足夠快腮考。然后模擬了1w個(gè)不存在于布隆過濾器中的key雇毫,匹配錯(cuò)誤率為318/10000,也就是說踩蔚,出錯(cuò)率大概為3%棚放,跟蹤下BloomFilter的源碼發(fā)現(xiàn)默認(rèn)的容錯(cuò)率就是0.03:
public static <T> BloomFilter<T> create(Funnel<T> funnel, int expectedInsertions /* n */) {
return create(funnel, expectedInsertions, 0.03); // FYI, for 3%, we always get 5 hash functions
}
我們可調(diào)用BloomFilter的這個(gè)方法顯式的指定誤判率:
private static BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), capacity,0.01);
我們斷點(diǎn)跟蹤下,誤判率為0.02和默認(rèn)的0.03時(shí)候的區(qū)別:
對(duì)比兩個(gè)出錯(cuò)率可以發(fā)現(xiàn)馅闽,誤判率為0.02時(shí)數(shù)組大小為8142363飘蚯,0.03時(shí)為7298440馍迄,誤判率降低了0.01,BloomFilter維護(hù)的數(shù)組大小也減少了843923孝冒,可見BloomFilter默認(rèn)的誤判率0.03是設(shè)計(jì)者權(quán)衡系統(tǒng)性能后得出的值柬姚。要注意的是,布隆過濾器不支持刪除操作庄涡。用在這邊解決緩存穿透問題就是:
public String getByKey(String key) {
// 通過key獲取value
String value = redisService.get(key);
if (StringUtil.isEmpty(value)) {
if (bloomFilter.mightContain(key)) {
value = userService.getById(key);
redisService.set(key, value);
return value;
} else {
return null;
}
}
return value;
}
(三)緩存雪崩問題
緩存在同一時(shí)間內(nèi)大量鍵過期(失效),接著來的一大波請(qǐng)求瞬間都落在了數(shù)據(jù)庫中導(dǎo)致連接異常搬设。
解決方案:
方案1穴店、也是像解決緩存穿透一樣加鎖排隊(duì),實(shí)現(xiàn)同上;
方案2拿穴、建立備份緩存泣洞,緩存A和緩存B,A設(shè)置超時(shí)時(shí)間默色,B不設(shè)值超時(shí)時(shí)間球凰,先從A讀緩存,A沒有讀B腿宰,并且更新A緩存和B緩存;
方案3呕诉、設(shè)置緩存超時(shí)時(shí)間的時(shí)候加上一個(gè)隨機(jī)的時(shí)間長度,比如這個(gè)緩存key的超時(shí)時(shí)間是固定的5分鐘加上隨機(jī)的2分鐘吃度,醬紫可從一定程度上避免雪崩問題甩挫;
public String getByKey(String keyA,String keyB) {
String value = redisService.get(keyA);
if (StringUtil.isEmpty(value)) {
value = redisService.get(keyB);
String newValue = getFromDbById();
redisService.set(keyA,newValue,31, TimeUnit.DAYS);
redisService.set(keyB,newValue);
}
return value;
}
(四)緩存并發(fā)問題
這里的并發(fā)指的是多個(gè)redis的client同時(shí)set key引起的并發(fā)問題。其實(shí)redis自身就是單線程操作椿每,多個(gè)client并發(fā)操作伊者,按照先到先執(zhí)行的原則,先到的先執(zhí)行间护,其余的阻塞亦渗。當(dāng)然,另外的解決方案是把redis.set操作放在隊(duì)列中使其串行化汁尺,必須的一個(gè)一個(gè)執(zhí)行法精,具體的代碼就不上了,當(dāng)然加鎖也是可以的均函,至于為什么不用redis中的事務(wù)亿虽,留給各位看官自己思考探究。
同時(shí)需要更多java相關(guān)資料以及面試心得和視頻資料的苞也,歡迎加QQ群:810589193
免費(fèi)獲取Java工程化洛勉、高性能及分布式、高性能如迟、高架構(gòu)收毫、性能調(diào)優(yōu)攻走、Spring、MyBatis此再、Netty源碼分析等多個(gè)知識(shí)點(diǎn)高級(jí)進(jìn)階干貨的直播免費(fèi)學(xué)習(xí)權(quán)限及相關(guān)視頻資料昔搂,還有spring和虛擬機(jī)等書籍掃描版