优雅的缓存解决方案--设置过期时间

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 工具类来实现缓存。
Git源码,欢迎clone和fork

参考资料

发布了42 篇原创文章 · 获赞 48 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/chachapaofan/article/details/88930778