面試官:你知道緩存擊穿、緩存穿透、緩存雪崩嗎?
前言
又到了一年一度的金三銀四了,大家在面試的時候一定被問到過Redis緩存問題吧。可能有些初學者對“緩存擊穿、緩存穿透、緩存雪崩”這幾個名詞感到陌生,或者了解過但是一時半會沒辦法理解。沒關系,希望通過本文可以讓你輕松理解這些概念并掌握其解決方案,然后在即將到來的金三銀四面試中對你有所幫助。
面試題剖析
花里胡哨的名詞
剛開始我以為“緩存擊穿、緩存穿透、緩存雪崩”說的是3個問題,在各個博客以及視頻的講解下越來越繞。最后我捋了一下,這TM不是一個問題嗎。
為了讓大家也繞一繞,我把各博客對“緩存擊穿、緩存穿透、緩存雪崩”的描述貼在這里:
緩存擊穿是指一個熱點的Key在某個瞬間過期失效了,大量的并發請求在緩存獲取不到數據后直接請求數據庫的現象。
緩存穿透是指查詢一個根本不存在的數據,緩存和數據庫都不會命中,導致每次請求都要到數據庫去查詢。
緩存雪崩指的是緩存由于宕機或者某些原因不能提供服務,導致所有的請求去訪問數據庫,造成數據庫查詢壓力驟增從而宕機。
透過現象看本質
我就非常不理解了,為什么把緩存帶來的一個問題分好幾個場景去描述,還這解決方案,那解決方案的,花里胡哨的增加了大家的理解難度。
在我看來“緩存擊穿、緩存穿透,緩存雪崩”都是在說一個問題,那就是:
緩存沒命中,請求落到數據庫了
而“緩存雪崩”才突出了問題的本質:
沒有緩存的緩沖,數據庫承受不了那么大的壓力,可能會造成宕機等問題。
仔細想想是不是這樣?“緩存擊穿、緩存穿透、緩存雪崩”最終的描述都是請求落到數據庫了,只不過場景不同罷了。但不論哪種場景,在并發高的情況下都會給數據庫帶來壓力。
所以,一個問題分這么多場景,引出這么多名詞,我認為就是在增加大家的理解難度。
面試題解決方案
有問題就會有解決方案,既然看了這篇文章就不要死記硬背了,不然過段時間又會忘記,跟著思路順其自然的理解。
透過現象看本質
對于以上的幾個場景,要解決的問題就是:
如何提高緩存命中率。
也就是盡量避免請求打到數據庫中,尤其是高并發的請求。主要涉及兩個層面:
- 緩存組件要可靠:首先要確保緩存組件足夠可靠。
- 代碼邏輯要嚴謹:在編寫代碼使用緩存時盡量要把各種場景考慮進去,把問題當作功能的一部分。
像“緩存擊穿、緩存穿透”問題的產生都屬于代碼邏輯不嚴謹。熱點Key怎么能突然消失呢?一個相同的請求怎么能并發訪問到數據庫呢?怎么能允許一個不存在的數據一直請求呢?這些問題在我看來都是不應該發生的。
接下來就針對引起“緩存擊穿、緩存穿透、緩存雪崩”的幾個問題進行剖析解決。
提高緩存命中率一:完美處理熱點Key的消失
熱點數據通常分為可控和不可控。拿電商系統來講,商品分類屬于可控,因為基本上這類數據是通過后臺配置的。而一些商品可能會因為某個原因突然爆火成為熱點數據,這類數據屬于不可控。
不論可控或不可控,熱點數據不可以突然就消失,所以在緩存時要有對應的策略。
- 像商品分類這類數據就可以不設置過期時間。
- 而像不可控的熱點數據,要靠一些策略避免其過期,比如通過“看門狗”方式監控熱點Key,快過期時進行“續命”。
可以都不設置過期時間,讓淘汰策略去淘汰數據嗎?
非常不建議。
之前生產環境曾遇到過一個問題:用戶每次登錄之后會莫名其妙退出。經過排查發現,原來是因為Redis服務容量不足,所以最近登錄生成的token一直被淘汰。
雖然沒有報錯,但是給用戶帶來不好的體驗,對產品造成非常不好的影響。
當然,避免不了熱點Key被人為刪除或者其他惡意破壞,當發生這種情況怎么辦?
如果熱點Key不存在緩存中,勢必要去數據庫中查詢了。此時,如果并發請求過高,一定不能讓所有請求打到數據庫,可以對該key進行加鎖處理,獲取到鎖的請求去數據庫訪問并緩存,其他請求則等待該key緩存后再訪問緩存。
因為平時寫代碼會很自然考慮到這一點,所以這也是為什么我剛開始一直不理解“緩存擊穿”這樣的問題。
提高緩存命中率二:避免查詢不存在的數據
造成“查詢不存在的數據”的原因要么是代碼或數據出現問題,要么是遭到惡意的攻擊造成的空命中。總之,這種情況無法完全避免。
但是,我們知道哪些數據會被緩存。這樣的話,我們可以將這些數據放在一個“大集合”中,當請求的數據不存在這個“大集合”時,直接返回NULL即可。
那么問題來了:這個“大集合”放在哪里?肯定不能是數據庫,但是內存容量又是有限的。怎么辦?
有一個叫布隆過濾器的數據結構可以解決這個問題。其主要用于檢測一個元素是否在一個集合里,其原理是:數據通過一組哈希函數映射到位圖中,不論該元素多大都只需要占用1位,從而節省大量空間。如下圖
布隆過濾器原理
這樣的話,我就可以將要緩存的數據先放在布隆過濾器中,當查詢的數據不在布隆過濾器時就可以直接返回NULL了。
感興趣的可以看下 面試官:如何在海量數據中快速檢測某個數據
提高緩存命中率三:降低緩存服務的不可用
降低緩存服務的不可用也就是提高緩存服務的可用性,也就是Redis的高可用,這個沒有什么邏輯就不展開了。
面試題案例
模擬案例
現在,通過代碼模擬一個因“緩存擊穿、緩存穿透、緩存雪崩”,請求并發到MySQL服務上,看會發生什么事。
服務器環境:1核1G
編程語言: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("訪問緩存");
} 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個線程去查詢數據。cacheExist為true時訪問緩存,為false時去請求數據庫。
- 通過定時任務在1分鐘后將cacheExist設置為false。各位就想象成熱點Key的突然消失、查詢不存在的數據、redis的宕機。
案例執行效果
代碼在執行1分鐘后就會報下面的錯誤信息:
com.mysql.jdbc.exceptions.jdbc4.MySQLNonTransientConnectionException: Data source rejected establishment of connection, message from server: "Too many connections"
這是因為MySQL最大連接數只有151,遠遠低于并發線程數1500。
mysql> show variables like '%max_connections%';
+-----------------+-------+
| Variable_name | Value |
+-----------------+-------+
| max_connections | 151 |
+-----------------+-------+
此時,我將MySQL最大連接數設置為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 |
+-----------------+-------+
現在執行 SHOW STATUS LIKE 'Threads_connected' 去查看MySQL連接線程數會發現數值突然升高,當連接數為1283 左右時,就會發現MySQL服務已經斷開連接或者服務器宕機,也就是緩存雪崩的效果。
圖片
MySQL壓力過高宕機
總結
面試時不要被花里胡哨的問題迷惑住,要思考一下問題的本質。
“緩存擊穿、緩存穿透、緩存雪崩”問題的本質就是:
當緩存沒命中或失效,并發的請求打到數據庫怎么辦?
通過上面的描述,此類問題要有以下考慮:
- 提高緩存命中率。比如,要解決熱點Key的突然消失、要避免查詢不存在的數據等。
- 數據庫并發請求要設置合理。太低了浪費資源,太高了就會出現MySQL服務宕機情況。
本文轉載自微信公眾號「Hi程序員」,可以通過以下二維碼關注。轉載本文請聯系Hi程序員公眾號。