1. 程式人生 > >Spring Boot + Redis實現快取

Spring Boot + Redis實現快取

快取作為開發中提高服務效能相對有效的一種方式,在實際開發中得到廣泛使用。在Spring 3.1之前,如果想使用快取,相對是比較麻煩的,往往在業務程式碼中要摻雜快取的邏輯,比如判斷快取是否存在,存在則取快取,不存在在從DB中讀取,然後再講資料存入快取中,使用起來相當不方便。Spring 3.1引入了基於註釋的快取技術,它本質上不是一個具體的快取實現方案(例如EHCache、Redis、MemoryCache),而是一個對快取使用的抽象,通過在程式碼中新增少量註解,即能夠達到快取方法的返回物件的效果。也就是說底層具體快取實現對於開發人員來講是透明的,實現快取和具體業務程式碼的解耦。目前Spring註解支援的快取有java.util.concurrent.ConcurrentMap,Ehcache 2.x,Redis等。本文介紹如何通過Spring註解結合底層Redis實現快取,首先要引入如下兩個包:

spring-boot-starter-data-redis
spring-boot-starter-cache
redis.clients.jedis

1. 專案結構

|   pom.xml
|   springboot-13-redis-cache.iml
+---src
|   +---main
|   |   +---java
|   |   |   \---com
|   |   |       \---zhuoli
|   |   |           \---service
|   |   |               \---springboot
|   |   |                   \---redis
|   |   |                       \---cache
|   |   |                           |   SpringBootRedisCacheApplicationContext.java
|   |   |                           |
|   |   |                           +---controller
|   |   |                           |       UserController.java
|   |   |                           |
|   |   |                           +---repository
|   |   |                           |   +---conf
|   |   |                           |   |       DataSourceConfig.java
|   |   |                           |   |       RedisCacheConfig.java
|   |   |                           |   |
|   |   |                           |   +---mapper
|   |   |                           |   |       UserMapper.java
|   |   |                           |   |
|   |   |                           |   +---model
|   |   |                           |   |       User.java
|   |   |                           |   |       UserExample.java
|   |   |                           |   |
|   |   |                           |   \---service
|   |   |                           |       |   UserRepository.java
|   |   |                           |       |
|   |   |                           |       \---impl
|   |   |                           |               UserRepositoryImpl.java
|   |   |                           |
|   |   |                           \---service
|   |   |                               |   UserControllerService.java
|   |   |                               |
|   |   |                               \---impl
|   |   |                                       UserControllerServiceImpl.java
|   |   |
|   |   \---resources
|   |       |   application.properties
|   |       |
|   |       +---autogen
|   |       |       generatorConfig_zhuoli.xml
|   |       |
|   |       \---base
|   |           \---com
|   |               \---zhuoli
|   |                   \---service
|   |                       \---springboot
|   |                           \---redis
|   |                               \---cache
|   |                                   \---repository
|   |                                       \---mapper
|   |                                               UserMapper.xml
|   |
|   \---test
|       \---java

2. pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.zhuoli.service</groupId>
    <artifactId>springboot-13-redis-cache</artifactId>
    <version>1.0-SNAPSHOT</version>

    <!-- Spring Boot 啟動父依賴 -->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.3.RELEASE</version>
    </parent>

    <build>
        <plugins>
            <plugin>
                <groupId>org.mybatis.generator</groupId>
                <artifactId>mybatis-generator-maven-plugin</artifactId>
                <version>1.3.5</version>
                <!--如果不配置configuration節點,配置檔名字必須為generatorConfig.xml-->
                <configuration>
                    <!--可以自定義generatorConfig檔名-->
                    <configurationFile>src/main/resources/autogen/generatorConfig_zhuoli.xml</configurationFile>
                    <verbose>true</verbose>
                    <overwrite>true</overwrite>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
            </plugin>
        </plugins>
    </build>

    <dependencies>
        <!-- Exclude Spring Boot's Default Logging -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

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

        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.2</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-cache -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-cache</artifactId>
        </dependency>

        <!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>2.9.0</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.2</version>
            <scope>provided</scope>
        </dependency>

    </dependencies>

</project>

3. RedisCache配置

@Configuration
public class RedisCacheConfig {

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration("127.0.0.1", 6379);
        return new JedisConnectionFactory(redisStandaloneConfiguration);
    }

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        return template;
    }

    @Bean
    public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
        return new RedisCacheManager(
                RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory),
                this.getRedisCacheConfigurationWithTtl(600), // 預設策略,未配置的key會使用這個
                this.getRedisCacheConfigurationMap() // 指定key策略
        );
    }

    private Map<String, RedisCacheConfiguration> getRedisCacheConfigurationMap() {
        Map<String, RedisCacheConfiguration> redisCacheConfigurationMap = new HashMap<>();
        redisCacheConfigurationMap.put("user", this.getRedisCacheConfigurationWithTtl(10));
        redisCacheConfigurationMap.put("other", this.getRedisCacheConfigurationWithTtl(18000));

        return redisCacheConfigurationMap;
    }

    private RedisCacheConfiguration getRedisCacheConfigurationWithTtl(Integer seconds) {
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);

        RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig();
        redisCacheConfiguration = redisCacheConfiguration.serializeValuesWith(
                RedisSerializationContext
                        .SerializationPair
                        .fromSerializer(jackson2JsonRedisSerializer)
        ).entryTtl(Duration.ofSeconds(seconds));

        return redisCacheConfiguration;
    }
}

這裡分別講一下RedisCacheConfig中的幾個Bean配置

3.1 RedisConnectionFactory定義

RedisConnectionFactory主要用來定義Redis資料來源連線,最開始我的想法是,通過呼叫JedisConnectionFactory的setHostName等方法去指定資料來源連線的,如下:

@Bean
JedisConnectionFactory jedisConnectionFactory() {
    JedisConnectionFactory jedisConFactory
            = new JedisConnectionFactory();

    jedisConFactory.setHostName("localhost");
    jedisConFactory.setPort(6379);
    return jedisConFactory;
}

但是檢視官方文件發現,Spring Boot 2.0後setHostName、setPort方法都已經過期了,Spring Boot 2.0後使用RedisStandaloneConfiguration替代檢視文件後,RedisConnectionFactory Bean定義如下:

@Bean
public RedisConnectionFactory redisConnectionFactory() {
    RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration("127.0.0.1", 6379);
    return new JedisConnectionFactory(redisStandaloneConfiguration);
}

3.2 定製RedisTemplate

自動配置的RedisTemplate並不能滿足大部分專案的需求,比如我們基本都需要設定特定的Serializer(RedisTemplate預設會使用JdkSerializationRedisSerializer)。

Redis底層中儲存的資料只是位元組,雖然Redis本身支援各種型別(List, Hash等),但在大多數情況下,這些指的是資料的儲存方式,而不是它所代表的內容(內容都是byte),使用者自己來決定資料如何被轉換成String或任何其他物件。使用者自定義型別和原始資料型別之間的互相轉換通過RedisSerializer介面(org.springframework.data.redis.serializer)來處理,顧名思義,它負責處理序列化/反序列化過程。包中多個實現可以開箱即用,比如:StringRedisSerializer和JdkSerializationRedisSerialize、用來處理JSON格式的資料的Jackson2JsonRedisSerializer或GenericJackson2JsonRedisSerializer。

上述RedisTemplate ValueSerializer序列化使用GenericJackson2JsonRedisSerializer而不是Jackson2JsonRedisSerializer,因為Jackson2JsonRedisSerializer需要為每一個需要序列化進Redis的類指定一個Jackson2JsonRedisSerializer,因為其建構函式中需要指定一個型別來做反序列化:

redis.setValueSerializer(new Jackson2JsonRedisSerializer<Product>(Product.class));

如果應用中有大量物件需要快取,這顯然是不合適的。而GenericJackson2JsonRedisSerializer直接把型別資訊序列化到了JSON格式中,讓一個例項可以操作多個物件的反序列化。

3.3 定製RedisCacheManager

有時候Spring Boot自動給我們配置的RedisCacheManager不能滿足我們應用的需求,比如我們要對快取的key進行設定宣告週期,這時候可以通過自定義CacheManager的方式實現。由於Spring Boot2.0之後,RedisCacheManager類發生了很多改變,原來的一堆方法比如setExpires、setUsePrefix、setDefaultExpiration都已經不存在了,通過檢視原始碼及文件,最終找到一種Spring Boot 2.*版本複合需求的RedisCache設定快取宣告週期的方法。其實RedisCacheManager還有很多其他的建構函式,可以按照具體需求選擇。上述配置實現cache名稱為user的快取生命週期為10S,名稱為other的快取生命週期為18000S,除此之外所有cache的宣告週期預設為600S。

4. 快取註解介紹

Spring針對快取提供的註解主要包含@Cacheable、@CachePut、@CacheEvict 三個,具體含義如下: 4.1 示例說明

@CachePut(value = “user”, key = “#user.id”,condition = “#user.username.length() < 10”) 只快取使用者名稱長度少於10的資料

@Cacheable(value = “user”, key = “#id”,condition = “#id < 10″) 只快取ID小於10的資料

@Cacheable(value=”user”,key=”#user.username.concat(##user.password)”) 快取後的redis key為 user::username::password(其中username和password為EL表示式)

@CacheEvict(value=”user”,allEntries=true,beforeInvocation=true) 加上beforeInvocation=true後,不管內部是否報錯,快取都將被清除

5. 快取使用

我在repository查詢方法上新增快取,實際開發中也可以把快取放在service層。repository層快取使用如下:

@Cacheable(value = "user", key = "#id")
@Override
public User getUserById(Long id) {
    log.info("資料庫取資料");
    return userMapper.selectByPrimaryKey(id);
}

@Cacheable註解表示當第一次呼叫getUserById方法時,會進行資料庫查詢,並將查詢到的結果User快取到Redis,redis的key為user::id,@Cacheable註解中,key屬性支援EL表示式,#id表示具體的引數id,比如方法請求id為6,則最終Redis快取的key為user::6

@CacheEvict(value = "user", key = "#id")
@Override
public int delUserById(Long id) {
    UserExample example = new UserExample();
    example.createCriteria().andIdEqualTo(id);
    return userMapper.deleteByExample(example);
    /*等價於
    return userMapper.deleteByPrimaryKey(id);
    */
}

@CachePut(value = "user", key = "#user.id")
@Override
public User updateUser(User user) {
    userMapper.updateByPrimaryKey(user);
    return userMapper.selectByPrimaryKey(user.getId());
}

6. 開啟快取功能

@EnableCaching註解,開啟快取功能,如下:

@SpringBootApplication
@EnableCaching
@Import(value = {DataSourceConfig.class, RedisCacheConfig.class})
public class SpringBootRedisCacheApplicationContext {
    public static void main(String[] args) {
        SpringApplication.run(SpringBootRedisCacheApplicationContext.class, args);
    }
}

篇幅原因,controller層、service層的程式碼這裡就不展示了,有興趣的同學可以到文章底部連結看一下示例程式碼。

7. 測試

之前在RedisCacheConfig中,我將名稱為user的快取宣告週期設定為10S,是為了測試宣告週期設定是否生效,根據測試結果顯示,宣告週期是生效的。正式進行測試之前,注意將user快取宣告週期設定為一個較大值。

7.1 @Cacheable

7.2 @CachePut

7.3 @CacheEvict

預設情況下,RedisCache 不會快取任何null values,因為Redis會丟棄沒有value的keys。使用CacheEvict清楚快取後,會使用org.springframework.cache.support.NullValue作為佔位符儲存。