面試官:你知道緩存擊穿、緩存穿透、緩存雪崩嗎?
前言
又到了一年一度的金三銀四了,大家在面試的時(shí)候一定被問(wèn)到過(guò)Redis緩存問(wèn)題吧。可能有些初學(xué)者對(duì)“緩存擊穿、緩存穿透、緩存雪崩”這幾個(gè)名詞感到陌生,或者了解過(guò)但是一時(shí)半會(huì)沒(méi)辦法理解。沒(méi)關(guān)系,希望通過(guò)本文可以讓你輕松理解這些概念并掌握其解決方案,然后在即將到來(lái)的金三銀四面試中對(duì)你有所幫助。
面試題剖析
花里胡哨的名詞
剛開(kāi)始我以為“緩存擊穿、緩存穿透、緩存雪崩”說(shuō)的是3個(gè)問(wèn)題,在各個(gè)博客以及視頻的講解下越來(lái)越繞。最后我捋了一下,這TM不是一個(gè)問(wèn)題嗎。
為了讓大家也繞一繞,我把各博客對(duì)“緩存擊穿、緩存穿透、緩存雪崩”的描述貼在這里:
緩存擊穿是指一個(gè)熱點(diǎn)的Key在某個(gè)瞬間過(guò)期失效了,大量的并發(fā)請(qǐng)求在緩存獲取不到數(shù)據(jù)后直接請(qǐng)求數(shù)據(jù)庫(kù)的現(xiàn)象。
緩存穿透是指查詢(xún)一個(gè)根本不存在的數(shù)據(jù),緩存和數(shù)據(jù)庫(kù)都不會(huì)命中,導(dǎo)致每次請(qǐng)求都要到數(shù)據(jù)庫(kù)去查詢(xún)。
緩存雪崩指的是緩存由于宕機(jī)或者某些原因不能提供服務(wù),導(dǎo)致所有的請(qǐng)求去訪(fǎng)問(wèn)數(shù)據(jù)庫(kù),造成數(shù)據(jù)庫(kù)查詢(xún)壓力驟增從而宕機(jī)。
透過(guò)現(xiàn)象看本質(zhì)
我就非常不理解了,為什么把緩存帶來(lái)的一個(gè)問(wèn)題分好幾個(gè)場(chǎng)景去描述,還這解決方案,那解決方案的,花里胡哨的增加了大家的理解難度。
在我看來(lái)“緩存擊穿、緩存穿透,緩存雪崩”都是在說(shuō)一個(gè)問(wèn)題,那就是:
緩存沒(méi)命中,請(qǐng)求落到數(shù)據(jù)庫(kù)了
而“緩存雪崩”才突出了問(wèn)題的本質(zhì):
沒(méi)有緩存的緩沖,數(shù)據(jù)庫(kù)承受不了那么大的壓力,可能會(huì)造成宕機(jī)等問(wèn)題。
仔細(xì)想想是不是這樣?“緩存擊穿、緩存穿透、緩存雪崩”最終的描述都是請(qǐng)求落到數(shù)據(jù)庫(kù)了,只不過(guò)場(chǎng)景不同罷了。但不論哪種場(chǎng)景,在并發(fā)高的情況下都會(huì)給數(shù)據(jù)庫(kù)帶來(lái)壓力。
所以,一個(gè)問(wèn)題分這么多場(chǎng)景,引出這么多名詞,我認(rèn)為就是在增加大家的理解難度。
面試題解決方案
有問(wèn)題就會(huì)有解決方案,既然看了這篇文章就不要死記硬背了,不然過(guò)段時(shí)間又會(huì)忘記,跟著思路順其自然的理解。
透過(guò)現(xiàn)象看本質(zhì)
對(duì)于以上的幾個(gè)場(chǎng)景,要解決的問(wèn)題就是:
如何提高緩存命中率。
也就是盡量避免請(qǐng)求打到數(shù)據(jù)庫(kù)中,尤其是高并發(fā)的請(qǐng)求。主要涉及兩個(gè)層面:
- 緩存組件要可靠:首先要確保緩存組件足夠可靠。
- 代碼邏輯要嚴(yán)謹(jǐn):在編寫(xiě)代碼使用緩存時(shí)盡量要把各種場(chǎng)景考慮進(jìn)去,把問(wèn)題當(dāng)作功能的一部分。
像“緩存擊穿、緩存穿透”問(wèn)題的產(chǎn)生都屬于代碼邏輯不嚴(yán)謹(jǐn)。熱點(diǎn)Key怎么能突然消失呢?一個(gè)相同的請(qǐng)求怎么能并發(fā)訪(fǎng)問(wèn)到數(shù)據(jù)庫(kù)呢?怎么能允許一個(gè)不存在的數(shù)據(jù)一直請(qǐng)求呢?這些問(wèn)題在我看來(lái)都是不應(yīng)該發(fā)生的。
接下來(lái)就針對(duì)引起“緩存擊穿、緩存穿透、緩存雪崩”的幾個(gè)問(wèn)題進(jìn)行剖析解決。
提高緩存命中率一:完美處理熱點(diǎn)Key的消失
熱點(diǎn)數(shù)據(jù)通常分為可控和不可控。拿電商系統(tǒng)來(lái)講,商品分類(lèi)屬于可控,因?yàn)榛旧线@類(lèi)數(shù)據(jù)是通過(guò)后臺(tái)配置的。而一些商品可能會(huì)因?yàn)槟硞€(gè)原因突然爆火成為熱點(diǎn)數(shù)據(jù),這類(lèi)數(shù)據(jù)屬于不可控。
不論可控或不可控,熱點(diǎn)數(shù)據(jù)不可以突然就消失,所以在緩存時(shí)要有對(duì)應(yīng)的策略。
- 像商品分類(lèi)這類(lèi)數(shù)據(jù)就可以不設(shè)置過(guò)期時(shí)間。
- 而像不可控的熱點(diǎn)數(shù)據(jù),要靠一些策略避免其過(guò)期,比如通過(guò)“看門(mén)狗”方式監(jiān)控?zé)狳c(diǎn)Key,快過(guò)期時(shí)進(jìn)行“續(xù)命”。
可以都不設(shè)置過(guò)期時(shí)間,讓淘汰策略去淘汰數(shù)據(jù)嗎?
非常不建議。
之前生產(chǎn)環(huán)境曾遇到過(guò)一個(gè)問(wèn)題:用戶(hù)每次登錄之后會(huì)莫名其妙退出。經(jīng)過(guò)排查發(fā)現(xiàn),原來(lái)是因?yàn)镽edis服務(wù)容量不足,所以最近登錄生成的token一直被淘汰。
雖然沒(méi)有報(bào)錯(cuò),但是給用戶(hù)帶來(lái)不好的體驗(yàn),對(duì)產(chǎn)品造成非常不好的影響。
當(dāng)然,避免不了熱點(diǎn)Key被人為刪除或者其他惡意破壞,當(dāng)發(fā)生這種情況怎么辦?
如果熱點(diǎn)Key不存在緩存中,勢(shì)必要去數(shù)據(jù)庫(kù)中查詢(xún)了。此時(shí),如果并發(fā)請(qǐng)求過(guò)高,一定不能讓所有請(qǐng)求打到數(shù)據(jù)庫(kù),可以對(duì)該key進(jìn)行加鎖處理,獲取到鎖的請(qǐng)求去數(shù)據(jù)庫(kù)訪(fǎng)問(wèn)并緩存,其他請(qǐng)求則等待該key緩存后再訪(fǎng)問(wèn)緩存。
因?yàn)槠綍r(shí)寫(xiě)代碼會(huì)很自然考慮到這一點(diǎn),所以這也是為什么我剛開(kāi)始一直不理解“緩存擊穿”這樣的問(wèn)題。
提高緩存命中率二:避免查詢(xún)不存在的數(shù)據(jù)
造成“查詢(xún)不存在的數(shù)據(jù)”的原因要么是代碼或數(shù)據(jù)出現(xiàn)問(wèn)題,要么是遭到惡意的攻擊造成的空命中。總之,這種情況無(wú)法完全避免。
但是,我們知道哪些數(shù)據(jù)會(huì)被緩存。這樣的話(huà),我們可以將這些數(shù)據(jù)放在一個(gè)“大集合”中,當(dāng)請(qǐng)求的數(shù)據(jù)不存在這個(gè)“大集合”時(shí),直接返回NULL即可。
那么問(wèn)題來(lái)了:這個(gè)“大集合”放在哪里?肯定不能是數(shù)據(jù)庫(kù),但是內(nèi)存容量又是有限的。怎么辦?
有一個(gè)叫布隆過(guò)濾器的數(shù)據(jù)結(jié)構(gòu)可以解決這個(gè)問(wèn)題。其主要用于檢測(cè)一個(gè)元素是否在一個(gè)集合里,其原理是:數(shù)據(jù)通過(guò)一組哈希函數(shù)映射到位圖中,不論該元素多大都只需要占用1位,從而節(jié)省大量空間。如下圖
布隆過(guò)濾器原理
這樣的話(huà),我就可以將要緩存的數(shù)據(jù)先放在布隆過(guò)濾器中,當(dāng)查詢(xún)的數(shù)據(jù)不在布隆過(guò)濾器時(shí)就可以直接返回NULL了。
感興趣的可以看下 面試官:如何在海量數(shù)據(jù)中快速檢測(cè)某個(gè)數(shù)據(jù)
提高緩存命中率三:降低緩存服務(wù)的不可用
降低緩存服務(wù)的不可用也就是提高緩存服務(wù)的可用性,也就是Redis的高可用,這個(gè)沒(méi)有什么邏輯就不展開(kāi)了。
面試題案例
模擬案例
現(xiàn)在,通過(guò)代碼模擬一個(gè)因“緩存擊穿、緩存穿透、緩存雪崩”,請(qǐng)求并發(fā)到MySQL服務(wù)上,看會(huì)發(fā)生什么事。
服務(wù)器環(huán)境:1核1G
編程語(yǔ)言:Java
案例代碼
public class MainTest {
private static final String DB_URL = "jdbc:mysql://127.0.0.1:3306/test";
private static final String USER = "root";
private static final String PASS = "Mysql123.";
public static void main(String[] args) throws InterruptedException {
Timer timer = new Timer();
TimerTask task = new TimerTask() {
@Override
public void run() {
QueryTask.cacheExist = false;
}
};
timer.schedule(task, 60 * 1000);
while (true) {
ExecutorService executorService = Executors.newFixedThreadPool(1500);
for (int i = 0; i <1500 ; i++) {
executorService.submit(new MainTest.QueryTask());
System.gc();
}
}
}
static class QueryTask implements Runnable {
static boolean cacheExist = true;
@Override
public void run() {
try {
if (cacheExist) {
System.out.println("訪(fǎng)問(wèn)緩存");
} else {
Class.forName("com.mysql.jdbc.Driver");
Connection conn = DriverManager.getConnection(DB_URL, USER, PASS);
Statement statement = conn.createStatement();
Thread.sleep(3000);
String query = "SELECT * FROM test_cache";
ResultSet rs = statement.executeQuery(query);
while (rs.next()) {
int id = rs.getInt("id");
String value = rs.getString("value");
System.out.println("ID: " + id + ", Value: " + value);
}
rs.close();
statement.close();
conn.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
上面的代碼主要做了兩件事:
- 模擬1500個(gè)線(xiàn)程去查詢(xún)數(shù)據(jù)。cacheExist為true時(shí)訪(fǎng)問(wèn)緩存,為false時(shí)去請(qǐng)求數(shù)據(jù)庫(kù)。
- 通過(guò)定時(shí)任務(wù)在1分鐘后將cacheExist設(shè)置為false。各位就想象成熱點(diǎn)Key的突然消失、查詢(xún)不存在的數(shù)據(jù)、redis的宕機(jī)。
案例執(zhí)行效果
代碼在執(zhí)行1分鐘后就會(huì)報(bào)下面的錯(cuò)誤信息:
com.mysql.jdbc.exceptions.jdbc4.MySQLNonTransientConnectionException: Data source rejected establishment of connection, message from server: "Too many connections"
這是因?yàn)镸ySQL最大連接數(shù)只有151,遠(yuǎn)遠(yuǎn)低于并發(fā)線(xiàn)程數(shù)1500。
mysql> show variables like '%max_connections%';
+-----------------+-------+
| Variable_name | Value |
+-----------------+-------+
| max_connections | 151 |
+-----------------+-------+
此時(shí),我將MySQL最大連接數(shù)設(shè)置為1500。
mysql> SET GLOBAL max_connections = 1500;
Query OK, 0 rows affected (0.00 sec)
mysql> show variables like '%max_connections%';
+-----------------+-------+
| Variable_name | Value |
+-----------------+-------+
| max_connections | 1500 |
+-----------------+-------+
現(xiàn)在執(zhí)行 SHOW STATUS LIKE 'Threads_connected' 去查看MySQL連接線(xiàn)程數(shù)會(huì)發(fā)現(xiàn)數(shù)值突然升高,當(dāng)連接數(shù)為1283 左右時(shí),就會(huì)發(fā)現(xiàn)MySQL服務(wù)已經(jīng)斷開(kāi)連接或者服務(wù)器宕機(jī),也就是緩存雪崩的效果。
圖片
MySQL壓力過(guò)高宕機(jī)
總結(jié)
面試時(shí)不要被花里胡哨的問(wèn)題迷惑住,要思考一下問(wèn)題的本質(zhì)。
“緩存擊穿、緩存穿透、緩存雪崩”問(wèn)題的本質(zhì)就是:
當(dāng)緩存沒(méi)命中或失效,并發(fā)的請(qǐng)求打到數(shù)據(jù)庫(kù)怎么辦?
通過(guò)上面的描述,此類(lèi)問(wèn)題要有以下考慮:
- 提高緩存命中率。比如,要解決熱點(diǎn)Key的突然消失、要避免查詢(xún)不存在的數(shù)據(jù)等。
- 數(shù)據(jù)庫(kù)并發(fā)請(qǐng)求要設(shè)置合理。太低了浪費(fèi)資源,太高了就會(huì)出現(xiàn)MySQL服務(wù)宕機(jī)情況。
本文轉(zhuǎn)載自微信公眾號(hào)「Hi程序員」,可以通過(guò)以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系Hi程序員公眾號(hào)。