11 多級(jí)緩存 Caffeine + Redis

緩存

一般來(lái)說(shuō)緩存分:本地緩存描滔、分布式緩存蔬顾。

本地緩存有:Guava、Caffeine等昭齐;分布式緩存大部分由NoSql數(shù)據(jù)庫(kù)組成:redis尿招、memcache等。

本地緩存最大的優(yōu)勢(shì)是沒有網(wǎng)絡(luò)的限制阱驾、穩(wěn)定就谜、速度快,缺點(diǎn)是容量上限小里覆,同時(shí)只能供單體應(yīng)用使用丧荐。

我們先分別簡(jiǎn)單試一下本地緩存和分布式緩存,然后嘗試寫一個(gè)兼容兩者的版本喧枷。

緩存邏輯

我們對(duì)dao層進(jìn)行切面虹统,大致的思路如下:

  1. 針對(duì)dao層每個(gè)類每個(gè)方法做切面。
  2. Before隧甚,判斷SqlCommandType
    • 如果是SELECT车荔,則嘗試取緩存。
    • 否則清除緩存呻逆。
    • 清除緩存時(shí)夸赫,順便把相關(guān)表下的所有key清除。
  3. SELECT或取不到緩存就執(zhí)行dao
  4. after咖城,判斷操作是:
    • 如果是SELECT,且沒有緩存呼奢,則緩存當(dāng)前方法及返回值宜雀。
    • 保存緩存時(shí),順便把相關(guān)表和key的映射放入緩存

邏輯實(shí)現(xiàn)

MyCacheInterceptor

攔截器握础,包含了大部分的邏輯辐董。

package com.it_laowu.springbootstudy.springbootstudydemo.core.cache;
......
@Aspect
@Component
public class MyCacheInterceptor {
    @Autowired
    SqlSessionFactory sqlSessionFactory;
    @Autowired
    MyCacheFactory myCacheFactory;
    
    //execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern)throws-pattern?)
    @Pointcut("execution(public * com.it_laowu.springbootstudy.springbootstudydemo.dao.*.*(..))")
    private void anyMethod(){}

    @Around("anyMethod()")
    public Object aroundDao(ProceedingJoinPoint pjp) throws Throwable {
        CacheKeyInfo cacheKeyInfo = getCacheKeyInfo(pjp);
        Pair<Boolean, Object> ret = beforeProceed(cacheKeyInfo);
        Object retData;
        if (ret.getFirst()){ 
            retData = ret.getSecond();
        }
        else{
            retData=pjp.proceed();
        }
        afterProceed(cacheKeyInfo,retData,ret.getFirst());
        return retData;
    }

    private void afterProceed(CacheKeyInfo cacheKeyInfo, Object retData,boolean exists) {
        Console.log("after caching*******************************************");
        if (cacheKeyInfo.getSqlCommandType()==SqlCommandType.SELECT && !exists){
            setCacheData(cacheKeyInfo,retData);
        }
        // gc無(wú)法回收caffeine的正常引用類型
        // System.gc();
    }

    private Pair<Boolean, Object> beforeProceed(CacheKeyInfo cacheKeyInfo) {
        Console.log("before caching******************************************");
        //這里粗糙一點(diǎn),其實(shí)可以自定義哪些command取緩存禀综,哪些刪緩存
        if (cacheKeyInfo.getSqlCommandType()==SqlCommandType.SELECT){
            return getCacheData(cacheKeyInfo);
        }
        else{
            evictCacheData(cacheKeyInfo);
        }
        return Pair.of(false, "");
    }

    private CacheKeyInfo getCacheKeyInfo(ProceedingJoinPoint pjp) {
        MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
        Class<?> daoInterface =  pjp.getTarget().getClass().getInterfaces()[0];
        Method method = methodSignature.getMethod();
        return new CacheKeyInfo(daoInterface,method,pjp.getArgs(),sqlSessionFactory.getConfiguration());
    }

    private Pair<Boolean, Object> getCacheData(CacheKeyInfo cacheKeyInfo) {
        MyCache cache = myCacheFactory.getCache(cacheKeyInfo);
        String key = cacheKeyInfo.getCacheKey();
        if (cache.exists(key)) {
            return Pair.of(true, cache.get(key));
        } else {
            return Pair.of(false, "");
        }
    }

    private void setCacheData(CacheKeyInfo cacheKeyInfo,Object value){
        MyCache cache = myCacheFactory.getCache(cacheKeyInfo);
        cache.set(cacheKeyInfo, value);
    }

    private void evictCacheData(CacheKeyInfo cacheKeyInfo){
        MyCache cache = myCacheFactory.getCache(cacheKeyInfo);
        cache.evict(cacheKeyInfo);
    }
}

CacheKeyInfo

用于計(jì)算處理緩存的key简烘、sqlCommandType苔严、涉及表refTables

pom.xml增加依賴:

<!-- sql語(yǔ)句解析 -->
<dependency>
    <groupId>com.github.jsqlparser</groupId>
    <artifactId>jsqlparser</artifactId>
    <version>3.2</version>
</dependency>
package com.it_laowu.springbootstudy.springbootstudydemo.core.cache;
......
@Data
public class CacheKeyInfo {
    private Object[] methodArgs;
    private Method method;
    private Configuration configuration;
    private MapperMethod.MethodSignature methodSignature;
    private Class<?> daoInterface;
    private String cacheKey;
    private SqlCommandType sqlCommandType;
    private List<String> refTableNames;

    public CacheKeyInfo(Class<?> daoInterface, Method method, Object[] methodArgs, Configuration configuration) {
        this.daoInterface = daoInterface;
        this.method = method;
        this.methodArgs = methodArgs;
        this.configuration = configuration;
        this.methodSignature = new MapperMethod.MethodSignature(configuration, daoInterface, method);
        this.cacheKey = createCacheKey();
        this.sqlCommandType = createSqlCommandType();
        this.refTableNames = createRefTableNames();
    }

    private String createCacheKey() {
        StringBuilder sb = new StringBuilder();
        sb.append(daoInterface.getName());
        sb.append("::");
        sb.append(method.getName());
        sb.append("::");
        Arrays.stream(getMethodArgs()).forEach(i -> {
            sb.append(i.toString());
        });
        return sb.toString();
    }

    private SqlCommandType createSqlCommandType() {
        String mapperId = getDaoInterface().getName() + "." + getMethod().getName();
        return getConfiguration().getMappedStatement(mapperId, false).getSqlCommandType();
    }

    private List<String> createRefTableNames() {
        String mapperId = getDaoInterface().getName() + "." + getMethod().getName();
        MappedStatement mappedStatement = getConfiguration().getMappedStatement(mapperId);
        Object sqlCommandParam = getMethodSignature().convertArgsToSqlCommandParam(getMethodArgs());
        BoundSql boundSql = mappedStatement.getBoundSql(wrapCollection(sqlCommandParam));
        String sql = boundSql.getSql();
        return getTablesFromSql(sql);
    }

    private List<String> getTablesFromSql(String sql) {
        try {
            Statement statement;
            statement = CCJSqlParserUtil.parse(sql);
            TablesNamesFinder tablesNamesFinder = new TablesNamesFinder();
             List<String> tableList = tablesNamesFinder.getTableList(statement);
            // Select selectStatement = (Select) statement;
            return tableList;
        } catch (JSQLParserException e) {
            e.printStackTrace();
        }
        return new ArrayList<String>();
    }

    private Object wrapCollection(final Object object) {
        if (object instanceof Collection) {
            DefaultSqlSession.StrictMap<Object> map = new DefaultSqlSession.StrictMap<Object>();
            map.put("collection", object);
            if (object instanceof List) {
                map.put("list", object);
            }
            return map;
        } else if (object != null && object.getClass().isArray()) {
            DefaultSqlSession.StrictMap<Object> map = new DefaultSqlSession.StrictMap<Object>();
            map.put("array", object);
            return map;
        }
        return object;
    }
}

抽象Cache

我們使用一個(gè)MyCache接口孤澎,定義一下常用的緩存操作届氢,再定一個(gè)MyCacheFactory,管理多種緩存覆旭。

MyCache

注意這里清除緩存用了兩個(gè)方法退子,一個(gè)清除key,一個(gè)清除當(dāng)前操作相關(guān)的所有緩存型将。

package com.it_laowu.springbootstudy.springbootstudydemo.core.cache;

public interface MyCache {
    Object get(String key);
    boolean set(String key,Object value);
    boolean set(CacheKeyInfo cacheKeyInfo,Object value);
    boolean evict(String key);
    boolean evict(CacheKeyInfo cacheKeyInfo);
    boolean exists(String key);
}

MyCacheFactory

這里先考慮本地緩存寂祥,所以寫死用local

package com.it_laowu.springbootstudy.springbootstudydemo.core.cache;
......
@Component
public class MyCacheFactory {

    @Autowired
    private RedisUtil redisUtil;

    @Autowired
    private CacheProperties cacheProperties;

    private Map<String, MyCache> caches = new ConcurrentHashMap<String, MyCache>();

    public MyCache getCache(CacheKeyInfo cacheKeyInfo) {
        if (caches.isEmpty()) {
            init();
        }
        //這里可以根據(jù)cacheProperties七兜,返回不同類型cache
        return caches.get("distribution");
    }

    private synchronized void init() {
        Cache<String, Object> cacheObj = Caffeine.newBuilder().expireAfterAccess(1, TimeUnit.HOURS).maximumSize(10000).build();
        caches.put("local", new MyCaffeineCache(cacheObj));
        caches.put("distribution", new MyRedisCache(redisUtil, cacheProperties));
    }
}

本地緩存實(shí)現(xiàn)

我選擇使用Caffeine做為本地緩存丸凭。實(shí)現(xiàn)MyCache接口就可以了。

Caffeine是一種非常好用的本地緩存腕铸,特點(diǎn)是使用了W-TinyLFU算法贮乳,大致原理是:

  1. 將所有鍵通過(guò)hash值映射到某塊bit數(shù)組,并統(tǒng)計(jì)訪問(wèn)頻率恬惯。同時(shí)使用
  2. 將緩存分為:窗口緩存LRU算法占1%向拆、主緩存SLRU+LFU算法占99%(又分為熱80%、冷20%)
  3. 窗口緩存中的LRU犧牲者酪耳,進(jìn)入主緩存的冷區(qū)域浓恳,進(jìn)行PK。
  4. 算法3大大降低了熱點(diǎn)key由于沒有積累頻率被淘汰的可能性碗暗。

pom中添加依賴:

<!-- 緩存 -->
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
    <version>2.8.6</version>
</dependency>

MyCaffeineCache

package com.it_laowu.springbootstudy.springbootstudydemo.core.cache;
......
public class MyCaffeineCache implements MyCache {

    private Cache<String, Object> cacheObj;

    public MyCaffeineCache(Cache<String, Object> cacheObj) {
        this.cacheObj = cacheObj;
    }

    @Override
    public Object get(String key) {
        DebugUtils.writeInColor("this is get from caffeine");
        return cacheObj.getIfPresent(key);
    }

    @Override
    public boolean set(String key, Object value) {
        try {
            cacheObj.put(key, value);
            DebugUtils.writeInColor("this is put in caffeine");
            return true;
        } catch (Exception ex) {
            return false;
        }
    }

    @Override
    public boolean evict(String key) {
        try {
            cacheObj.invalidate(key);
            return true;
        } catch (Exception ex) {
            return false;
        }
    }

    @Override
    public boolean exists(String key) {
        return cacheObj.asMap().containsKey(key);
    }

    @Override
    public boolean set(CacheKeyInfo cacheKeyInfo, Object value) {
        try {
            String key = cacheKeyInfo.getCacheKey();
            cacheObj.put(key, value);
            cacheKeyInfo.getRefTableNames().forEach(i -> {
                cacheObj.asMap().compute(i, (k, v) -> {
                    if (v == null) {
                        List<String> o = new ArrayList<String>();
                        o.add(key);
                        return o;
                    } else {
                        List<String> o = (List<String>) v;
                        o.remove(key);
                        o.add(key);
                        return o;
                    }
                });
            });
            DebugUtils.writeInColor("this is put ref in caffeine");
            return true;
        } catch (Exception ex) {
            return false;
        }
    }

    @Override
    public boolean evict(CacheKeyInfo cacheKeyInfo) {
        try {
            //清緩存并不是清除當(dāng)前key颈将,而是key -->表-->key
            List<String> tables =  cacheKeyInfo.getRefTableNames();
            List<String> keys = new ArrayList<String>();
            cacheKeyInfo.getRefTableNames().forEach(i -> {
                keys.addAll((ArrayList<String>)cacheObj.getIfPresent(i));
            });
            cacheObj.invalidateAll(keys);
            cacheObj.invalidateAll(tables);
            DebugUtils.writeInColor("this is evict ref in caffeine");
            return true;
        } catch (Exception ex) {
            return false;
        }
    }
}

分布式緩存Redis

package com.it_laowu.springbootstudy.springbootstudydemo.core.cache;
......

public class MyRedisCache implements MyCache {
    private RedisUtil redisUtil;
    private CacheProperties cacheProperties;

    public MyRedisCache(RedisUtil _redisUtil, CacheProperties _cacheProperties) {
        redisUtil = _redisUtil;
        cacheProperties = _cacheProperties;
    }

    @Override
    public Object get(String key) {
        DebugUtils.writeInColor("this is get from redis");
        return redisUtil.get(key);
    }

    @Override
    public boolean set(String key, Object value) {
        try {
            DebugUtils.writeInColor("this is put in redis");            
            return redisUtil.set(key, value);
        } catch (Exception ex) {
            return false;
        }
    }

    @Override
    public boolean evict(String key) {
        try {
            redisUtil.del(key);
            return true;
        } catch (Exception ex) {
            return false;
        }
    }

    @Override
    public boolean exists(String key) {
        return redisUtil.hasKey(key);
    }

    @Override
    public boolean set(CacheKeyInfo cacheKeyInfo, Object value) {
        try {
            String key = cacheKeyInfo.getCacheKey();
            Boolean b = redisUtil.set(key, value,cacheProperties.getExpireSecond());
            cacheKeyInfo.getRefTableNames().forEach(i -> {
                redisUtil.sSetAndTime(i, cacheProperties.getExpireSecond(), key);
            });
            DebugUtils.writeInColor("this is put ref in redis");
            return b;
        } catch (Exception ex) {
            return false;
        }
    }

    @Override
    public boolean evict(CacheKeyInfo cacheKeyInfo) {
        try {
            //清緩存并不是清除當(dāng)前key,而是key -->表-->key
            String[] tables =  cacheKeyInfo.getRefTableNames().toArray(new String[0]);
            List<String> keys = new ArrayList<String>();
            cacheKeyInfo.getRefTableNames().forEach(i -> {
                keys.addAll(Arrays.asList(redisUtil.sGet(i).toArray(new String[0])));
            });
            redisUtil.del(keys.toArray(new String[0]));
            redisUtil.del(tables);
            DebugUtils.writeInColor("this is evict ref in redis");
            return true;
        } catch (Exception ex) {
            return false;
        }
    }

}

總結(jié)

本文初步實(shí)現(xiàn)了一個(gè)切面層級(jí)的多級(jí)緩存(本地+分布式)言疗。應(yīng)該說(shuō)是相當(dāng)初步的晴圾,還有很多細(xì)節(jié)可以完善:

  1. 把一些常用參數(shù)維護(hù)到application.yml中
  2. 增加注解型緩存,優(yōu)先級(jí)應(yīng)該高于切面噪奄。
  3. 緩存分級(jí)死姚,同時(shí)加入算法
  4. 實(shí)際使用中,發(fā)現(xiàn)讀取mybatis設(shè)置比較費(fèi)時(shí)勤篮,可以考慮加入字典都毒。
  5. redis支持Cluster

感興趣的可以留言,給我進(jìn)一步完善的動(dòng)力碰缔。

完畢账劲。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子瀑焦,更是在濱河造成了極大的恐慌腌且,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,627評(píng)論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件榛瓮,死亡現(xiàn)場(chǎng)離奇詭異铺董,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)榆芦,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,180評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門柄粹,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人匆绣,你說(shuō)我怎么就攤上這事驻右。” “怎么了崎淳?”我有些...
    開封第一講書人閱讀 169,346評(píng)論 0 362
  • 文/不壞的土叔 我叫張陵堪夭,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我拣凹,道長(zhǎng)森爽,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 60,097評(píng)論 1 300
  • 正文 為了忘掉前任嚣镜,我火速辦了婚禮爬迟,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘菊匿。我一直安慰自己付呕,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,100評(píng)論 6 398
  • 文/花漫 我一把揭開白布跌捆。 她就那樣靜靜地躺著徽职,像睡著了一般。 火紅的嫁衣襯著肌膚如雪佩厚。 梳的紋絲不亂的頭發(fā)上姆钉,一...
    開封第一講書人閱讀 52,696評(píng)論 1 312
  • 那天,我揣著相機(jī)與錄音抄瓦,去河邊找鬼潮瓶。 笑死,一個(gè)胖子當(dāng)著我的面吹牛闺鲸,可吹牛的內(nèi)容都是我干的筋讨。 我是一名探鬼主播,決...
    沈念sama閱讀 41,165評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼摸恍,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起立镶,我...
    開封第一講書人閱讀 40,108評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤壁袄,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后媚媒,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體嗜逻,經(jīng)...
    沈念sama閱讀 46,646評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,709評(píng)論 3 342
  • 正文 我和宋清朗相戀三年缭召,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了栈顷。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,861評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡嵌巷,死狀恐怖萄凤,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情搪哪,我是刑警寧澤靡努,帶...
    沈念sama閱讀 36,527評(píng)論 5 351
  • 正文 年R本政府宣布,位于F島的核電站晓折,受9級(jí)特大地震影響惑朦,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜漓概,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,196評(píng)論 3 336
  • 文/蒙蒙 一漾月、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧胃珍,春花似錦梁肿、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,698評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至缔莲,卻和暖如春哥纫,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背痴奏。 一陣腳步聲響...
    開封第一講書人閱讀 33,804評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工蛀骇, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人读拆。 一個(gè)月前我還...
    沈念sama閱讀 49,287評(píng)論 3 379
  • 正文 我出身青樓擅憔,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親檐晕。 傳聞我的和親對(duì)象是個(gè)殘疾皇子暑诸,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,860評(píng)論 2 361

推薦閱讀更多精彩內(nèi)容