1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        SpringBoot 緩存系統(tǒng)大總結(jié)

        共 30050字,需瀏覽 61分鐘

         ·

        2020-11-21 22:53


        點(diǎn)擊上方Java專欄”,選擇“置頂或者星標(biāo)”


        第一時(shí)間閱讀精彩文章!


        點(diǎn)擊這段文字獲?。?/strong>5個(gè)可以寫(xiě)到簡(jiǎn)歷的項(xiàng)目實(shí)戰(zhàn)視頻教程(含源碼)



        1?SpringBoot緩存系統(tǒng)

        參考來(lái)源:www.cnblogs.com/jeffwongishandsome

        緩存是最直接有效提升系統(tǒng)性能的手段之一。個(gè)人認(rèn)為用好用對(duì)緩存是優(yōu)秀程序員的必備基本素質(zhì)。

        本文結(jié)合實(shí)際開(kāi)發(fā)經(jīng)驗(yàn),從簡(jiǎn)單概念原理和代碼入手,一步一步搭建一個(gè)簡(jiǎn)單的二級(jí)緩存系統(tǒng)。

        一、通用緩存接口

        1、緩存基礎(chǔ)算法

        • FIFO(First In First Out),先進(jìn)先出,和OS里的FIFO思路相同,如果一個(gè)數(shù)據(jù)最先進(jìn)入緩存中,當(dāng)緩存滿的時(shí)候,應(yīng)當(dāng)把最先進(jìn)入緩存的數(shù)據(jù)給移除掉。

        • LFU(Least Frequently Used),最不經(jīng)常使用,如果一個(gè)數(shù)據(jù)在最近一段時(shí)間內(nèi)使用次數(shù)很少,那么在將來(lái)一段時(shí)間內(nèi)被使用的可能性也很小。

        • LRU(Least Recently Used),最近最少使用,如果一個(gè)數(shù)據(jù)在最近一段時(shí)間沒(méi)有被訪問(wèn)到,那么在將來(lái)它被訪問(wèn)的可能性也很小。也就是說(shuō),當(dāng)限定的空間已存滿數(shù)據(jù)時(shí),應(yīng)當(dāng)把最久沒(méi)有被訪問(wèn)到的數(shù)據(jù)移除。

        2、接口定義

        簡(jiǎn)單定義緩存接口,大致可以抽象如下:

        package com.power.demo.cache.contract;
        import java.util.function.Function;
        /** * 緩存提供者接口 **/public interface CacheProviderService {
        /** * 查詢緩存 * * @param key 緩存鍵 不可為空 **/ extends Object> T get(String key);
        /** * 查詢緩存 * * @param key 緩存鍵 不可為空 * @param function 如沒(méi)有緩存,調(diào)用該callable函數(shù)返回對(duì)象 可為空 **/ extends Object> T get(String key, Function<String, T> function);
        /** * 查詢緩存 * * @param key 緩存鍵 不可為空 * @param function 如沒(méi)有緩存,調(diào)用該callable函數(shù)返回對(duì)象 可為空 * @param funcParm function函數(shù)的調(diào)用參數(shù) **/ extends Object, M extends Object> T get(String key, Function function, M funcParm);
        /** * 查詢緩存 * * @param key 緩存鍵 不可為空 * @param function 如沒(méi)有緩存,調(diào)用該callable函數(shù)返回對(duì)象 可為空 * @param expireTime 過(guò)期時(shí)間(單位:毫秒) 可為空 **/ extends Object> T get(String key, Function<String, T> function, Long expireTime);
        /** * 查詢緩存 * * @param key 緩存鍵 不可為空 * @param function 如沒(méi)有緩存,調(diào)用該callable函數(shù)返回對(duì)象 可為空 * @param funcParm function函數(shù)的調(diào)用參數(shù) * @param expireTime 過(guò)期時(shí)間(單位:毫秒) 可為空 **/ extends Object, M extends Object> T get(String key, Function function, M funcParm, Long expireTime);
        /** * 設(shè)置緩存鍵值 * * @param key 緩存鍵 不可為空 * @param obj 緩存值 不可為空 **/ extends Object> void set(String key, T obj);
        /** * 設(shè)置緩存鍵值 * * @param key 緩存鍵 不可為空 * @param obj 緩存值 不可為空 * @param expireTime 過(guò)期時(shí)間(單位:毫秒) 可為空 **/ extends Object> void set(String key, T obj, Long expireTime);
        /** * 移除緩存 * * @param key 緩存鍵 不可為空 **/ void remove(String key);
        /** * 是否存在緩存 * * @param key 緩存鍵 不可為空 **/ boolean contains(String key);}

        注意,這里列出的只是常見(jiàn)緩存功能接口,一些在特殊場(chǎng)景下用到的統(tǒng)計(jì)類的接口、分布式鎖、自增(減)等功能不在討論范圍之內(nèi)。

        Get相關(guān)方法,注意多個(gè)參數(shù)的情況,緩存接口里面?zhèn)魅说腇unction,這是Java8提供的函數(shù)式接口,雖然支持的入?yún)€(gè)數(shù)有限(這里你會(huì)非常懷念.NET下的Func委托),但是僅對(duì)Java這個(gè)語(yǔ)言來(lái)說(shuō),這真是一個(gè)重大的進(jìn)步^_^。

        接口定義好了,下面就要實(shí)現(xiàn)緩存提供者程序了。按照存儲(chǔ)類型的不同,本文簡(jiǎn)單實(shí)現(xiàn)最常用的兩種緩存提供者:本地緩存和分布式緩存。

        二、本地緩存

        本地緩存,也就是JVM級(jí)別的緩存(本地緩存可以認(rèn)為是直接在進(jìn)程內(nèi)通信調(diào)用,而分布式緩存則需要通過(guò)網(wǎng)絡(luò)進(jìn)行跨進(jìn)程通信調(diào)用),一般有很多種實(shí)現(xiàn)方式,比如直接使用Hashtable、ConcurrentHashMap等天生線程安全的集合作為緩存容器,或者使用一些成熟的開(kāi)源組件,如EhCache、Guava Cache等。本文選擇上手簡(jiǎn)單的Guava緩存。

        1、什么是Guava

        Guava,簡(jiǎn)單來(lái)說(shuō)就是一個(gè)開(kāi)發(fā)類庫(kù),且是一個(gè)非常豐富強(qiáng)大的開(kāi)發(fā)工具包,號(hào)稱可以讓使用Java語(yǔ)言更令人愉悅,主要包括基本工具類庫(kù)和接口、緩存、發(fā)布訂閱風(fēng)格的事件總線等。在實(shí)際開(kāi)發(fā)中,我用的最多的是集合、緩存和常用類型幫助類,很多人都對(duì)這個(gè)類庫(kù)稱贊有加。

        2、添加依賴

          <dependency>            <groupId>com.google.guavagroupId>            <artifactId>guavaartifactId>        dependency>

        3、實(shí)現(xiàn)接口

        package com.power.demo.cache.impl;
        import com.google.common.cache.Cache;import com.google.common.cache.CacheBuilder;import com.google.common.collect.Maps;import com.power.demo.cache.contract.CacheProviderService;import com.power.demo.common.AppConst;import org.springframework.beans.factory.annotation.Qualifier;import org.springframework.context.annotation.ComponentScan;import org.springframework.context.annotation.Configuration;import org.springframework.util.StringUtils;
        import java.util.Map;import java.util.concurrent.TimeUnit;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;import java.util.function.Function;
        /* * 本地緩存提供者服務(wù) (Guava Cache) * */@Configuration@ComponentScan(basePackages = AppConst.BASE_PACKAGE_NAME)@Qualifier("localCacheService")public class LocalCacheProviderImpl implements CacheProviderService {
        private static Map<String, Cache<String, Object>> _cacheMap = Maps.newConcurrentMap();
        static {
        Cache<String, Object> cacheContainer = CacheBuilder.newBuilder() .maximumSize(AppConst.CACHE_MAXIMUM_SIZE) .expireAfterWrite(AppConst.CACHE_MINUTE, TimeUnit.MILLISECONDS)//最后一次寫(xiě)入后的一段時(shí)間移出 //.expireAfterAccess(AppConst.CACHE_MINUTE, TimeUnit.MILLISECONDS) //最后一次訪問(wèn)后的一段時(shí)間移出 .recordStats()//開(kāi)啟統(tǒng)計(jì)功能 .build();
        _cacheMap.put(String.valueOf(AppConst.CACHE_MINUTE), cacheContainer); }
        /** * 查詢緩存 * * @param key 緩存鍵 不可為空 **/ public extends Object> T get(String key) { T obj = get(key, null, null, AppConst.CACHE_MINUTE);
        return obj; }
        /** * 查詢緩存 * * @param key 緩存鍵 不可為空 * @param function 如沒(méi)有緩存,調(diào)用該callable函數(shù)返回對(duì)象 可為空 **/ public extends Object> T get(String key, Function<String, T> function) { T obj = get(key, function, key, AppConst.CACHE_MINUTE);
        return obj; }
        /** * 查詢緩存 * * @param key 緩存鍵 不可為空 * @param function 如沒(méi)有緩存,調(diào)用該callable函數(shù)返回對(duì)象 可為空 * @param funcParm function函數(shù)的調(diào)用參數(shù) **/ public extends Object, M extends Object> T get(String key, Function function, M funcParm) { T obj = get(key, function, funcParm, AppConst.CACHE_MINUTE);
        return obj; }
        /** * 查詢緩存 * * @param key 緩存鍵 不可為空 * @param function 如沒(méi)有緩存,調(diào)用該callable函數(shù)返回對(duì)象 可為空 * @param expireTime 過(guò)期時(shí)間(單位:毫秒) 可為空 **/ public extends Object> T get(String key, Function<String, T> function, Long expireTime) { T obj = get(key, function, key, expireTime);
        return obj; }
        /** * 查詢緩存 * * @param key 緩存鍵 不可為空 * @param function 如沒(méi)有緩存,調(diào)用該callable函數(shù)返回對(duì)象 可為空 * @param funcParm function函數(shù)的調(diào)用參數(shù) * @param expireTime 過(guò)期時(shí)間(單位:毫秒) 可為空 **/ public extends Object, M extends Object> T get(String key, Function function, M funcParm, Long expireTime) { T obj = null; if (StringUtils.isEmpty(key) == true) { return obj; }
        expireTime = getExpireTime(expireTime);
        Cache<String, Object> cacheContainer = getCacheContainer(expireTime);
        try { if (function == null) { obj = (T) cacheContainer.getIfPresent(key); } else { final Long cachedTime = expireTime; obj = (T) cacheContainer.get(key, () -> { T retObj = function.apply(funcParm); return retObj; }); } } catch (Exception e) { e.printStackTrace(); }
        return obj; }
        /** * 設(shè)置緩存鍵值 直接向緩存中插入值,這會(huì)直接覆蓋掉給定鍵之前映射的值 * * @param key 緩存鍵 不可為空 * @param obj 緩存值 不可為空 **/ public extends Object> void set(String key, T obj) {
        set(key, obj, AppConst.CACHE_MINUTE); }
        /** * 設(shè)置緩存鍵值 直接向緩存中插入值,這會(huì)直接覆蓋掉給定鍵之前映射的值 * * @param key 緩存鍵 不可為空 * @param obj 緩存值 不可為空 * @param expireTime 過(guò)期時(shí)間(單位:毫秒) 可為空 **/ public extends Object> void set(String key, T obj, Long expireTime) { if (StringUtils.isEmpty(key) == true) { return; }
        if (obj == null) { return; }
        expireTime = getExpireTime(expireTime);
        Cache<String, Object> cacheContainer = getCacheContainer(expireTime);
        cacheContainer.put(key, obj); }
        /** * 移除緩存 * * @param key 緩存鍵 不可為空 **/ public void remove(String key) { if (StringUtils.isEmpty(key) == true) { return; }
        long expireTime = getExpireTime(AppConst.CACHE_MINUTE);
        Cache<String, Object> cacheContainer = getCacheContainer(expireTime);
        cacheContainer.invalidate(key); }
        /** * 是否存在緩存 * * @param key 緩存鍵 不可為空 **/ public boolean contains(String key) { boolean exists = false; if (StringUtils.isEmpty(key) == true) { return exists; }
        Object obj = get(key);
        if (obj != null) { exists = true; }
        return exists; }
        private static Lock lock = new ReentrantLock();
        private Cache<String, Object> getCacheContainer(Long expireTime) {
        Cache<String, Object> cacheContainer = null; if (expireTime == null) { return cacheContainer; }
        String mapKey = String.valueOf(expireTime);
        if (_cacheMap.containsKey(mapKey) == true) { cacheContainer = _cacheMap.get(mapKey); return cacheContainer; }
        try { lock.lock(); cacheContainer = CacheBuilder.newBuilder() .maximumSize(AppConst.CACHE_MAXIMUM_SIZE) .expireAfterWrite(expireTime, TimeUnit.MILLISECONDS)//最后一次寫(xiě)入后的一段時(shí)間移出 //.expireAfterAccess(AppConst.CACHE_MINUTE, TimeUnit.MILLISECONDS) //最后一次訪問(wèn)后的一段時(shí)間移出 .recordStats()//開(kāi)啟統(tǒng)計(jì)功能 .build();
        _cacheMap.put(mapKey, cacheContainer);
        } finally { lock.unlock(); }
        return cacheContainer; }
        /** * 獲取過(guò)期時(shí)間 單位:毫秒 * * @param expireTime 傳人的過(guò)期時(shí)間 單位毫秒 如小于1分鐘,默認(rèn)為10分鐘 **/ private Long getExpireTime(Long expireTime) { Long result = expireTime; if (expireTime == null || expireTime < AppConst.CACHE_MINUTE / 10) { result = AppConst.CACHE_MINUTE; }
        return result; }}

        4、注意事項(xiàng)

        Guava Cache初始化容器時(shí),支持緩存過(guò)期策略,類似FIFO、LRU和LFU等算法。

        • expireAfterWrite:最后一次寫(xiě)入后的一段時(shí)間移出。

        • expireAfterAccess:最后一次訪問(wèn)后的一段時(shí)間移出。

        Guava Cache對(duì)緩存過(guò)期時(shí)間的設(shè)置實(shí)在不夠友好。常見(jiàn)的應(yīng)用場(chǎng)景,比如,有些幾乎不變的基礎(chǔ)數(shù)據(jù)緩存1天,有些熱點(diǎn)數(shù)據(jù)緩存2小時(shí),有些會(huì)話數(shù)據(jù)緩存5分鐘等等。

        通常我們認(rèn)為設(shè)置緩存的時(shí)候帶上緩存的過(guò)期時(shí)間是非常容易的,而且只要一個(gè)緩存容器實(shí)例即可,比如.NET下的ObjectCache、System.Runtime.Cache等等。

        但是Guava Cache不是這個(gè)實(shí)現(xiàn)思路,如果緩存的過(guò)期時(shí)間不同,Guava的CacheBuilder要初始化多份Cache實(shí)例。

        好在我在實(shí)現(xiàn)的時(shí)候注意到了這個(gè)問(wèn)題,并且提供了解決方案,可以看到getCacheContainer這個(gè)函數(shù),根據(jù)過(guò)期時(shí)長(zhǎng)做緩存實(shí)例判斷,就算不同過(guò)期時(shí)間的多實(shí)例緩存也是完全沒(méi)有問(wèn)題的。

        三、分布式緩存

        分布式緩存產(chǎn)品非常多,本文使用應(yīng)用普遍的Redis,在Spring Boot應(yīng)用中使用Redis非常簡(jiǎn)單。

        1、什么是Redis

        Redis是一款開(kāi)源(BSD許可)的、用C語(yǔ)言寫(xiě)成的高性能的鍵-值存儲(chǔ)(key-value store)。它常被稱作是一款數(shù)據(jù)結(jié)構(gòu)服務(wù)器(data structure server)。它可以被用作緩存、消息中間件和數(shù)據(jù)庫(kù),在很多應(yīng)用中,經(jīng)??吹接腥诉x擇使用Redis做緩存,實(shí)現(xiàn)分布式鎖和分布式Session等。作為緩存系統(tǒng)時(shí),和經(jīng)典的KV結(jié)構(gòu)的Memcached非常相似,但又有很多不同。

        Redis支持豐富的數(shù)據(jù)類型。Redis的鍵值可以包括字符串(strings)類型,同時(shí)它還包括哈希(hashes)、列表(lists)、集合(sets)和有序集合(sorted sets)等數(shù)據(jù)類型。對(duì)于這些數(shù)據(jù)類型,你可以執(zhí)行原子操作。例如:對(duì)字符串進(jìn)行附加操作(append);遞增哈希中的值;向列表中增加元素;計(jì)算集合的交集、并集與差集等。

        Redis的數(shù)據(jù)類型:

        Keys:非二進(jìn)制安全的字符類型( not binary-safe strings ),由于key不是binary safe的字符串,所以像“my key”和“mykey\n”這樣包含空格和換行的key是不允許的。
        Values:Strings、Hash、Lists、 Sets、 Sorted sets。考慮到Redis單線程操作模式,Value的粒度不應(yīng)該過(guò)大,緩存的值越大,越容易造成阻塞和排隊(duì)。

        為了獲得優(yōu)異的性能,Redis采用了內(nèi)存中(in-memory)數(shù)據(jù)集(dataset)的方式。同時(shí),Redis支持?jǐn)?shù)據(jù)的持久化,你可以每隔一段時(shí)間將數(shù)據(jù)集轉(zhuǎn)存到磁盤上(snapshot),或者在日志尾部追加每一條操作命令(append only file,aof)。

        Redis同樣支持主從復(fù)制(master-slave replication),并且具有非常快速的非阻塞首次同步( non-blocking first synchronization)、網(wǎng)絡(luò)斷開(kāi)自動(dòng)重連等功能。

        同時(shí)Redis還具有其它一些特性,其中包括簡(jiǎn)單的事物支持、發(fā)布訂閱 ( pub/sub)、管道(pipeline)和虛擬內(nèi)存(vm)等 。

        2、添加依賴

           <dependency>            <groupId>org.springframework.bootgroupId>            <artifactId>spring-boot-starter-data-redisartifactId>        dependency>

        3、配置Redis

        在application.properties配置文件中,配置Redis常用參數(shù):

        ## Redis緩存相關(guān)配置#Redis數(shù)據(jù)庫(kù)索引(默認(rèn)為0)spring.redis.database=0#Redis服務(wù)器地址spring.redis.host=127.0.0.1#Redis服務(wù)器端口spring.redis.port=6379  #Redis服務(wù)器密碼(默認(rèn)為空)spring.redis.password=123321#Redis連接超時(shí)時(shí)間 默認(rèn):5分鐘(單位:毫秒)spring.redis.timeout=300000ms#Redis連接池最大連接數(shù)(使用負(fù)值表示沒(méi)有限制)spring.redis.jedis.pool.max-active=512#Redis連接池中的最小空閑連接spring.redis.jedis.pool.min-idle=0#Redis連接池中的最大空閑連接spring.redis.jedis.pool.max-idle=8#Redis連接池最大阻塞等待時(shí)間(使用負(fù)值表示沒(méi)有限制)spring.redis.jedis.pool.max-wait=-1ms

        常見(jiàn)的需要注意的是最大連接數(shù)(spring.redis.jedis.pool.max-active )和超時(shí)時(shí)間(spring.redis.jedis.pool.max-wait)。Redis在生產(chǎn)環(huán)境中出現(xiàn)故障的頻率經(jīng)常和這兩個(gè)參數(shù)息息相關(guān)。

        接著定義一個(gè)繼承自CachingConfigurerSupport(請(qǐng)注意cacheManager和keyGenerator這兩個(gè)方法在子類的實(shí)現(xiàn))的RedisConfig類:

        package com.power.demo.cache.config;
        import org.springframework.cache.CacheManager;import org.springframework.cache.annotation.CachingConfigurerSupport;import org.springframework.cache.annotation.EnableCaching;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.data.redis.cache.RedisCacheManager;import org.springframework.data.redis.connection.RedisConnectionFactory;import org.springframework.data.redis.core.RedisTemplate;import org.springframework.data.redis.serializer.StringRedisSerializer;
        /** * Redis緩存配置類 */@Configuration@EnableCachingpublic class RedisConfig extends CachingConfigurerSupport {
        @Bean public CacheManager cacheManager(RedisConnectionFactory connectionFactory) { return RedisCacheManager.create(connectionFactory); }
        @Bean public RedisTemplate redisTemplate(RedisConnectionFactory factory) { RedisTemplate template = new RedisTemplate<>();
        //Jedis的Key和Value的序列化器默認(rèn)值是JdkSerializationRedisSerializer //經(jīng)實(shí)驗(yàn),JdkSerializationRedisSerializer通過(guò)RedisDesktopManager看到的鍵值對(duì)不能正常解析
        //設(shè)置key的序列化器 template.setKeySerializer(new StringRedisSerializer());
        ////設(shè)置value的序列化器 默認(rèn)值是JdkSerializationRedisSerializer //使用Jackson序列化器的問(wèn)題是,復(fù)雜對(duì)象可能序列化失敗,比如JodaTime的DateTime類型
        // //使用Jackson2,將對(duì)象序列化為JSON // Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); // //json轉(zhuǎn)對(duì)象類,不設(shè)置默認(rèn)的會(huì)將json轉(zhuǎn)成hashmap // ObjectMapper om = new ObjectMapper(); // om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); // om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); // jackson2JsonRedisSerializer.setObjectMapper(om); // template.setValueSerializer(jackson2JsonRedisSerializer);
        //將redis連接工廠設(shè)置到模板類中 template.setConnectionFactory(factory);
        return template; }
        // //自定義緩存key生成策略// @Bean// public KeyGenerator keyGenerator() {// return new KeyGenerator() {// @Override// public Object generate(Object target, java.lang.reflect.Method method, Object... params) {// StringBuffer sb = new StringBuffer();// sb.append(target.getClass().getName());// sb.append(method.getName());// for (Object obj : params) {// if (obj == null) {// continue;// }// sb.append(obj.toString());// }// return sb.toString();// }// };// }}

        在RedisConfig這個(gè)類上加上@EnableCaching這個(gè)注解,這個(gè)注解會(huì)被Spring發(fā)現(xiàn),并且會(huì)創(chuàng)建一個(gè)切面(aspect) 并觸發(fā)Spring緩存注解的切點(diǎn)(pointcut)。據(jù)所使用的注解以及緩存的狀態(tài),這個(gè)切面會(huì)從緩存中獲取數(shù)據(jù),將數(shù)據(jù)添加到緩存之中或者從緩存中移除某個(gè)值。

        cacheManager方法,申明一個(gè)緩存管理器(CacheManager)的bean,作用就是@EnableCaching這個(gè)切面在新增緩存或者刪除緩存的時(shí)候會(huì)調(diào)用這個(gè)緩存管理器的方法。keyGenerator方法,可以根據(jù)需求自定義緩存key生成策略。

        而redisTemplate方法,則主要是設(shè)置Redis模板類,比如鍵和值的序列化器(從這里可以看出,Redis的鍵值對(duì)必須可序列化)、redis連接工廠等。

        RedisTemplate支持的序列化器主要有如下幾種:

        • JdkSerializationRedisSerializer:使用Java序列化;

        • StringRedisSerializer:序列化String類型的key和value;

        • GenericToStringSerializer:使用Spring轉(zhuǎn)換服務(wù)進(jìn)行序列化;

        • JacksonJsonRedisSerializer:使用Jackson 1,將對(duì)象序列化為JSON;

        • Jackson2JsonRedisSerializer:使用Jackson 2,將對(duì)象序列化為JSON;

        • OxmSerializer:使用Spring O/X映射的編排器和解排器(marshaler和unmarshaler)實(shí)現(xiàn)序列化,用于XML序列化;

        注意:RedisTemplate的鍵和值序列化器,默認(rèn)情況下都是JdkSerializationRedisSerializer,它們都可以自定義設(shè)置序列化器。

        推薦將字符串鍵使用StringRedisSerializer序列化器,因?yàn)檫\(yùn)維的時(shí)候好排查問(wèn)題,JDK序列化器的也能識(shí)別,但是可讀性稍差(是因?yàn)榫彺娣?wù)器沒(méi)有JRE嗎?),見(jiàn)如下效果:

        而值序列化器則要復(fù)雜的多,很多人推薦使用Jackson2JsonRedisSerializer序列化器,但是實(shí)際開(kāi)發(fā)過(guò)程中,經(jīng)常有人碰到反序列化錯(cuò)誤,經(jīng)過(guò)排查多數(shù)都和Jackson2JsonRedisSerializer這個(gè)序列化器有關(guān)。

        4、實(shí)現(xiàn)接口

        使用RedisTemplate,在Spring Boot中調(diào)用Redis接口比直接調(diào)用Jedis簡(jiǎn)單多了。

        package com.power.demo.cache.impl;
        import com.power.demo.cache.contract.CacheProviderService;import com.power.demo.common.AppConst;import org.springframework.beans.factory.annotation.Qualifier;import org.springframework.context.annotation.ComponentScan;import org.springframework.context.annotation.Configuration;import org.springframework.data.redis.core.RedisTemplate;import org.springframework.data.redis.core.ValueOperations;import org.springframework.util.StringUtils;
        import javax.annotation.Resource;import java.io.Serializable;import java.util.concurrent.TimeUnit;import java.util.function.Function;
        @Configuration@ComponentScan(basePackages = AppConst.BASE_PACKAGE_NAME)@Qualifier("redisCacheService")public class RedisCacheProviderImpl implements CacheProviderService {
        @Resource private RedisTemplateObject> redisTemplate;
        /** * 查詢緩存 * * @param key 緩存鍵 不可為空 **/ public extends Object> T get(String key) { T obj = get(key, null, null, AppConst.CACHE_MINUTE);
        return obj; }
        /** * 查詢緩存 * * @param key 緩存鍵 不可為空 * @param function 如沒(méi)有緩存,調(diào)用該callable函數(shù)返回對(duì)象 可為空 **/ public extends Object> T get(String key, Function<String, T> function) { T obj = get(key, function, key, AppConst.CACHE_MINUTE);
        return obj; }
        /** * 查詢緩存 * * @param key 緩存鍵 不可為空 * @param function 如沒(méi)有緩存,調(diào)用該callable函數(shù)返回對(duì)象 可為空 * @param funcParm function函數(shù)的調(diào)用參數(shù) **/ public extends Object, M extends Object> T get(String key, Function function, M funcParm) { T obj = get(key, function, funcParm, AppConst.CACHE_MINUTE);
        return obj; }
        /** * 查詢緩存 * * @param key 緩存鍵 不可為空 * @param function 如沒(méi)有緩存,調(diào)用該callable函數(shù)返回對(duì)象 可為空 * @param expireTime 過(guò)期時(shí)間(單位:毫秒) 可為空 **/ public extends Object> T get(String key, Function<String, T> function, Long expireTime) { T obj = get(key, function, key, expireTime);
        return obj; }
        /** * 查詢緩存 * * @param key 緩存鍵 不可為空 * @param function 如沒(méi)有緩存,調(diào)用該callable函數(shù)返回對(duì)象 可為空 * @param funcParm function函數(shù)的調(diào)用參數(shù) * @param expireTime 過(guò)期時(shí)間(單位:毫秒) 可為空 **/ public extends Object, M extends Object> T get(String key, Function function, M funcParm, Long expireTime) { T obj = null; if (StringUtils.isEmpty(key) == true) { return obj; }
        expireTime = getExpireTime(expireTime);
        try {
        ValueOperationsObject> operations = redisTemplate.opsForValue(); obj = (T) operations.get(key); if (function != null && obj == null) { obj = function.apply(funcParm); if (obj != null) { set(key, obj, expireTime);//設(shè)置緩存信息 } } } catch (Exception e) { e.printStackTrace(); }
        return obj; }
        /** * 設(shè)置緩存鍵值 直接向緩存中插入值,這會(huì)直接覆蓋掉給定鍵之前映射的值 * * @param key 緩存鍵 不可為空 * @param obj 緩存值 不可為空 **/ public extends Object> void set(String key, T obj) {
        set(key, obj, AppConst.CACHE_MINUTE); }
        /** * 設(shè)置緩存鍵值 直接向緩存中插入值,這會(huì)直接覆蓋掉給定鍵之前映射的值 * * @param key 緩存鍵 不可為空 * @param obj 緩存值 不可為空 * @param expireTime 過(guò)期時(shí)間(單位:毫秒) 可為空 **/ public extends Object> void set(String key, T obj, Long expireTime) { if (StringUtils.isEmpty(key) == true) { return; }
        if (obj == null) { return; }
        expireTime = getExpireTime(expireTime);
        ValueOperationsObject> operations = redisTemplate.opsForValue();
        operations.set(key, obj);
        redisTemplate.expire(key, expireTime, TimeUnit.MILLISECONDS); }
        /** * 移除緩存 * * @param key 緩存鍵 不可為空 **/ public void remove(String key) { if (StringUtils.isEmpty(key) == true) { return; }
        redisTemplate.delete(key); }
        /** * 是否存在緩存 * * @param key 緩存鍵 不可為空 **/ public boolean contains(String key) { boolean exists = false; if (StringUtils.isEmpty(key) == true) { return exists; }
        Object obj = get(key);
        if (obj != null) { exists = true; }
        return exists; }
        /** * 獲取過(guò)期時(shí)間 單位:毫秒 * * @param expireTime 傳人的過(guò)期時(shí)間 單位毫秒 如小于1分鐘,默認(rèn)為10分鐘 **/ private Long getExpireTime(Long expireTime) { Long result = expireTime; if (expireTime == null || expireTime < AppConst.CACHE_MINUTE / 10) { result = AppConst.CACHE_MINUTE; }
        return result; }}

        注意:很多教程里都講到通過(guò)注解的方式(@Cacheable,@CachePut、@CacheEvict和@Caching)實(shí)現(xiàn)數(shù)據(jù)緩存,根據(jù)實(shí)踐,我個(gè)人是不推崇這種使用方式的。

        四、緩存“及時(shí)”過(guò)期問(wèn)題

        這個(gè)也是開(kāi)發(fā)和運(yùn)維過(guò)程中非常經(jīng)典的問(wèn)題。

        有些公司寫(xiě)緩存客戶端的時(shí)候,會(huì)給每個(gè)團(tuán)隊(duì)分別定義一個(gè)Area,但是這個(gè)只能做到緩存鍵的分布區(qū)分,不能保證緩存“實(shí)時(shí)”有效的過(guò)期。

        多年以前我寫(xiě)過(guò)一篇結(jié)合實(shí)際情況的文章,也就是加上緩存版本,請(qǐng)猛擊這里 ,算是提供了一種相對(duì)有效的方案,不過(guò)高并發(fā)站點(diǎn)要慎重,防止發(fā)生雪崩效應(yīng)。

        Redis還有一些其他常見(jiàn)問(wèn)題,比如:Redis的字符串類型Key和Value都有限制,且都是不能超過(guò)512M,請(qǐng)猛擊這里。還有最大連接數(shù)和超時(shí)時(shí)間設(shè)置等問(wèn)題,本文就不再一一列舉了。

        五、二級(jí)緩存

        在配置文件中,加上緩存提供者開(kāi)關(guān):

        ##是否啟用本地緩存spring.power.isuselocalcache=1##是否啟用Redis緩存spring.power.isuserediscache=1

        緩存提供者程序都實(shí)現(xiàn)好了,我們會(huì)再包裝一個(gè)調(diào)用外觀類PowerCacheBuilder,加上緩存版本控制,可以輕松自如地控制和切換緩存,code talks:

        package com.power.demo.cache;
        import com.google.common.collect.Lists;import com.power.demo.cache.contract.CacheProviderService;import com.power.demo.common.AppConst;import com.power.demo.common.AppField;import com.power.demo.util.ConfigUtil;import com.power.demo.util.PowerLogger;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.beans.factory.annotation.Qualifier;import org.springframework.context.annotation.ComponentScan;import org.springframework.context.annotation.Configuration;import org.springframework.util.StringUtils;
        import java.util.List;import java.util.UUID;import java.util.concurrent.TimeUnit;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;import java.util.function.Function;
        /* * 支持多緩存提供程序多級(jí)緩存的緩存幫助類 * */@Configuration@ComponentScan(basePackages = AppConst.BASE_PACKAGE_NAME)public class PowerCacheBuilder {
        @Autowired @Qualifier("localCacheService") private CacheProviderService localCacheService;
        @Autowired @Qualifier("redisCacheService") private CacheProviderService redisCacheService;
        private static List _listCacheProvider = Lists.newArrayList();
        private static final Lock providerLock = new ReentrantLock();
        /** * 初始化緩存提供者 默認(rèn)優(yōu)先級(jí):先本地緩存,后分布式緩存 **/ private List getCacheProviders() {
        if (_listCacheProvider.size() > 0) { return _listCacheProvider; }
        //線程安全 try { providerLock.tryLock(1000, TimeUnit.MILLISECONDS);
        if (_listCacheProvider.size() > 0) { return _listCacheProvider; }
        String isUseCache = ConfigUtil.getConfigVal(AppField.IS_USE_LOCAL_CACHE);
        CacheProviderService cacheProviderService = null;
        //啟用本地緩存 if ("1".equalsIgnoreCase(isUseCache)) { _listCacheProvider.add(localCacheService); }
        isUseCache = ConfigUtil.getConfigVal(AppField.IS_USE_REDIS_CACHE);
        //啟用Redis緩存 if ("1".equalsIgnoreCase(isUseCache)) { _listCacheProvider.add(redisCacheService);
        resetCacheVersion();//設(shè)置分布式緩存版本號(hào) }
        PowerLogger.info("初始化緩存提供者成功,共有" + _listCacheProvider.size() + "個(gè)"); } catch (Exception e) { e.printStackTrace();
        _listCacheProvider = Lists.newArrayList();
        PowerLogger.error("初始化緩存提供者發(fā)生異常:{}", e); } finally { providerLock.unlock(); }
        return _listCacheProvider; }
        /** * 查詢緩存 * * @param key 緩存鍵 不可為空 **/ public extends Object> T get(String key) { T obj = null;
        //key = generateVerKey(key);//構(gòu)造帶版本的緩存鍵
        for (CacheProviderService provider : getCacheProviders()) {
        obj = provider.get(key);
        if (obj != null) { return obj; } }
        return obj; }
        /** * 查詢緩存 * * @param key 緩存鍵 不可為空 * @param function 如沒(méi)有緩存,調(diào)用該callable函數(shù)返回對(duì)象 可為空 **/ public extends Object> T get(String key, Function<String, T> function) { T obj = null;
        for (CacheProviderService provider : getCacheProviders()) {
        if (obj == null) { obj = provider.get(key, function); } else if (function != null && obj != null) {//查詢并設(shè)置其他緩存提供者程序緩存 provider.get(key, function); }
        //如果callable函數(shù)為空 而緩存對(duì)象不為空 及時(shí)跳出循環(huán)并返回 if (function == null && obj != null) { return obj; }
        }
        return obj; }
        /** * 查詢緩存 * * @param key 緩存鍵 不可為空 * @param function 如沒(méi)有緩存,調(diào)用該callable函數(shù)返回對(duì)象 可為空 * @param funcParm function函數(shù)的調(diào)用參數(shù) **/ public extends Object, M extends Object> T get(String key, Function function, M funcParm) { T obj = null;
        for (CacheProviderService provider : getCacheProviders()) {
        if (obj == null) { obj = provider.get(key, function, funcParm); } else if (function != null && obj != null) {//查詢并設(shè)置其他緩存提供者程序緩存 provider.get(key, function, funcParm); }
        //如果callable函數(shù)為空 而緩存對(duì)象不為空 及時(shí)跳出循環(huán)并返回 if (function == null && obj != null) { return obj; } }
        return obj; }
        /** * 查詢緩存 * * @param key 緩存鍵 不可為空 * @param function 如沒(méi)有緩存,調(diào)用該callable函數(shù)返回對(duì)象 可為空 * @param expireTime 過(guò)期時(shí)間(單位:毫秒) 可為空 **/ public extends Object> T get(String key, Function<String, T> function, long expireTime) { T obj = null;
        for (CacheProviderService provider : getCacheProviders()) {
        if (obj == null) { obj = provider.get(key, function, expireTime); } else if (function != null && obj != null) {//查詢并設(shè)置其他緩存提供者程序緩存 provider.get(key, function, expireTime); }
        //如果callable函數(shù)為空 而緩存對(duì)象不為空 及時(shí)跳出循環(huán)并返回 if (function == null && obj != null) { return obj; } }
        return obj; }
        /** * 查詢緩存 * * @param key 緩存鍵 不可為空 * @param function 如沒(méi)有緩存,調(diào)用該callable函數(shù)返回對(duì)象 可為空 * @param funcParm function函數(shù)的調(diào)用參數(shù) * @param expireTime 過(guò)期時(shí)間(單位:毫秒) 可為空 **/ public extends Object, M extends Object> T get(String key, Function function, M funcParm, long expireTime) { T obj = null;
        for (CacheProviderService provider : getCacheProviders()) {
        if (obj == null) { obj = provider.get(key, function, funcParm, expireTime); } else if (function != null && obj != null) {//查詢并設(shè)置其他緩存提供者程序緩存 provider.get(key, function, funcParm, expireTime); }
        //如果callable函數(shù)為空 而緩存對(duì)象不為空 及時(shí)跳出循環(huán)并返回 if (function == null && obj != null) { return obj; } }
        return obj; }
        /** * 設(shè)置緩存鍵值 直接向緩存中插入或覆蓋值 * * @param key 緩存鍵 不可為空 * @param obj 緩存值 不可為空 **/ public extends Object> void set(String key, T obj) {
        //key = generateVerKey(key);//構(gòu)造帶版本的緩存鍵
        for (CacheProviderService provider : getCacheProviders()) {
        provider.set(key, obj);
        } }
        /** * 設(shè)置緩存鍵值 直接向緩存中插入或覆蓋值 * * @param key 緩存鍵 不可為空 * @param obj 緩存值 不可為空 * @param expireTime 過(guò)期時(shí)間(單位:毫秒) 可為空 **/ public extends Object> void set(String key, T obj, Long expireTime) {
        //key = generateVerKey(key);//構(gòu)造帶版本的緩存鍵
        for (CacheProviderService provider : getCacheProviders()) {
        provider.set(key, obj, expireTime);
        } }
        /** * 移除緩存 * * @param key 緩存鍵 不可為空 **/ public void remove(String key) {
        //key = generateVerKey(key);//構(gòu)造帶版本的緩存鍵
        if (StringUtils.isEmpty(key) == true) { return; }
        for (CacheProviderService provider : getCacheProviders()) {
        provider.remove(key);
        } }
        /** * 是否存在緩存 * * @param key 緩存鍵 不可為空 **/ public boolean contains(String key) { boolean exists = false;
        //key = generateVerKey(key);//構(gòu)造帶版本的緩存鍵
        if (StringUtils.isEmpty(key) == true) { return exists; }
        Object obj = get(key);
        if (obj != null) { exists = true; }
        return exists; }
        /** * 獲取分布式緩存版本號(hào) **/ public String getCacheVersion() { String version = ""; boolean isUseCache = checkUseRedisCache();
        //未啟用Redis緩存 if (isUseCache == false) { return version; }
        version = redisCacheService.get(AppConst.CACHE_VERSION_KEY);
        return version; }
        /** * 重置分布式緩存版本 如果啟用分布式緩存,設(shè)置緩存版本 **/ public String resetCacheVersion() { String version = ""; boolean isUseCache = checkUseRedisCache();
        //未啟用Redis緩存 if (isUseCache == false) { return version; }
        //設(shè)置緩存版本 version = String.valueOf(Math.abs(UUID.randomUUID().hashCode())); redisCacheService.set(AppConst.CACHE_VERSION_KEY, version);
        return version; }
        /** * 如果啟用分布式緩存,獲取緩存版本,重置查詢的緩存key,可以實(shí)現(xiàn)相對(duì)實(shí)時(shí)的緩存過(guò)期控制 *

        * 如沒(méi)有啟用分布式緩存,緩存key不做修改,直接返回 **/ public String generateVerKey(String key) {
        String result = key; if (StringUtils.isEmpty(key) == true) { return result; }
        boolean isUseCache = checkUseRedisCache();
        //沒(méi)有啟用分布式緩存,緩存key不做修改,直接返回 if (isUseCache == false) { return result; }
        String version = redisCacheService.get(AppConst.CACHE_VERSION_KEY); if (StringUtils.isEmpty(version) == true) { return result; }
        result = String.format("%s_%s", result, version);
        return result; }
        /** * 驗(yàn)證是否啟用分布式緩存 **/ private boolean checkUseRedisCache() { boolean isUseCache = false; String strIsUseCache = ConfigUtil.getConfigVal(AppField.IS_USE_REDIS_CACHE);
        isUseCache = "1".equalsIgnoreCase(strIsUseCache);
        return isUseCache; }}

        單元測(cè)試如下:

        @Test    public void testCacheVerson() throws Exception {
        String version = cacheBuilder.getCacheVersion(); System.out.println(String.format("當(dāng)前緩存版本:%s", version));
        String cacheKey = cacheBuilder.generateVerKey("goods778899");
        GoodsVO goodsVO = new GoodsVO(); goodsVO.setGoodsId(UUID.randomUUID().toString()); goodsVO.setCreateTime(new Date()); goodsVO.setCreateDate(new DateTime(new Date())); goodsVO.setGoodsType(1024); goodsVO.setGoodsCode("123456789"); goodsVO.setGoodsName("我的測(cè)試商品");
        cacheBuilder.set(cacheKey, goodsVO);
        GoodsVO goodsVO1 = cacheBuilder.get(cacheKey);
        Assert.assertNotNull(goodsVO1);
        version = cacheBuilder.resetCacheVersion(); System.out.println(String.format("重置后的緩存版本:%s", version));

        cacheKey = cacheBuilder.generateVerKey("goods112233");
        cacheBuilder.set(cacheKey, goodsVO);
        GoodsVO goodsVO2 = cacheBuilder.get(cacheKey);
        Assert.assertNotNull(goodsVO2);
        Assert.assertTrue("兩個(gè)緩存對(duì)象的主鍵相同", goodsVO1.getGoodsId().equals(goodsVO2.getGoodsId())); }

        一個(gè)滿足基本功能的多級(jí)緩存系統(tǒng)就好了。

        在Spring Boot應(yīng)用中使用緩存則非常簡(jiǎn)潔,選擇調(diào)用上面包裝好的緩存接口即可。

        String cacheKey = _cacheBuilder.generateVerKey("com.power.demo.apiservice.impl.getgoodsbyid." + request.getGoodsId());
        GoodsVO goodsVO = _cacheBuilder.get(cacheKey, _goodsService::getGoodsByGoodsId, request.getGoodsId());

        到這里Spring Boot業(yè)務(wù)系統(tǒng)開(kāi)發(fā)中最常用到的ORM,緩存和隊(duì)列三板斧就介紹完了。

        在開(kāi)發(fā)的過(guò)程中你會(huì)發(fā)現(xiàn),Java真的是非常非常中規(guī)中矩的語(yǔ)言,你需要不斷折騰并熟悉常見(jiàn)的開(kāi)源中間件和工具,開(kāi)源的輪子實(shí)在是太豐富,多嘗試幾個(gè),實(shí)踐出真知。

        今天就分享這么多,關(guān)于SpringBoot緩存系統(tǒng),學(xué)會(huì)了多少? ?


        ---END---
        文末福利



        瀏覽 57
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        評(píng)論
        圖片
        表情
        推薦
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

          <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            豆花av电影在线 成人无码一区二区三区 | 成人网站在线观看视频 | 久久葡京 | 人成视频在线免费观看 | 日本午夜大片 | 最新中文字幕免费在线观看 | 日本黄色视频大全 | dy888夜精品国产专区 | 国产伦精品一区二区三区视频网站 | 91女学生在线观看 |