談談服務端快取的幾種用法

iammutex發表於2015-04-22

快取是一個常談常新的話題,作為一名服務端的技術,如果你入行一年都還沒用過memcached類產品,那隻能說你的公司實在太小了,或者你乾的活實在太邊緣了。

說起快取,可能大家最直接想到的就是:“在資料庫前面擋一層”。這是快取最原始的意義,同時也引申出了快取最普遍的用法。

原始模式

程式碼示例1(原始模式):

//從快取中獲取資料[較快的方式]
data = getfromcache(id)
if data == null then
    //從資料庫中獲取資料[較慢的方式]
    data = getfromdb(id)
    //快取1天
    setintocache(id, data, 86400)
    return data
end

return data

快取加鎖

上面這種情況下,當同時有N個請求到達,都同時執行getfromcache,那麼都會發現data在快取中不存在,然後都會去呼叫getfromdb,以及setintocache。這是不必要的,那麼我們有沒有辦法減少這些併發呢。

最直接的想法是加鎖,當進入if條件中時,加一把鎖,讓其他程式不再執行下面的邏輯,而是等第一個程式的setintocache執行完成後,再重新執行一次getfromcache。

那這個鎖如何加呢?這裡推薦一種省時省力的方法。通過直接在快取value中設定過期時間來實現。

比如快取的value值為data,那我們修改一下,把它放到一個json中,改成

{data:data,atime:1429618765}

我們增加了一個atime來記錄快取生成的時間。而邏輯就變成下面這樣。

程式碼示例2(快取加鎖):

//從快取中獲取資料[較快的方式]
data = getfromcache(id)
data = json.decode(data)
//如果通過檢查快取生成時間,發現快取已經過於陳舊,那麼就將快取過期時間設定為現在開始的5分鐘以後(這樣其他併發程式就會以為此快取還未過期,還會繼續使用5分鐘,只讓當前這一個請求去重建快取)
if data != null && data.atime+86400 < now then
    data.atime = now+300-86400
    data = json.encode(data)
    //對真正的cache來說,快取10天或者更長時間
    setintocache(id, data, 864000)
    //這裡把data設定成null是為了走到下面的if中去重建快取
    data = null
end

if data == null then
    //從資料庫中獲取資料[較慢的方式]
    data = getfromdb(id)
    data = {data:data, atime:now}
    data = json.encode(data)
    //對真正的cache來說,快取10天或者更長時間
    setintocache(id, data, 864000)
    return data
end

return data

你可以會發現,這裡也會存在併發啊,和上面例1一樣,第一個getfromcache到setintocache之間,如果同時有N個請求到來,不還是都會執行這段操作,都會去查庫嗎。

沒錯,是這樣的。但是我們仔細看一下,例1中,從getfromcache到setintocache之間,經歷了一次漫長的getfromdb操作,這個時間耗費可能是上百毫秒的。而我們例2中,並沒有進行什麼操作,這個時間耗費只在毫秒甚至微秒級的。

所以例1中getfromcache到setintocache之間的併發是遠大於例2中的。例2中通過減小時間視窗,有效的模擬了鎖機制。同時還沒有增強額外的儲存複雜度。所以是推薦的一種方式。

可以說,我們所有的快取都應該是例2的方式,他在各方面都優於例1(多儲存的一個atime欄位耗費的記憶體基本可以忽略不計。且atime很多時候對於除錯程式還很有用)。

主動更新快取

那這樣就夠了嗎?對於被動過期型的快取,這樣基本就可以了。但是現實中還有一種快取,是主動更新的。試想有一種快取,我們要求必須和資料庫中的資料一致,不能出現陳舊資料。那麼上面的快取方式就不合適了。

我們必然會新增一個流程:即當資料庫有更新時,同時更新快取,因為快取會自己重建,也可以修改為當資料庫有更新時,同時刪除快取。

這裡提到刪除或者更新快取,就有點意思了。我們上面講到的都是非常簡單的快取,即一個id對應一個key。那麼試想,如果我們有一個分頁快取,快取了某一個文章最新的前10頁資料。分別的key是page_1,page_2…page10。

那麼當我們有一條新資料產生,這10頁就都失效了,需要更新或者刪除10次。這顯然是不太科學的做法。

那麼我們應該怎麼做呢。我們可以借用上面例2中的方法,例2中,我們在快取中增加了一個atime欄位,標識為快取的生成時間。我們既然知道快取什麼時候生成的,那問題就好解決了。我們在每次有新資料產生時,都去更新一個updatetime欄位。然後獲取分頁快取的時候,看一下這個updatetime欄位是不是在atime之後,如果是,那麼說明這份快取太舊了,需要走更新流程。

程式碼示例3(避免批量更新):

//從快取中獲取資料[較快的方式][這裡的兩次get普通的快取系統都支援一個請求完成]
data = getfromcache(id)
updatetime = getupdatetime(id)
data = json.decode(data)
//如果通過檢查快取生成時間,發現快取已經過於陳舊,那麼就將快取過期時間設定為現在開始的5分鐘以後(這樣其他併發程式就會以為此快取還未過期,還會繼續使用5分鐘,只讓當前這一個請求去重建快取)
if data != null && (data.atime+86400 < now || date.atime < updatetime) then
    data.atime = now+300-86400
    data = json.encode(data)
    //對真正的cache來說,快取10天或者更長時間
    setintocache(id, data, 864000)
    //這裡把data設定成null是為了走到下面的if中去重建快取
    data = null
end

if data == null then
    //從資料庫中獲取資料[較慢的方式]
    data = getfromdb(id)
    data = {data:data, atime:now}
    data = json.encode(data)
    //對真正的cache來說,快取10天或者更長時間
    setintocache(id, data, 864000)
    return data
end

return data

這僅僅是在程式碼示例2的基礎上增加了下面這一個條件判斷而已

date.atime < updatetime

這樣,無論是快取儲存時間過期了,還是快取本身有更新,都會觸發帶鎖機制的快取更新。

好了,先說到這裡,回頭有想起來的再做更新。原文地址


順便插播一則招聘廣告。(碼字不易,求別刪招聘廣告,謝!)

易手機座標深圳,做一款易用安全的老年智慧手機,做老年手機第一品牌。現在灰常需要服務端同學入夥。有興趣的同學請私信或簡歷發郵箱:ligang#pingyijinren.com

相關文章