大數據

賊厲害,手擼的 SpringBoot 緩存系統,性能槓槓的!

QQ圖片20201111221022.jpg

一、通用緩存接口

二、本地緩存

三、分佈式緩存

四、緩存“及時”過期問題

五、二級緩存

緩存是最直接有效提升系統性能的手段之一。個人認為用好用對緩存是優秀程序員的必備基本素質。

本文結合實際開發經驗,從簡單概念原理和代碼入手,一步一步搭建一個簡單的二級緩存系統。

一、通用緩存接口
1、緩存基礎算法
(1)、FIFO(First In First Out),先進先出,和OS裡的FIFO思路相同,如果一個數據最先進入緩存中,當緩存滿的時候,應當把最先進入緩存的數據給移除掉。(2)、LFU(Least Frequently Used),最不經常使用,如果一個數據在最近一段時間內使用次數很少,那麼在將來一段時間內被使用的可能性也很小。(3)、LRU(Least Recently Used),最近最少使用,如果一個數據在最近一段時間沒有被訪問到,那麼在將來它被訪問的可能性也很小。也就是說,當限定的空間已存滿數據時,應當把最久沒有被訪問到的數據移除。

2、接口定義
簡單定義緩存接口,大致可以抽象如下:

package com.power.demo.cache.contract;

import java.util.function.Function;

/**
 * 緩存提供者接口
 **/
public interface CacheProviderService {

    /**
     * 查詢緩存
     *
     * @param key 緩存鍵 不可為空
     **/
    <T extends Object> T get(String key);

    /**
     * 查詢緩存
     *
     * @param key      緩存鍵 不可為空
     * @param function 如沒有緩存,調用該callable函數返回對象 可為空
     **/
    <T extends Object> T get(String key, Function<String, T> function);

    /**
     * 查詢緩存
     *
     * @param key      緩存鍵 不可為空
     * @param function 如沒有緩存,調用該callable函數返回對象 可為空
     * @param funcParm function函數的調用參數
     **/
    <T extends Object, M extends Object> T get(String key, Function<M, T> function, M funcParm);

    /**
     * 查詢緩存
     *
     * @param key        緩存鍵 不可為空
     * @param function   如沒有緩存,調用該callable函數返回對象 可為空
     * @param expireTime 過期時間(單位:毫秒) 可為空
     **/
    <T extends Object> T get(String key, Function<String, T> function, Long expireTime);

    /**
     * 查詢緩存
     *
     * @param key        緩存鍵 不可為空
     * @param function   如沒有緩存,調用該callable函數返回對象 可為空
     * @param funcParm   function函數的調用參數
     * @param expireTime 過期時間(單位:毫秒) 可為空
     **/
    <T extends Object, M extends Object> T get(String key, Function<M, T> function, M funcParm, Long expireTime);

    /**
     * 設置緩存鍵值
     *
     * @param key 緩存鍵 不可為空
     * @param obj 緩存值 不可為空
     **/
    <T extends Object> void set(String key, T obj);

    /**
     * 設置緩存鍵值
     *
     * @param key        緩存鍵 不可為空
     * @param obj        緩存值 不可為空
     * @param expireTime 過期時間(單位:毫秒) 可為空
     **/
    <T extends Object> void set(String key, T obj, Long expireTime);

    /**
     * 移除緩存
     *
     * @param key 緩存鍵 不可為空
     **/
    void remove(String key);

    /**
     * 是否存在緩存
     *
     * @param key 緩存鍵 不可為空
     **/
    boolean contains(String key);
}

注意,這裡列出的只是常見緩存功能接口,一些在特殊場景下用到的統計類的接口、分佈式鎖、自增(減)等功能不在討論範圍之內。

Get相關方法,注意多個參數的情況,緩存接口裡面傳人的Function,這是Java8提供的函數式接口,雖然支持的入參個數有限(這裡你會非常懷念.NET下的Func委託),但是僅對Java這個語言來說,這真是一個重大的進步^_^。

接口定義好了,下面就要實現緩存提供者程序了。按照存儲類型的不同,本文簡單實現最常用的兩種緩存提供者:本地緩存和分佈式緩存。

二、本地緩存
本地緩存,也就是JVM級別的緩存(本地緩存可以認為是直接在進程內通信調用,而分佈式緩存則需要通過網絡進行跨進程通信調用),一般有很多種實現方式,比如直接使用Hashtable、ConcurrentHashMap等天生線程安全的集合作為緩存容器,或者使用一些成熟的開源組件,如EhCache、Guava Cache等。本文選擇上手簡單的Guava緩存。

1、什麼是Guava
Guava,簡單來說就是一個開發類庫,且是一個非常豐富強大的開發工具包,號稱可以讓使用Java語言更令人愉悅,主要包括基本工具類庫和接口、緩存、發佈訂閱風格的事件總線等。在實際開發中,我用的最多的是集合、緩存和常用類型幫助類,很多人都對這個類庫稱讚有加。

2、添加依賴

<dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
        </dependency>

3、實現接口

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;

/*
 * 本地緩存提供者服務 (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)//最後一次寫入後的一段時間移出
                //.expireAfterAccess(AppConst.CACHE_MINUTE, TimeUnit.MILLISECONDS) //最後一次訪問後的一段時間移出
                .recordStats()//開啟統計功能
                .build();

        _cacheMap.put(String.valueOf(AppConst.CACHE_MINUTE), cacheContainer);
    }

    /**
     * 查詢緩存
     *
     * @param key 緩存鍵 不可為空
     **/
    public <T extends Object> T get(String key) {
        T obj = get(key, null, null, AppConst.CACHE_MINUTE);

        return obj;
    }

    /**
     * 查詢緩存
     *
     * @param key      緩存鍵 不可為空
     * @param function 如沒有緩存,調用該callable函數返回對象 可為空
     **/
    public <T 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 如沒有緩存,調用該callable函數返回對象 可為空
     * @param funcParm function函數的調用參數
     **/
    public <T extends Object, M extends Object> T get(String key, Function<M, T> function, M funcParm) {
        T obj = get(key, function, funcParm, AppConst.CACHE_MINUTE);

        return obj;
    }

    /**
     * 查詢緩存
     *
     * @param key        緩存鍵 不可為空
     * @param function   如沒有緩存,調用該callable函數返回對象 可為空
     * @param expireTime 過期時間(單位:毫秒) 可為空
     **/
    public <T 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   如沒有緩存,調用該callable函數返回對象 可為空
     * @param funcParm   function函數的調用參數
     * @param expireTime 過期時間(單位:毫秒) 可為空
     **/
    public <T extends Object, M extends Object> T get(String key, Function<M, T> 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;
    }

    /**
     * 設置緩存鍵值  直接向緩存中插入值,這會直接覆蓋掉給定鍵之前映射的值
     *
     * @param key 緩存鍵 不可為空
     * @param obj 緩存值 不可為空
     **/
    public <T extends Object> void set(String key, T obj) {

        set(key, obj, AppConst.CACHE_MINUTE);
    }

    /**
     * 設置緩存鍵值  直接向緩存中插入值,這會直接覆蓋掉給定鍵之前映射的值
     *
     * @param key        緩存鍵 不可為空
     * @param obj        緩存值 不可為空
     * @param expireTime 過期時間(單位:毫秒) 可為空
     **/
    public <T 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)//最後一次寫入後的一段時間移出
                    //.expireAfterAccess(AppConst.CACHE_MINUTE, TimeUnit.MILLISECONDS) //最後一次訪問後的一段時間移出
                    .recordStats()//開啟統計功能
                    .build();

            _cacheMap.put(mapKey, cacheContainer);

        } finally {
            lock.unlock();
        }

        return cacheContainer;
    }

    /**
     * 獲取過期時間 單位:毫秒
     *
     * @param expireTime 傳人的過期時間 單位毫秒 如小於1分鐘,默認為10分鐘
     **/
    private Long getExpireTime(Long expireTime) {
        Long result = expireTime;
        if (expireTime == null || expireTime < AppConst.CACHE_MINUTE / 10) {
            result = AppConst.CACHE_MINUTE;
        }

        return result;
    }
}

4、注意事項
Guava Cache初始化容器時,支持緩存過期策略,類似FIFO、LRU和LFU等算法。

expireAfterWrite:最後一次寫入後的一段時間移出。

expireAfterAccess:最後一次訪問後的一段時間移出。

Guava Cache對緩存過期時間的設置實在不夠友好。常見的應用場景,比如,有些幾乎不變的基礎數據緩存1天,有些熱點數據緩存2小時,有些會話數據緩存5分鐘等等。

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

但是Guava Cache不是這個實現思路,如果緩存的過期時間不同,Guava的CacheBuilder要初始化多份Cache實例。

好在我在實現的時候注意到了這個問題,並且提供瞭解決方案,可以看到getCacheContainer這個函數,根據過期時長做緩存實例判斷,就算不同過期時間的多實例緩存也是完全沒有問題的。

三、分佈式緩存
分佈式緩存產品非常多,本文使用應用普遍的Redis,在Spring Boot應用中使用Redis非常簡單。

1、什麼是Redis
Redis是一款開源(BSD許可)的、用C語言寫成的高性能的鍵-值存儲(key-value store)。它常被稱作是一款數據結構服務器(data structure server)。它可以被用作緩存、消息中間件和數據庫,在很多應用中,經常看到有人選擇使用Redis做緩存,實現分佈式鎖和分佈式Session等。作為緩存系統時,和經典的KV結構的Memcached非常相似,但又有很多不同。Redis支持豐富的數據類型。Redis的鍵值可以包括字符串(strings)類型,同時它還包括哈希(hashes)、列表(lists)、集合(sets)和有序集合(sorted sets)等數據類型。對於這些數據類型,你可以執行原子操作。例如:對字符串進行附加操作(append);遞增哈希中的值;向列表中增加元素;計算集合的交集、並集與差集等。

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

為了獲得優異的性能,Redis採用了內存中(in-memory)數據集(dataset)的方式。同時,Redis支持數據的持久化,你可以每隔一段時間將數據集轉存到磁盤上(snapshot),或者在日誌尾部追加每一條操作命令(append only file,aof)。Redis同樣支持主從複製(master-slave replication),並且具有非常快速的非阻塞首次同步( non-blocking first synchronization)、網絡斷開自動重連等功能。同時Redis還具有其它一些特性,其中包括簡單的事物支持、發佈訂閱 ( pub/sub)、管道(pipeline)和虛擬內存(vm)等 。

2、添加依賴

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

3、配置Redis
在application.properties配置文件中,配置Redis常用參數:

Redis緩存相關配置

Redis數據庫索引(默認為0)

spring.redis.database=0

Redis服務器地址

spring.redis.host=127.0.0.1

Redis服務器端口

spring.redis.port=6379

Redis服務器密碼(默認為空)

spring.redis.password=123321

Redis連接超時時間 默認:5分鐘(單位:毫秒)

spring.redis.timeout=300000ms

Redis連接池最大連接數(使用負值表示沒有限制)

spring.redis.jedis.pool.max-active=512

Redis連接池中的最小空閒連接

spring.redis.jedis.pool.min-idle=0

Redis連接池中的最大空閒連接

spring.redis.jedis.pool.max-idle=8

Redis連接池最大阻塞等待時間(使用負值表示沒有限制)

spring.redis.jedis.pool.max-wait=-1ms

常見的需要注意的是最大連接數(spring.redis.jedis.pool.max-active )和超時時間(spring.redis.jedis.pool.max-wait)。Redis在生產環境中出現故障的頻率經常和這兩個參數息息相關。

接著定義一個繼承自CachingConfigurerSupport(請注意cacheManager和keyGenerator這兩個方法在子類的實現)的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
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {

    @Bean
    public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
        return RedisCacheManager.create(connectionFactory);
    }

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();

        //Jedis的Key和Value的序列化器默認值是JdkSerializationRedisSerializer
        //經實驗,JdkSerializationRedisSerializer通過RedisDesktopManager看到的鍵值對不能正常解析

        //設置key的序列化器
        template.setKeySerializer(new StringRedisSerializer());

        ////設置value的序列化器  默認值是JdkSerializationRedisSerializer
        //使用Jackson序列化器的問題是,複雜對象可能序列化失敗,比如JodaTime的DateTime類型

        //        //使用Jackson2,將對象序列化為JSON
        //        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        //        //json轉對象類,不設置默認的會將json轉成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連接工廠設置到模板類中
        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這個類上加上@EnableCaching這個註解,這個註解會被Spring發現,並且會創建一個切面(aspect) 並觸發Spring緩存註解的切點(pointcut)。據所使用的註解以及緩存的狀態,這個切面會從緩存中獲取數據,將數據添加到緩存之中或者從緩存中移除某個值。cacheManager方法,申明一個緩存管理器(CacheManager)的bean,作用就是@EnableCaching這個切面在新增緩存或者刪除緩存的時候會調用這個緩存管理器的方法。keyGenerator方法,可以根據需求自定義緩存key生成策略。

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

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

JdkSerializationRedisSerializer:使用Java序列化;StringRedisSerializer:序列化String類型的key和value;GenericToStringSerializer:使用Spring轉換服務進行序列化;JacksonJsonRedisSerializer:使用Jackson 1,將對象序列化為JSON;Jackson2JsonRedisSerializer:使用Jackson 2,將對象序列化為JSON;OxmSerializer:使用Spring O/X映射的編排器和解排器(marshaler和unmarshaler)實現序列化,用於XML序列化;

注意:RedisTemplate的鍵和值序列化器,默認情況下都是JdkSerializationRedisSerializer,它們都可以自定義設置序列化器。推薦將字符串鍵使用StringRedisSerializer序列化器,因為運維的時候好排查問題,JDK序列化器的也能識別,但是可讀性稍差(是因為緩存服務器沒有JRE嗎?),見如下效果:

image.png

而值序列化器則要複雜的多,很多人推薦使用Jackson2JsonRedisSerializer序列化器,但是實際開發過程中,經常有人碰到反序列化錯誤,經過排查多數都和Jackson2JsonRedisSerializer這個序列化器有關。

4、實現接口
使用RedisTemplate,在Spring Boot中調用Redis接口比直接調用Jedis簡單多了。

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 RedisTemplate<Serializable, Object> redisTemplate;

    /**
     * 查詢緩存
     *
     * @param key 緩存鍵 不可為空
     **/
    public <T extends Object> T get(String key) {
        T obj = get(key, null, null, AppConst.CACHE_MINUTE);

        return obj;
    }

    /**
     * 查詢緩存
     *
     * @param key      緩存鍵 不可為空
     * @param function 如沒有緩存,調用該callable函數返回對象 可為空
     **/
    public <T 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 如沒有緩存,調用該callable函數返回對象 可為空
     * @param funcParm function函數的調用參數
     **/
    public <T extends Object, M extends Object> T get(String key, Function<M, T> function, M funcParm) {
        T obj = get(key, function, funcParm, AppConst.CACHE_MINUTE);

        return obj;
    }

    /**
     * 查詢緩存
     *
     * @param key        緩存鍵 不可為空
     * @param function   如沒有緩存,調用該callable函數返回對象 可為空
     * @param expireTime 過期時間(單位:毫秒) 可為空
     **/
    public <T 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   如沒有緩存,調用該callable函數返回對象 可為空
     * @param funcParm   function函數的調用參數
     * @param expireTime 過期時間(單位:毫秒) 可為空
     **/
    public <T extends Object, M extends Object> T get(String key, Function<M, T> function, M funcParm, Long expireTime) {
        T obj = null;
        if (StringUtils.isEmpty(key) == true) {
            return obj;
        }

        expireTime = getExpireTime(expireTime);

        try {

            ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
            obj = (T) operations.get(key);
            if (function != null && obj == null) {
                obj = function.apply(funcParm);
                if (obj != null) {
                    set(key, obj, expireTime);//設置緩存信息
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        return obj;
    }

    /**
     * 設置緩存鍵值  直接向緩存中插入值,這會直接覆蓋掉給定鍵之前映射的值
     *
     * @param key 緩存鍵 不可為空
     * @param obj 緩存值 不可為空
     **/
    public <T extends Object> void set(String key, T obj) {

        set(key, obj, AppConst.CACHE_MINUTE);
    }

    /**
     * 設置緩存鍵值  直接向緩存中插入值,這會直接覆蓋掉給定鍵之前映射的值
     *
     * @param key        緩存鍵 不可為空
     * @param obj        緩存值 不可為空
     * @param expireTime 過期時間(單位:毫秒) 可為空
     **/
    public <T extends Object> void set(String key, T obj, Long expireTime) {
        if (StringUtils.isEmpty(key) == true) {
            return;
        }

        if (obj == null) {
            return;
        }

        expireTime = getExpireTime(expireTime);

        ValueOperations<Serializable, Object> 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;
    }

    /**
     * 獲取過期時間 單位:毫秒
     *
     * @param expireTime 傳人的過期時間 單位毫秒 如小於1分鐘,默認為10分鐘
     **/
    private Long getExpireTime(Long expireTime) {
        Long result = expireTime;
        if (expireTime == null || expireTime < AppConst.CACHE_MINUTE / 10) {
            result = AppConst.CACHE_MINUTE;
        }

        return result;
    }
}

注意:很多教程裡都講到通過註解的方式(@Cacheable,@CachePut、@CacheEvict和@Caching)實現數據緩存,根據實踐,我個人是不推崇這種使用方式的。

四、緩存“及時”過期問題
這個也是開發和運維過程中非常經典的問題。

有些公司寫緩存客戶端的時候,會給每個團隊分別定義一個Area,但是這個只能做到緩存鍵的分佈區分,不能保證緩存“實時”有效的過期。

多年以前我寫過一篇結合實際情況的文章,也就是加上緩存版本,請猛擊這裡 ,算是提供了一種相對有效的方案,不過高併發站點要慎重,防止發生雪崩效應。

Redis還有一些其他常見問題,比如:Redis的字符串類型Key和Value都有限制,且都是不能超過512M,請猛擊這裡。還有最大連接數和超時時間設置等問題,本文就不再一一列舉了。

五、二級緩存
在配置文件中,加上緩存提供者開關:

是否啟用本地緩存

spring.power.isuselocalcache=1

是否啟用Redis緩存

spring.power.isuserediscache=1

緩存提供者程序都實現好了,我們會再包裝一個調用外觀類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;

/*
 * 支持多緩存提供程序多級緩存的緩存幫助類
 * */
@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<CacheProviderService> _listCacheProvider = Lists.newArrayList();

    private static final Lock providerLock = new ReentrantLock();

    /**
     * 初始化緩存提供者 默認優先級:先本地緩存,後分佈式緩存
     **/
    private List<CacheProviderService> 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();//設置分佈式緩存版本號
            }

            PowerLogger.info("初始化緩存提供者成功,共有" + _listCacheProvider.size() + "個");
        } catch (Exception e) {
            e.printStackTrace();

            _listCacheProvider = Lists.newArrayList();

            PowerLogger.error("初始化緩存提供者發生異常:{}", e);
        } finally {
            providerLock.unlock();
        }

        return _listCacheProvider;
    }

    /**
     * 查詢緩存
     *
     * @param key 緩存鍵 不可為空
     **/
    public <T extends Object> T get(String key) {
        T obj = null;

        //key = generateVerKey(key);//構造帶版本的緩存鍵

        for (CacheProviderService provider : getCacheProviders()) {

            obj = provider.get(key);

            if (obj != null) {
                return obj;
            }
        }

        return obj;
    }

    /**
     * 查詢緩存
     *
     * @param key      緩存鍵 不可為空
     * @param function 如沒有緩存,調用該callable函數返回對象 可為空
     **/
    public <T 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) {//查詢並設置其他緩存提供者程序緩存
                provider.get(key, function);
            }

            //如果callable函數為空 而緩存對象不為空 及時跳出循環並返回
            if (function == null && obj != null) {
                return obj;
            }

        }

        return obj;
    }

    /**
     * 查詢緩存
     *
     * @param key      緩存鍵 不可為空
     * @param function 如沒有緩存,調用該callable函數返回對象 可為空
     * @param funcParm function函數的調用參數
     **/
    public <T extends Object, M extends Object> T get(String key, Function<M, T> 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) {//查詢並設置其他緩存提供者程序緩存
                provider.get(key, function, funcParm);
            }

            //如果callable函數為空 而緩存對象不為空 及時跳出循環並返回
            if (function == null && obj != null) {
                return obj;
            }
        }

        return obj;
    }

    /**
     * 查詢緩存
     *
     * @param key        緩存鍵 不可為空
     * @param function   如沒有緩存,調用該callable函數返回對象 可為空
     * @param expireTime 過期時間(單位:毫秒) 可為空
     **/
    public <T 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) {//查詢並設置其他緩存提供者程序緩存
                provider.get(key, function, expireTime);
            }

            //如果callable函數為空 而緩存對象不為空 及時跳出循環並返回
            if (function == null && obj != null) {
                return obj;
            }
        }

        return obj;
    }

    /**
     * 查詢緩存
     *
     * @param key        緩存鍵 不可為空
     * @param function   如沒有緩存,調用該callable函數返回對象 可為空
     * @param funcParm   function函數的調用參數
     * @param expireTime 過期時間(單位:毫秒) 可為空
     **/
    public <T extends Object, M extends Object> T get(String key, Function<M, T> 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) {//查詢並設置其他緩存提供者程序緩存
                provider.get(key, function, funcParm, expireTime);
            }

            //如果callable函數為空 而緩存對象不為空 及時跳出循環並返回
            if (function == null && obj != null) {
                return obj;
            }
        }

        return obj;
    }

    /**
     * 設置緩存鍵值  直接向緩存中插入或覆蓋值
     *
     * @param key 緩存鍵 不可為空
     * @param obj 緩存值 不可為空
     **/
    public <T extends Object> void set(String key, T obj) {

        //key = generateVerKey(key);//構造帶版本的緩存鍵

        for (CacheProviderService provider : getCacheProviders()) {

            provider.set(key, obj);

        }
    }

    /**
     * 設置緩存鍵值  直接向緩存中插入或覆蓋值
     *
     * @param key        緩存鍵 不可為空
     * @param obj        緩存值 不可為空
     * @param expireTime 過期時間(單位:毫秒) 可為空
     **/
    public <T extends Object> void set(String key, T obj, Long expireTime) {

        //key = generateVerKey(key);//構造帶版本的緩存鍵

        for (CacheProviderService provider : getCacheProviders()) {

            provider.set(key, obj, expireTime);

        }
    }

    /**
     * 移除緩存
     *
     * @param key 緩存鍵 不可為空
     **/
    public void remove(String key) {

        //key = generateVerKey(key);//構造帶版本的緩存鍵

        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);//構造帶版本的緩存鍵

        if (StringUtils.isEmpty(key) == true) {
            return exists;
        }

        Object obj = get(key);

        if (obj != null) {
            exists = true;
        }

        return exists;
    }

    /**
     * 獲取分佈式緩存版本號
     **/
    public String getCacheVersion() {
        String version = "";
        boolean isUseCache = checkUseRedisCache();

        //未啟用Redis緩存
        if (isUseCache == false) {
            return version;
        }

        version = redisCacheService.get(AppConst.CACHE_VERSION_KEY);

        return version;
    }

    /**
     * 重置分佈式緩存版本  如果啟用分佈式緩存,設置緩存版本
     **/
    public String resetCacheVersion() {
        String version = "";
        boolean isUseCache = checkUseRedisCache();

        //未啟用Redis緩存
        if (isUseCache == false) {
            return version;
        }

        //設置緩存版本
        version = String.valueOf(Math.abs(UUID.randomUUID().hashCode()));
        redisCacheService.set(AppConst.CACHE_VERSION_KEY, version);

        return version;
    }

    /**
     * 如果啟用分佈式緩存,獲取緩存版本,重置查詢的緩存key,可以實現相對實時的緩存過期控制
     * <p>
     * 如沒有啟用分佈式緩存,緩存key不做修改,直接返回
     **/
    public String generateVerKey(String key) {

        String result = key;
        if (StringUtils.isEmpty(key) == true) {
            return result;
        }

        boolean isUseCache = checkUseRedisCache();

        //沒有啟用分佈式緩存,緩存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;
    }

    /**
     * 驗證是否啟用分佈式緩存
     **/
    private boolean checkUseRedisCache() {
        boolean isUseCache = false;
        String strIsUseCache = ConfigUtil.getConfigVal(AppField.IS_USE_REDIS_CACHE);

        isUseCache = "1".equalsIgnoreCase(strIsUseCache);

        return isUseCache;
    }
}

單元測試如下:

@Test
   public void testCacheVerson() throws Exception {

       String version = cacheBuilder.getCacheVersion();
       System.out.println(String.format("當前緩存版本:%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("我的測試商品");

       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("兩個緩存對象的主鍵相同", goodsVO1.getGoodsId().equals(goodsVO2.getGoodsId()));
   }

一個滿足基本功能的多級緩存系統就好了。

在Spring Boot應用中使用緩存則非常簡潔,選擇調用上面包裝好的緩存接口即可。

String cacheKey = _cacheBuilder.generateVerKey("com.power.demo.apiservice.impl.getgoodsbyid." + request.getGoodsId());

GoodsVO goodsVO = _cacheBuilder.get(cacheKey, _goodsService::getGoodsByGoodsId, request.getGoodsId());

到這裡Spring Boot業務系統開發中最常用到的ORM,緩存和隊列三板斧就介紹完了。

在開發的過程中你會發現,Java真的是非常非常中規中矩的語言,你需要不斷折騰並熟悉常見的開源中間件和工具,開源的輪子實在是太豐富,多嘗試幾個,實踐出真知。

Java 的知識面非常廣,面試問的涉及也非常廣泛,重點包括:Java 基礎、Java 併發,JVM、MySQL、數據結構、算法、Spring、微服務、MQ 等等,涉及的知識點何其龐大,所以我們在複習的時候也往往無從下手,今天小編給大家帶來一套 Java 面試題,題庫非常全面,包括 Java 基礎、Java 集合、JVM、Java 併發、Spring全家桶、Redis、MySQL、Dubbo、Netty、MQ 等等,包含 Java 後端知識點 2000 + ,部分如下:

資料獲取方式:關注公種浩:“程序員白楠楠”

Leave a Reply

Your email address will not be published. Required fields are marked *