把redis作為緩存使用已經(jīng)是司空見慣备恤,但是使用redis后也可能會(huì)碰到一系列的問題稿饰,尤其是數(shù)據(jù)量很大的時(shí)候锦秒,經(jīng)典的幾個(gè)問題如下:
(一)緩存和數(shù)據(jù)庫間數(shù)據(jù)一致性問題
分布式環(huán)境下(單機(jī)就不用說了)非常容易出現(xiàn)緩存和數(shù)據(jù)庫間的數(shù)據(jù)一致性問題,針對這一點(diǎn)的話喉镰,只能說旅择,如果你的項(xiàng)目對緩存的要求是強(qiáng)一致性的,那么請不要使用緩存侣姆。我們只能采取合適的策略來降低緩存和數(shù)據(jù)庫間數(shù)據(jù)不一致的概率生真,而無法保證兩者間的強(qiáng)一致性。合適的策略包括 合適的緩存更新策略捺宗,更新數(shù)據(jù)庫后要及時(shí)更新緩存柱蟀、緩存失敗時(shí)增加重試機(jī)制,例如MQ模式的消息隊(duì)列蚜厉。
(二)緩存擊穿問題
緩存擊穿表示惡意用戶模擬請求很多緩存中不存在的數(shù)據(jù)产弹,由于緩存中都沒有,導(dǎo)致這些請求短時(shí)間內(nèi)直接落在了數(shù)據(jù)庫上弯囊,導(dǎo)致數(shù)據(jù)庫異常痰哨。這個(gè)我們在實(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、接口限流與熔斷舷手、降級
重要的接口一定要做好限流策略,防止用戶惡意刷接口男窟,同時(shí)要降級準(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ū)別:
對比兩個(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)大量鍵過期(失效)盏档,接著來的一大波請求瞬間都落在了數(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ù)银受,留給各位看官自己思考探究。