spring cache實現方法緩存(spring boot + redis)

本文代碼示例:https://gitee.com/imlichao/redis_cache-examplehtml

分析

    緩存是分佈式系統中的重要組件,主要解決高併發,大數據場景下,熱點數據訪問的性能問題。提供高性能的數據快速訪問。本文主要介紹基於springboot 框架下使用spring cache和redis進行方法緩存的方案。java

    方法緩存比較適合於靜態頁面或查詢結果複用性較高的業務。舉個例子,一個接口負責查詢首頁廣告,若是每一個用戶看到的首頁廣告都是同樣的,那麼就能夠直接從緩存中讀取大大減小了系統性能消耗。對於不適合使用緩存的業務也舉個例子,一個接口負責查詢離當前用戶座標最近的商家,顯然用戶座標相同的概率會很是低,因此這種緩存即消耗空間又很難命中。綜上所述,訪問量大且出入參是一個有限集合的業務更加適合緩存。git

    緩存與數據源的一致性也是比較重要的問題。若是數據已經產生變化而緩存不更新,那麼咱們將讀取不到最新的數據。因此業務的實時性要求決定了緩存一致性的實時性。因爲一致性問題須要在更新節點增長大量的緩存失效邏輯,因此咱們的項目目前並無進行改造。當前的方案是緩存在必定時間內自動失效,也就是說緩存設置爲5分鐘失效,那麼修改的內容將在5分鐘後才能被查詢出來。固然這只是在實時性要求不高的業務中使用。推薦一篇解決一致性問題的文章:https://blog.csdn.net/java_dyq/article/details/51997045redis

 

Spring 聲明式緩存管理

API文檔:https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/cache/annotation/package-summary.htmlspring

Spring 3.1 引入了激動人心的基於註釋(annotation)的緩存(cache)技術,它本質上不是一個具體的緩存實現方案(例如 EHCache 或者 OSCache),而是一個對緩存使用的抽象,經過在既有代碼中添加少許它定義的各類 annotation,即可以達到緩存方法的返回對象的效果。數據庫

Spring 的緩存技術還具有至關的靈活性,不只可以使用 SpEL(Spring Expression Language)來定義緩存的 key 和各類 condition,還提供開箱即用的緩存臨時存儲方案,也支持和主流的專業緩存例如 EHCache 集成。json

其特色總結以下:api

  • 經過少許的配置 annotation 註釋便可使得既有代碼支持緩存
  • 支持開箱即用 Out-Of-The-Box,即不用安裝和部署額外第三方組件便可使用緩存
  • 支持 Spring Express Language,能使用對象的任何屬性或者方法來定義緩存的 key 和 condition
  • 支持 AspectJ,並經過其實現任何方法的緩存支持
  • 支持自定義 key 和自定義緩存管理者,具備至關的靈活性和擴展性

 

聲明式緩存經常使用註釋緩存

spring cache 中最主要使用三個註釋標籤,即 @Cacheable、@CachePut 和 @CacheEvictspringboot

@Cacheable 的做用
主要針對方法配置,可以根據方法的請求參數對其結果進行緩存。
@Cacheable 主要的參數
value 緩存的名稱

每個緩存名稱表明一個緩存對象。當一個方法填寫多個緩存名稱時將建立多個緩存對象。當多個方法使用同一緩存名稱時相同參數的緩存會被覆蓋。因此一般狀況咱們使用「包名+類名+方法名」或者使用接口的RequestMapping做爲緩存名稱防止命名重複引發的問題。

單緩存名稱:@Cacheable(value=」mycache」) 
多緩存名稱:@Cacheable(value={」cache1」,」cache2」}

key 緩存的 key

key標記了緩存對象下的每一條緩存。若是不指定key則系統自動按照方法的全部入參生成key,也就是說相同的入參值將會返回一樣的緩存結果。

若是指定key則要按照 SpEL 表達式編寫使用的入參列表。以下列不管方法存在多少個入參,只要userName值一致,則會返回相同的緩存結果。

@Cacheable(value=」testcache」,key=」#userName」)

condition 緩存的條件

知足條件後方法結果纔會被緩存。不填寫則認爲無條件所有緩存。

條件使用 SpEL表達式編寫,返回 true 或者 false,只有爲 true 才進行緩存

以下例,只有用戶名長度大於2時參會進行緩存
@Cacheable(value=」testcache」,condition=」#userName.length()>2」)

 

@CachePut 的做用
主要針對方法配置,可以根據方法的請求參數對其結果進行緩存。和 @Cacheable 不一樣的是,它每次都會觸發真實方法的調用,此註解常被用於更新緩存使用。
@CachePut 主要的參數
value 緩存的名稱

例如:
@CachePut(value=」mycache」) 
@CachePut(value={」cache1」,」cache2」}

key 緩存的 key 例如:
@CachePut(value=」testcache」,key=」#userName」)
condition 緩存的條件 例如:
@CachePut(value=」testcache」,condition=」#userName.length()>2」)

 

@CacheEvict 的做用
主要針對方法配置,可以根據必定的條件對緩存進行清空
@CacheEvict 主要的參數
value 緩存的名稱

刪除指定名稱的緩存對象。必須與下面的其中一個參數配合使用

例如:
@CacheEvict(value=」mycache」) 或者
@CacheEvict(value={」cache1」,」cache2」}

key 緩存的 key

刪除指定key的緩存對象

例如:
@CacheEvict(value=」testcache」,key=」#userName」)

condition 緩存的條件

刪除指定條件的緩存對象

例如:
@CacheEvict(value=」testcache」,condition=」#userName.length()>2」)

allEntries 方法執行後清空全部緩存

缺省爲 false,若是指定爲 true,則方法調用後將當即清空全部緩存。

例如:
@CacheEvict(value=」testcache」,allEntries=true)

beforeInvocation 方法執行前清空全部緩存

缺省爲 false,若是指定爲 true,則在方法尚未執行的時候就清空緩存,缺省狀況下,若是方法執行拋出異常,則不會清空緩存。

例如:
@CacheEvict(value=」testcache」,beforeInvocation=true)

 

spring boot中開啓緩存

Spring Boot 爲咱們提供了多種緩存CacheManager配置方案。默認狀況下會使用基於內存map一種緩存方案ConcurrenMapCacheManager。固然我沒也能夠經過配置使用 Generic、JCache (JSR-107)、EhCache 2.x、Hazelcast、Infinispan、Redis、Guava、Simple等技術進行緩存實現。

這裏使用默認的基於內存的方案進行舉例

引入依賴

在pom文件中引入緩存包

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

啓用緩存

在啓動類增長啓用緩存註解@EnableCaching

@SpringBootApplication 
@EnableCaching //啓用緩存
public class App {
    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }
}

緩存測試方法

測試方法作了一個2秒的延時

public class CacheTest {
    /**
     * 緩存測試方法延時兩秒
     * @param i
     * @return
     */
    @Cacheable(value = "cache_test")
    public String cacheFunction(int i){
        try {
            long time = 2000L;
            Thread.sleep(time);
        } catch (InterruptedException e) {
            throw new IllegalStateException(e);
        }

        return "success"+ i;
    }
}

調用緩存測試方法

這裏須要注意:不能在同一個類中調用被註解緩存了的方法。也就是說緩存調用方法和緩存註解方法不能在一個類中出現。

public class HelloController {
    @Autowired
    CacheTest cacheTest;

    @GetMapping(value = "/")
    public String hello(){
        for(int i=0;i<5;i++){
            System.out.println(new Date() + " " + cacheTest.cacheFunction(i));
        }
        return "/hello";
    }
}

測試結果

咱們能夠看出第一次執行時每間隔2秒打印了一次success

而第二次同一時間所有打印完成

Tue Jun 12 15:35:01 CST 2018 success0
Tue Jun 12 15:35:03 CST 2018 success1
Tue Jun 12 15:35:05 CST 2018 success2
Tue Jun 12 15:35:07 CST 2018 success3
Tue Jun 12 15:35:09 CST 2018 success4

Tue Jun 12 15:35:26 CST 2018 success0
Tue Jun 12 15:35:26 CST 2018 success1
Tue Jun 12 15:35:26 CST 2018 success2
Tue Jun 12 15:35:26 CST 2018 success3
Tue Jun 12 15:35:26 CST 2018 success4

 

spring boot中使用redis緩存

在上例得的基礎上咱們將使用redis做爲緩存的存儲方案。

引入redis依賴

在pom文件中引入redis緩存包

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

這裏須要注意有些資料依賴的資源爲spring-boot-starter-redis,這個依賴在spring boot 1.4版本以後被棄用了,改成使用spring-boot-starter-data-redis。

官方說明:

spring-boot-starter-redis

Starter for using Redis key-value data store with Spring Data Redis and the Jedis client. Deprecated as of 1.4 in favor of spring-boot-starter-data-redis

redis配置

在application.properties配置文件中增長redis配置

#redis配置
#Redis數據庫索引(緩存將使用此索引編號的數據庫)
spring.redis.database=10
#Redis服務器地址  
spring.redis.host=123.56.8.125 
#Redis服務器鏈接端口
spring.redis.port=6379 
#Redis服務器鏈接密碼(默認爲空)  
spring.redis.password=****** 
#鏈接超時時間 毫秒(默認2000)
#請求redis服務的超時時間,這裏注意設置成0時取默認時間2000
spring.redis.timeout=2000
#鏈接池最大鏈接數(使用負值表示沒有限制)  
#建議爲業務指望QPS/一個鏈接的QPS,例如50000/1000=50
#一次命令時間(borrow|return resource+Jedis執行命令+網絡延遲)的平均耗時約爲1ms,一個鏈接的QPS大約是1000
spring.redis.pool.max-active=50 
#鏈接池中的最大空閒鏈接 
#建議和最大鏈接數一致,這樣作的好處是鏈接數從不減小,從而避免了鏈接池伸縮產生的性能開銷。
spring.redis.pool.max-idle=50
#鏈接池中的最小空閒鏈接  
#建議爲0,在無請求的情況下從不建立連接
spring.redis.pool.min-idle=0 
#鏈接池最大阻塞等待時間 毫秒(-1表示沒有限制)  
#建議不要爲-1,鏈接池佔滿後沒法獲取鏈接時將在該時間內阻塞等待,超時後將拋出異常。
spring.redis.pool.max-wait=2000

spring boot使用JedisPool來做爲redis鏈接池。推薦一篇JedisPool資源池優化的文章:

https://yq.aliyun.com/articles/236383

Spring Boot會在偵測到存在Redis的依賴而且Redis的配置是可用的狀況下,使用RedisCacheManager 初始化CacheManager。

設置緩存生存時間

咱們能夠對redis緩存數據指定生存時間從而達到緩存自動失效的目的。

經過建立緩存配置文件類能夠設置緩存各項參數

@Configuration
public class RedisCacheConfig {
    @Bean
    public RedisCacheManager cacheManager(RedisTemplate redisTemplate) {
        //得到redis緩存管理類
        RedisCacheManager redisCacheManager = new RedisCacheManager(redisTemplate);
        // 開啓使用緩存名稱作爲key前綴(這樣全部同名緩存會整理在一塊兒比較容易查找)
        redisCacheManager.setUsePrefix(true);

        //這裏能夠設置一個默認的過時時間 單位是秒
        redisCacheManager.setDefaultExpiration(600L);
        // 設置緩存的過時時間 單位是秒
        Map<String, Long> expires = new HashMap<>();
        expires.put("pub.imlichao.CacheTest.cacheFunction", 100L);
        redisCacheManager.setExpires(expires);

        return redisCacheManager;
    }
}

設置過時時間時也能夠不採用expires.put("pub.imlichao.CacheTest.cacheFunction", 100L)的寫法,而是使用@Cacheable標籤的value值進行聲明,以下

@Configuration
public class RedisCacheConfig {
        ......
        // 設置緩存的過時時間 單位是秒
        Map<String, Long> expires = new HashMap<>();
        expires.put("cache_test", 100L);
        redisCacheManager.setExpires(expires);

        return redisCacheManager;
    }
}

設置緩存序列化方式

redisTemplate 默認的序列化方式爲 jdkSerializeable,咱們也可使用其餘序列化方式來達到不一樣的需求。好比咱們但願緩存的數據具備可讀性就能夠將其序列化爲json格式,json序列化可使用Jackson2JsonRedisSerialize或FastJsonRedisSerializer。若是咱們但願擁有更快的速度和佔用更小的存儲空間推薦使用KryoRedisSerializer進行序列化。

因爲redis緩存對可讀性沒什麼要求,而存儲空間和速度是比較重要的,因此這裏使用KryoRedisSerializer進行對象序列化。

添加Kryo依賴

<dependency>
     <groupId>com.esotericsoftware</groupId>
     <artifactId>kryo</artifactId>
     <version>4.0.2</version>
</dependency>

實現RedisSerializer接口建立KryoRedisSerializer序列化工具

import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.io.Input;
import com.esotericsoftware.kryo.io.Output;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;

import java.io.ByteArrayOutputStream;

public class KryoRedisSerializer<T> implements RedisSerializer<T> {

    public static final byte[] EMPTY_BYTE_ARRAY = new byte[0];

    private static final ThreadLocal<Kryo> kryos = ThreadLocal.withInitial(Kryo::new);

    private Class<T> clazz;

    public KryoRedisSerializer(Class<T> clazz) {
        super();
        this.clazz = clazz;
    }

    @Override
    public byte[] serialize(T t) throws SerializationException {
        if (t == null) {
            return EMPTY_BYTE_ARRAY;
        }

        Kryo kryo = kryos.get();
        kryo.setReferences(false);
        kryo.register(clazz);

        try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
             Output output = new Output(baos)) {
            kryo.writeClassAndObject(output, t);
            output.flush();
            return baos.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
        }

        return EMPTY_BYTE_ARRAY;
    }

    @Override
    public T deserialize(byte[] bytes) throws SerializationException {
        if (bytes == null || bytes.length <= 0) {
            return null;
        }

        Kryo kryo = kryos.get();
        kryo.setReferences(false);
        kryo.register(clazz);

        try (Input input = new Input(bytes)) {
            return (T) kryo.readClassAndObject(input);
        } catch (Exception e) {
            e.printStackTrace();
        }

        return null;
    }

}

修改配置文件替換默認序列化工具

@Configuration
public class RedisCacheConfig {
    @Bean
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);

        // KryoRedisSerializer 替換默認序列化
        KryoRedisSerializer kryoRedisSerializer = new KryoRedisSerializer(Object.class);
        redisTemplate.setValueSerializer(kryoRedisSerializer);
        redisTemplate.setKeySerializer(kryoRedisSerializer);
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }
}

測試結果

咱們能夠看到redis裏面建立了緩存

而且第二次成功的獲取了緩存

Fri Jun 15 10:51:04 CST 2018 success0
Fri Jun 15 10:51:07 CST 2018 success1
Fri Jun 15 10:51:09 CST 2018 success2
Fri Jun 15 10:51:11 CST 2018 success3
Fri Jun 15 10:51:13 CST 2018 success4

Fri Jun 15 10:52:49 CST 2018 success0
Fri Jun 15 10:52:49 CST 2018 success1
Fri Jun 15 10:52:49 CST 2018 success2
Fri Jun 15 10:52:49 CST 2018 success3
Fri Jun 15 10:52:49 CST 2018 success4
相關文章
相關標籤/搜索