【180409】本地緩存的選擇及其原理

概述

互聯(lián)網架構設計的五大要素:高性能禽笑、高可用入录、可伸縮性、可擴展性佳镜、安全纷跛。

如何做到高性能、高可用邀杏,緩存是一大助力贫奠。我們知道,絕大部分的時候望蜡,讀數據寫數據符合二八定律唤崭。并且讀數據中,百分之二十是數據被經常讀取(熱數據)脖律。那么我們解決這百分之二十的數據的方法就可以取得很好的一個性能谢肾。

以一句比較戲謔的話說說本質的東西:

互聯(lián)網架構設計中沒有什么不能通過一層抽象層(代理)解決的,如果有小泉,那就兩層芦疏。

他山之石

緩存分類

從很多互聯(lián)網架構設計中可以看到冕杠,從用戶在瀏覽器上輸入網址開始,經歷了太多的緩存酸茴。我大概列舉一下:

  1. 輸入網址后分预,查詢?yōu)g覽器緩存
  2. 查詢?yōu)g覽器dns緩存
  3. 查詢操作系統(tǒng)dns緩存
  4. 請求dns服務器,查詢dns服務器緩存
  5. 獲得ip薪捍,靜態(tài)資源走cdn緩存笼痹。動態(tài)數據走服務器
  6. 如果配置了頁面緩存,走頁面緩存
  7. 如果配置了本地緩存(localcache)酪穿,走本地緩存
  8. 如果配置了分布式緩存(如redis等等)凳干,走分布式緩存
  9. 數據庫操作,數據庫緩存

這里被济,我想主要講講2種localcache的選擇救赐,為什么要引入localcache,可以用《互聯(lián)網架構的三板斧》中的一句話來說明:

數據訪問熱點只磷,比如Detail中對某些熱點商品的訪問度非常高经磅,即使是Tair緩存這種Cache本身也有瓶頸問題,一旦請求量達到單機極限也會存在熱點保護問題喳瓣。有時看起來好像很容易解決,比如說做好限流就行赞别,但你想想一旦某個熱點觸發(fā)了一臺機器的限流閥值畏陕,那么這臺機器Cache的數據都將無效,進而間接導致Cache被擊穿仿滔,請求落地應用層數據庫出現雪崩現象惠毁。這類問題需要與具體Cache產品結合才能有比較好的解決方案,這里提供一個通用的解決思路崎页,就是在Cache的client端做本地Localcache鞠绰,當發(fā)現熱點數據時直接Cache在client里,而不要請求到Cache的Server飒焦。

還有一個原因是蜈膨,在做秒殺的時候,我可以在每一臺應用服務器中設置一個有失效時間的商品剩余數量的計數器牺荠,以達到盡可能在調用鏈前面攔截非有效請求翁巍。

分布式緩存

如何部署分布式緩存,這里不細說休雌。列一下我司的部署方式:


主從部署灶壶,app接入代理

這種方式是不太好擴機器。有一種比較好的方式是:一致性哈希算法杈曲。
可以參考這篇文章:他山之石中的《一致性哈希算法》

Localcache

在java中驰凛,localcache本質上是一個map胸懈,對應map中的每一個鍵值對,可以設置過期時間恰响,也可以通過如LRU(Least Recently Used 最近最少使用)做淘汰策略趣钱。

LRU算法實現原理參見他山之石中的《如何設計實現一個LRU Cache》。本質上是一個hashmap+雙向鏈表渔隶,每次訪問操作都將節(jié)點放在鏈表頭部羔挡,尾部自然就是最舊的節(jié)點了。


LRU算法原理

我對比了兩種LocalCache,google的Guava庫中的cache模塊Ehcache间唉。

Guava可以參考文章:他山之石中的《 [Google Guava] 3-緩存》
其基本原理為:ConcurrentMap(利用分段鎖降低鎖粒度) + LRU算法绞灼。

ConcurrentMap分段鎖原理參考《Java集合-ConcurrentHashMap原理分析》


ConcurrentMap分段鎖

分治思想。

Ehcache相關呈野,可以參考《ehcache基本原理》與《Spring+EhCache緩存實例》低矮。該緩存是一個比較重的localcache。有一個比較有意思的點是:支持磁盤緩存被冒,這樣媽媽再也不用擔心內存不夠用了>->军掂。

我的需求是:可以不用擔心內存不足的問題,做一些配置或不需要強一致性數據緩存昨悼。甚至可以做偽熱加載配置蝗锥。

因此,我選擇使用了Ehcache率触。如果只是做內存緩存终议,建議使用guava,有很多有意思的東西葱蝗,比如緩存失效穴张,自動從數據源加載數據等等。

Ehcache工具類

根據自身需要两曼,封裝了一個只走磁盤的無容量限制的localcache工具類皂甘,僅供參考,考慮到可以放在多個地方悼凑,因此沒有走配置文件

package com.fenqile.creditcard.appgatewaysale.provider.util;

import com.alibaba.fastjson.JSONObject;
import net.sf.ehcache.Cache;
import net.sf.ehcache.CacheManager;
import net.sf.ehcache.Element;
import net.sf.ehcache.config.CacheConfiguration;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.security.MessageDigest;

/**
 * User: Rudy Tan
 * Date: 2018/3/28
 *
 * 本地緩存工具偿枕,基于ehcache,磁盤存儲
 *
 * 可以運用于配置文件户辫、接口數據等等益老,
 *
 */
public class LocalCacheUtil {

    private static final CacheManager cacheManager = CacheManager.create();

    private static Logger LOG = LoggerFactory.getLogger(LocalCacheUtil.class);

    private static String md5(String str) {
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            byte[] bytes = md.digest(str.getBytes("utf-8"));

            final char[] HEX_DIGITS = "0123456789ABCDEF".toCharArray();
            StringBuilder ret = new StringBuilder(bytes.length * 2);
            for (int i=0; i<bytes.length; i++) {
                ret.append(HEX_DIGITS[(bytes[i] >> 4) & 0x0f]);
                ret.append(HEX_DIGITS[bytes[i] & 0x0f]);
            }
            return ret.toString();
        }
        catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    private static Cache getCacheInstance(){
        String cacheKey = "local_cache_"+ md5(Thread.currentThread().getStackTrace()[1].getClassName());
        if (!cacheManager.cacheExists(cacheKey)){
            synchronized (cacheManager){
                if (!cacheManager.cacheExists(cacheKey)){
                    CacheConfiguration cacheConfiguration =  new CacheConfiguration();
                    cacheConfiguration.setTimeToIdleSeconds(60);
                    cacheConfiguration.setTimeToLiveSeconds(60);
                    cacheConfiguration.setName(cacheKey);
                    cacheConfiguration.setMaxEntriesLocalHeap(1);
                    cacheConfiguration.setMaxEntriesLocalDisk(100000);
                    cacheConfiguration.setEternal(false);
                    cacheConfiguration.setOverflowToDisk(true);
                    cacheConfiguration.setMaxElementsInMemory(1);
                    cacheConfiguration.setCopyOnRead(true);
                    cacheConfiguration.setCopyOnWrite(true);

                    Cache cache = new Cache(cacheConfiguration);
                    cacheManager.addCache(cache);
                }
            }
        }
        return cacheManager.getCache(cacheKey);
    }

    private static Element serialization(String key, Object value, Integer expireTime){
        if (StringUtils.isEmpty(key)
                || null == expireTime
                || 0 == expireTime){
            return null;
        }

        String clazz = "";
        String content = "";
        if (null == value){
            clazz = "null";
            content = clazz + "_class&data_null";
        }else {
            clazz = value.getClass().getName();
            content = clazz + "_class&data_"+ JSONObject.toJSONString(value);
        }

        return new Element(key, content, expireTime, expireTime);
    }

    private static Object unSerialization(Element element){
        if (null == element){ return null; }

        String content = (String) element.getObjectValue();
        String[] data = content.split("_class&data_");
        Object response = null;
        try {
            if ("null".equalsIgnoreCase(data[0])){
                return null;
            }
            response = JSONObject.parseObject(data[1], Class.forName(data[0]));
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }

        return response;
    }


    /**
     * 設置本地緩存
     */
    public static boolean setCache(String key, Object value, Integer expireTime){
        Cache cache = getCacheInstance();
        if (null == cache){
            LOG.info("setCache:cache is null, {}, {}, {}", key, value, expireTime);
            return false;
        }

        if (StringUtils.isEmpty(key)
                || null == expireTime
                || 0 == expireTime){
            LOG.info("setCache:params is not ok, {}, {}, {}", key, value, expireTime);
            return false;
        }

        synchronized (cache){
            cache.put(serialization(key, value, expireTime));
            cache.flush();
        }
        return true;
    }

    /**
     * 獲取本地緩存
     */
    public static Object getCache(String key){
        Cache cache = getCacheInstance();
        if (null == cache
                || StringUtils.isEmpty(key)){
            LOG.info("getCache:params is not ok, {}", key);
            return null;
        }

        Element element = cache.get(key);
        return unSerialization(element);
    }

    /**
     * 清理本地緩存
     */
    public static boolean delCache(String key){
        Cache cache = getCacheInstance();
        if (null == cache){
            LOG.info("delCache:cache is null, {}", key);
            return true;
        }

        if (StringUtils.isEmpty(key)){
            LOG.info("delCache:params is not ok, {}", key);
            return true;
        }

        synchronized (cache){
            cache.put(serialization(key, null, 0));
            cache.flush();
        }
        return true;
    }
}

good luck.

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市寸莫,隨后出現的幾起案子捺萌,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,204評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件桃纯,死亡現場離奇詭異酷誓,居然都是意外死亡,警方通過查閱死者的電腦和手機态坦,發(fā)現死者居然都...
    沈念sama閱讀 93,091評論 3 395
  • 文/潘曉璐 我一進店門盐数,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人伞梯,你說我怎么就攤上這事玫氢。” “怎么了谜诫?”我有些...
    開封第一講書人閱讀 164,548評論 0 354
  • 文/不壞的土叔 我叫張陵漾峡,是天一觀的道長。 經常有香客問我喻旷,道長生逸,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,657評論 1 293
  • 正文 為了忘掉前任且预,我火速辦了婚禮槽袄,結果婚禮上,老公的妹妹穿的比我還像新娘锋谐。我一直安慰自己遍尺,他們只是感情好,可當我...
    茶點故事閱讀 67,689評論 6 392
  • 文/花漫 我一把揭開白布涮拗。 她就那樣靜靜地躺著乾戏,像睡著了一般。 火紅的嫁衣襯著肌膚如雪多搀。 梳的紋絲不亂的頭發(fā)上歧蕉,一...
    開封第一講書人閱讀 51,554評論 1 305
  • 那天灾部,我揣著相機與錄音康铭,去河邊找鬼。 笑死赌髓,一個胖子當著我的面吹牛从藤,可吹牛的內容都是我干的。 我是一名探鬼主播锁蠕,決...
    沈念sama閱讀 40,302評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼夷野,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了荣倾?” 一聲冷哼從身側響起悯搔,我...
    開封第一講書人閱讀 39,216評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎舌仍,沒想到半個月后妒貌,有當地人在樹林里發(fā)現了一具尸體通危,經...
    沈念sama閱讀 45,661評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,851評論 3 336
  • 正文 我和宋清朗相戀三年灌曙,在試婚紗的時候發(fā)現自己被綠了菊碟。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,977評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡在刺,死狀恐怖逆害,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情蚣驼,我是刑警寧澤魄幕,帶...
    沈念sama閱讀 35,697評論 5 347
  • 正文 年R本政府宣布,位于F島的核電站隙姿,受9級特大地震影響梅垄,放射性物質發(fā)生泄漏。R本人自食惡果不足惜输玷,卻給世界環(huán)境...
    茶點故事閱讀 41,306評論 3 330
  • 文/蒙蒙 一队丝、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧欲鹏,春花似錦机久、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,898評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至尤误,卻和暖如春侠畔,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背损晤。 一陣腳步聲響...
    開封第一講書人閱讀 33,019評論 1 270
  • 我被黑心中介騙來泰國打工软棺, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人尤勋。 一個月前我還...
    沈念sama閱讀 48,138評論 3 370
  • 正文 我出身青樓喘落,卻偏偏與公主長得像,于是被迫代替她去往敵國和親最冰。 傳聞我的和親對象是個殘疾皇子瘦棋,可洞房花燭夜當晚...
    茶點故事閱讀 44,927評論 2 355

推薦閱讀更多精彩內容

  • 緩存在日常開發(fā)中舉足輕重,如果你的應用對某類數據有著較高的讀取頻次暖哨,并且改動較小時那就非常適合利用緩存來提高性能赌朋。...
    tracy_668閱讀 4,306評論 0 9
  • 使用本地緩存需要注意兩個問題: 內存管理,及時解除無用對象的引用。防止大量無用對象進入old區(qū)沛慢,引發(fā) full g...
    Zal哥哥閱讀 1,635評論 1 1
  • 久違的晴天服球,家長會。 家長大會開好到教室時颠焦,離放學已經沒多少時間了斩熊。班主任說已經安排了三個家長分享經驗。 放學鈴聲...
    飄雪兒5閱讀 7,523評論 16 22
  • 今天感恩節(jié)哎伐庭,感謝一直在我身邊的親朋好友粉渠。感恩相遇!感恩不離不棄圾另。 中午開了第一次的黨會霸株,身份的轉變要...
    迷月閃星情閱讀 10,566評論 0 11
  • 可愛進取,孤獨成精集乔。努力飛翔去件,天堂翱翔。戰(zhàn)爭美好扰路,孤獨進取尤溜。膽大飛翔,成就輝煌汗唱。努力進取宫莱,遙望,和諧家園哩罪∈诎裕可愛游走...
    趙原野閱讀 2,727評論 1 1