Spring Boot2.X(一)使用Spring Cache + Redis 實現緩存

一、Spring Cache

1、基本介紹

基於註釋(annotation)的緩存(cache)技術是在Spring 3.1 引入的,它本質上不是一個具體的緩存實現方案(例如 EHCache),而是一個對緩存使用的抽象,通過在既有代碼中添加少量它定義的各種 annotation,就能夠達到緩存方法的返回對象的效果。

Spring 的緩存技術還具備相當的靈活性,不僅能夠使用 SpEL(Spring Expression Language)來定義緩存的 key 和各種 condition,還提供開箱即用的緩存臨時存儲方案,也支持和主流的專業緩存例如Redis集成。

主要特點如下:

  • 通過少量的配置 annotation 註釋即可使得既有代碼支持緩存
  • 支持 Spring Express Language,能使用對象的任何屬性或者方法來定義緩存的 key 和 condition
  • 支持 AspectJ,並通過其實現任何方法的緩存支持
  • 支持自定義 key 和自定義緩存管理者,具有相當的靈活性和擴展性
2、常用緩存註解

spring cache 常用註解: @Cacheable、@CachePut 和 @CacheEvict

2.1、@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”)

2.2、@CachePut

作用: 主要針對方法配置,能夠根據方法的請求參數對其結果進行緩存。和 @Cacheable 不同的是,它每次都會觸發真實方法的調用,此註解常被用於更新緩存使用。

參數介紹:

value:緩存的名稱

@CachePut(value=”mycache”)
@CachePut(value={”cache1”,”cache2”}

key:緩存的 key

@CachePut(value=”testcache”,key=”#userName”)

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

2.3、@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 cache默認緩存

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,該註解主要是用於spring framework中的註解驅動的緩存管理。如果使用了該註解,則就不需要在XML文件中配置cache manager了。

@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中使用Spring cache + redis緩存

Spring Cache集成redis的運行原理

Spring緩存抽象模塊通過CacheManager來創建、管理實際緩存組件,當SpringBoot應用程序引入spring-boot-starter-data-redis依賴後,容器中將註冊的是CacheManager實例RedisCacheManager對象,RedisCacheManager來負責創建RedisCache作爲緩存管理組件,由RedisCache操作redis服務器實現緩存數據操作。

引入redis依賴

在pom文件中引入redis

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

redis配置如下
在application.properties配置文件中增加redis配置

#redis配置
#Redis數據庫索引(緩存將使用此索引編號的數據庫)
spring.redis.database=10
#Redis服務器地址  
spring.redis.host=127.0.0.1
#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

設置緩存生存時間

我們可以對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("redisTemplate")
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
      	redisTemplate.setKeySerializer(new StringRedisSerializer());
         // KryoRedisSerializer 替換默認序列化
        redisTemplate.setDefaultSerializer(new KryoRedisSerializer());
        return redisTemplate;
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章