MySQL:緩存算什么東西?!
十年前,我們還是一個(gè)企業(yè)內(nèi)部的應(yīng)用,用戶不多,數(shù)據(jù)也不多。
Tomcat一天也處理不了多少請(qǐng)求,閑得無聊的時(shí)候只能和我聊天,這是沒有辦法的事情,因?yàn)檎麄€(gè)系統(tǒng)只有我們兩個(gè):
沒錯(cuò),我就是大名鼎鼎的MySQL ,我和Tomcat位于不同的機(jī)器上,每次通信都是一次網(wǎng)絡(luò)的請(qǐng)求。
這樣的情況持續(xù)了三年,我們倆把話都快要說盡了的時(shí)候,人類終于送來了一個(gè)新家伙:緩存。
從外表看來,這個(gè)緩存就是一個(gè)Map而已, 保存的都是一些(key,value)這樣的東西。
從內(nèi)部看,他還真是個(gè)Map,是那個(gè)叫做張大胖的人類寫的、一個(gè)線程安全的、可以設(shè)定過期時(shí)間的Map。
Tomcat和我都有點(diǎn)瞧不上他,覺得他實(shí)在是簡(jiǎn)陋,甚至難以成為一個(gè)獨(dú)立的組件。
更讓Tomcat不爽的是,這個(gè)簡(jiǎn)陋的家伙竟然和自己一起,共享JVM進(jìn)程。
慢慢地事情發(fā)生了變化,張大胖改變了程序邏輯:在之前,用戶的請(qǐng)求發(fā)到Tomcat這里,如果需要訪問數(shù)據(jù)庫的數(shù)據(jù), Tomcat直接就把SQL語句扔給我來執(zhí)行。
現(xiàn)在, 先要到那個(gè)Map,不,是緩存中查一下,看看有沒有相關(guān)數(shù)據(jù),如果有,直接就返回了,根本不用和我打交道; 如果緩存中沒有,那才發(fā)出SQL查詢,并且把緩存也給填充上,這樣下次就不用訪問數(shù)據(jù)庫了。
Tomcat整天和緩存打交道,聊得熱火朝天。 我觀察了幾天,終于明白這小子把我這個(gè)好基友給拋棄了。
Tomcat得意地對(duì)我說: “這緩存和我在一個(gè)進(jìn)程中,訪問起來速度快得很,立刻就能返回?cái)?shù)據(jù),哪里像你MySQL,慢慢悠悠地執(zhí)行半天?! ”
說完他又做了一個(gè)總結(jié):進(jìn)程內(nèi)調(diào)用就是好啊。
其實(shí)吧,緩存這小子的本質(zhì)我比誰都清楚,我內(nèi)部就有緩存啊,就是為了避免頻繁地訪問硬盤, 大家利用的都是程序的局部性原理嘛,有什么神秘的?!
我耐心蟄伏,等待機(jī)會(huì),準(zhǔn)備一舉把這個(gè)不知好歹的Map干掉。
從進(jìn)程內(nèi)到進(jìn)程外
過了幾個(gè)月,張大胖把系統(tǒng)的架構(gòu)做了升級(jí),為了應(yīng)對(duì)高并發(fā)的訪問,他用一個(gè)nginx來搞負(fù)載均衡,分發(fā)用戶的請(qǐng)求,在后面搞了很多Tomcat和很多進(jìn)程內(nèi)的緩存,我們的系統(tǒng)變成了這個(gè)樣子:
我一看就意識(shí)到我的機(jī)會(huì)來了:這緩存之間很容易出現(xiàn)不一致啊。
比如: 用戶的請(qǐng)求在JVM 1 中進(jìn)行處理,MySQL做了更新,JVM 1中相關(guān)的緩存也做了更新或者被刪除, 可是JVM 2和JVM 3中緩存的數(shù)據(jù)還是舊的啊。
不出我的所料,數(shù)據(jù)不一致的問題非常嚴(yán)重,用戶頻繁抱怨,緩存這小子這下要完蛋了!
可是緩存還想垂死掙扎,他說:”可以這樣嘛,如果一個(gè)JVM中的緩存發(fā)生了變化,就通知其他JVM。”
可是通知總會(huì)有延遲,如果JVM 1還沒來得及通知JVM 2和JVM 3, 而用戶的請(qǐng)求已經(jīng)在這兩臺(tái)機(jī)器上開始處理了,數(shù)據(jù)不一致還是存在。
特別是各個(gè)JVM之間需要來回交互,緩存的更新需要你通知我,我通知你,麻煩得要死。
Tomcat出了一個(gè)餿主意:“別讓緩存互相更新,讓緩存定時(shí)從MySQL那里更新!”
可是既然是定時(shí)更新,那緩存中的數(shù)據(jù)和我這里在某些時(shí)間段內(nèi)還是會(huì)出現(xiàn)不一致。
除非數(shù)據(jù)的變化頻率極低,否則這幾乎是個(gè)無解的問題。
終于,張大胖如我所愿, 把進(jìn)程內(nèi)緩存給刪除了!
我整打算好好跟Tomcat敘敘舊(這么多Tomcat啊!), 可是第二天他便弄來了一個(gè)新的家伙:Redis,還是緩存!
和之間那簡(jiǎn)陋的Map相比,Redis可是強(qiáng)大得太多太多了,這個(gè)緩存獨(dú)自霸占了一臺(tái)機(jī)器,讓幾個(gè)Tomcat都可以共享訪問。
換句話說,緩存從進(jìn)程內(nèi)搬到了進(jìn)程外!
我對(duì)Redis說:“你小子也需要網(wǎng)絡(luò)才能訪問了,和我差不多,有存在的必要嗎?”
Redis說:“當(dāng)然有了,雖然都是網(wǎng)絡(luò)訪問,但是我這里所有的數(shù)據(jù)可都在內(nèi)存中啊,訪問起來還是比你快。”
我承認(rèn),他說的是對(duì)的。
數(shù)據(jù)不一致
這天晚上,訪問量突然間特別的大,是平時(shí)的百倍,不,千倍。 據(jù)Redis說,這是張大胖那家伙在搞壓力測(cè)試了。
壓力測(cè)試過后,一地雞毛。 一盤點(diǎn)就發(fā)現(xiàn),Redis的數(shù)據(jù)和我的數(shù)據(jù)居然發(fā)現(xiàn)了不一致。
Redis傻眼了,這是怎么回事?數(shù)據(jù)不一致,人類肯定以我MySQL的數(shù)據(jù)庫數(shù)據(jù)為準(zhǔn)啊。
Tomcat提示Redis:“估計(jì)是高并發(fā)惹的禍,我們看看是怎么更新數(shù)據(jù)的。”
Redis說:“簡(jiǎn)單啊,先更新MySQL,然后更新我的數(shù)據(jù)。”
Tomcat說:“這是兩步操作,如果有兩個(gè)線程都在這么干,就出問題了! 比如MySQL的有個(gè)值是100,現(xiàn)在線程1想把它改成200, 線程2想把它改成300。”
Redis說:“看來這里有個(gè)大漏洞啊,那怎么辦?”
看著他們倆一籌莫展的樣子,我忍不住說道:“這還不簡(jiǎn)單,當(dāng)需要更新數(shù)據(jù)的時(shí)候,不要去更新緩存,把緩存中相關(guān)數(shù)據(jù)刪除就行了。”
Redis說:“你這是官報(bào)私仇吧,把數(shù)據(jù)從我這里刪除了,下次用戶訪問的時(shí)候沒有,還得找你去要,對(duì)不對(duì)?”
我說:“是得找我要,但是能解決你的問題啊,兩個(gè)線程同時(shí)寫,不會(huì)出現(xiàn)數(shù)據(jù)庫和緩存不一致啊。“
再說了,這其實(shí)不是我們能管的事情,咱們走著瞧,看看張大胖怎么做。”
第二天,張大胖果然按照我說的邏輯修改了程序,還美名其曰:Cache Aside Pattern。
雖然我一直想把緩存干掉,可是,幾天后的經(jīng)歷卻深刻地教育了我,緩存還是必不可少的......
(唉,再挖個(gè)坑,主題估計(jì)你也想到了,就是緩存穿透,擊穿,雪崩......)
【本文為51CTO專欄作者“劉欣”的原創(chuàng)稿件,轉(zhuǎn)載請(qǐng)通過作者微信公眾號(hào)coderising獲取授權(quán)】