優雅的快取解決方案--設定過期時間

米奇羅發表於2019-03-31

1. 前言

上篇文章介紹了利用 SpringCache 和 Redis 設定快取,但是SpringCache 註解並不支援設定快取時間,確實很令人頭疼。這篇文章將叫你用最簡單的方式解決 SpringCache 和 Redis 設定快取並設定快取時間。 此篇文章基於上篇部落格,有啥不懂的地方請檢視上篇部落格。 上篇文章連結:優雅的快取解決方案--SpringCache和Redis整合(SpringBoot)

2. 配置

@Cacheable註解不支援配置過期時間,所有需要通過配置CacheManneg來配置預設的過期時間和針對每個類或者是方法進行快取失效時間配置。

解決   可以採用如下的配置資訊來解決的設定失效時間問題配置資訊

修改配置類

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import java.io.Serializable;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;

/**
 * @Author: MaoLin
 * @Date: 2019/3/26 17:04
 * @Version 1.0
 */

@Configuration
@EnableCaching
public class RedisConfig implements Serializable {

     /**
     * 申明快取管理器,會建立一個切面(aspect)並觸發Spring快取註解的切點(pointcut)
     * 根據類或者方法所使用的註解以及快取的狀態,這個切面會從快取中獲取資料,將資料新增到快取之中或者從快取中移除某個值
     */

   /* @Bean
    public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
        return RedisCacheManager.create(redisConnectionFactory);
    }

    @Bean
    public RedisTemplate redisTemplate(RedisConnectionFactory factory) {
        // 建立一個模板類
        RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
        // 將剛才的redis連線工廠設定到模板類中
        template.setConnectionFactory(factory);
        // 設定key的序列化器
        template.setKeySerializer(new StringRedisSerializer());
        // 設定value的序列化器
        //使用Jackson 2,將物件序列化為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);

        return template;
    }*/


    /**
     * 最新版,設定redis快取過期時間
     */

    @Bean
    public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
        return new RedisCacheManager(RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory), this.getRedisCacheConfigurationWithTtl( 60), this.getRedisCacheConfigurationMap() // 指定 key 策略
        );
    }

    private Map<String, RedisCacheConfiguration> getRedisCacheConfigurationMap() {
        Map<String, RedisCacheConfiguration> redisCacheConfigurationMap = new HashMap<>();
        //SsoCache和BasicDataCache進行過期時間配置
     redisCacheConfigurationMap.put("messagCache", this.getRedisCacheConfigurationWithTtl(30 * 60));   redisCacheConfigurationMap.put("userCache", this.getRedisCacheConfigurationWithTtl(60));//自定義設定快取時間
    
        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;
    }
}
複製程式碼

測試

  • 設定快取名稱及快取時間(如下為60秒)
redisCacheConfigurationMap.put("userCache",this.getRedisCacheConfigurationWithTtl(60));
複製程式碼
  • 使用 加上註解即可 @Cacheable("userCache") 注:名稱為配置類裡面設定的名稱userCache,可設定多個快取名稱及時間

Controller測試類


import com.ml.demo.dao.UserDao;
import com.ml.demo.entity.User;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.io.Serializable;

/**
 * @Author: MaoLin
 * @Date: 2019/3/26 17:03
 * @Version 1.0
 */


@RestController
public class testController implements Serializable {
    @Resource
    private UserDao userDao;

    /**
     * 查詢出一條資料並且新增到快取
     *
     * @param userId
     * @return
     */
    @RequestMapping("/getUser")
    @Cacheable("userCache")
    public User getUser(@RequestParam(required = true) String userId) {
        System.out.println("如果沒有快取,就會呼叫下面方法,如果有快取,則直接輸出,不會輸出此段話");
        return userDao.getUser(Integer.parseInt(userId));
    }

    /**
     * 刪除一個快取
     *
     * @param userId
     * @return
     */
    @RequestMapping(value = "/deleteUser")
    @CacheEvict("userCache")
    public String deleteUser(@RequestParam(required = true) String userId) {
        return "刪除成功";
    }

    /**
     * 新增一條儲存的資料到快取,快取的key是當前user的id
     *
     * @param user
     * @return
     */
    @RequestMapping("/saveUser")
    @CachePut(value = "userCache", key = "#result.userId +''")
    public User saveUser(User user) {
        return user;
    }


    /**
     * 返回結果userPassword中含有nocache字串就不快取
     *
     * @param userId
     * @return
     */
    @RequestMapping("/getUser2")
    @CachePut(value = "userCache", unless = "#result.userPassword.contains('nocache')")
    public User getUser2(@RequestParam(required = true) String userId) {
        System.out.println("如果走到這裡說明,說明快取沒有生效!");
        User user = new User(Integer.parseInt(userId), "name_nocache" + userId, "nocache");
        return user;
    }


    @RequestMapping("/getUser3")
    @Cacheable(value = "userCache", key = "#root.targetClass.getName() + #root.methodName + #userId")
    public User getUser3(@RequestParam(required = true) String userId) {
        System.out.println("如果第二次沒有走到這裡說明快取被新增了");
        return userDao.getUser(Integer.parseInt(userId));
    }

}

複製程式碼

測試執行及結果

  • 儲存快取

    優雅的快取解決方案--設定過期時間

  • 檢視快取

    優雅的快取解決方案--設定過期時間

  • 檢視redis

    優雅的快取解決方案--設定過期時間

  • 一分鐘後快取過期

    優雅的快取解決方案--設定過期時間

  • 再查詢快取

    優雅的快取解決方案--設定過期時間

  • 控制檯執行結果

    優雅的快取解決方案--設定過期時間

3. 報錯解決

2019-03-31 14:21:05.163 ERROR 17056 --- [nio-8080-exec-4] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.data.redis.serializer.SerializationException: Could not read JSON: Cannot construct instance of `com.ml.demo.entity.User` (no Creators, like default construct, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
 at [Source: (byte[])"["com.ml.demo.entity.User",{"userId":11,"userName":"\"張三\"","userPassword":"123"}]"; line: 1, column: 29]; nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `com.ml.demo.entity.User` (no Creators, like default construct, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
 at [Source: (byte[])"["com.ml.demo.entity.User",{"userId":11,"userName":"\"張三\"","userPassword":"123"}]"; line: 1, column: 29]] with root cause
複製程式碼

這個 bug 調了好久才解決,其實問題很簡單。

原因:

原因是我在該實體類中新增了一個為了方便例項化該類用的建構函式,導致JVM不會新增預設的無參建構函式,而jackson的反序列化需要無參建構函式,因此報錯。

Response實體類同理。

解決:

在實體類中補上一個無參構造器即可。

public User() {}

小結&參考資料

小結

利用 Spring 提供的快取機制(物件)結合Redis 實現快取其實是很好的方法,但是沒有提供設定快取時間,這個就很不人性化了,Redis 的使用其實 Spring 還提供了 RedisTemplate 和 StringRedisTemplate 這兩個類都支援設定快取時間,如果要是覺得 SpringCache 的使用不太方便,可以利用 RedisTemplate 類自定義 Redis 工具類來實現快取。

參考資料

相關文章