专栏首页BAT的乌托邦【小家Spring】Spring Boot中使用RedisTemplate优雅的操作Redis,并且解决RedisTemplate泛型注入失败的问题

【小家Spring】Spring Boot中使用RedisTemplate优雅的操作Redis,并且解决RedisTemplate泛型注入失败的问题

前言

Spring为了简化我们对持久化层的操作,针对各种持久化方案提供了统一的Template进行操作。比如我们的熟悉的JdbcTemplate就是让我们方便的操作的关系型数据库的。

它有个Spring-Data的子项目,提供了各种SQL、NOSql的便捷操作。比如Redis、MongoDB、ES等等。然后本文主要针对于在SpringBoot中使用RedisTemplate来优雅的操作Redis数据库

虽然我们已经有了强大的缓存注解如:@Cacheable、@CachePut、@CacheEvict等。但是面对稍微复杂点的场景,一个注解是搞不定的

推荐阅读:【小家Spring】细说Spring IOC容器的自动装配(@Autowired),以及Spring4.0新特性之【泛型依赖注入】的源码级解析 先理解泛型依赖注入的原理,再去读下文注入失败问题,很多问题都会迎刃而解~

在Spring中缓存主要有一个缓存接口(Cache)与缓存管理接口(CacheManager)。可以通过扩展这两个接口实现对应的缓存管理。

关于Spring-data-redis

SpringBoot提供了关于redis的启动器:spring-boot-starter-data-redis内部实际依赖于Spring的子项目:Spring-data-redis。因此我们先了解下,这个子项目到底做了些什么呢?

  1. 连接池自动管理,提供了一个高度封装的“RedisTemplate”类

此处指的连接池不一定是JedisPool,因为SpringBoot2.0之后,底层默认不再采用Jedis作为实现了。而是采用效率更高,线程更安全的lettuce客户端

  1. 针对jedis客户端(或者是lettuce)中大量api进行了归类封装,将同一类型操作封装为operation接口

ValueOperations:简单K-V操作 SetOperations:set类型数据操作 ZSetOperations:zset类型数据操作 HashOperations:针对map类型的数据操作 ListOperations:针对list类型的数据操作

  1. 提供了对key的“bound”(绑定)便捷化操作API,可以通过bound封装指定的key,然后进行一系列的操作而无须“显式”的再次指定Key,即BoundKeyOperations:

BoundValueOperations BoundSetOperations BoundListOperations BoundSetOperations BoundHashOperations (其实底层就是个装饰器,对上面进行了装饰而已。。。但使用起来有时候还是方便了很多的)

BoundSetOperations<String, Long> boundSetOperations = redisTemplate.boundSetOps("aaa");
//然后其余的操作方式,和SetOperations一样
//所以如果你的key是不变的,使用它更方便且效率更高。但如果你的key是变的(比如有用户id参数),那使用它就不是很方便了

特别指出的是:虽然BoundSetOperations只是包了一层,但Spring对隐藏实现确实做得非常不错。BoundSetOperations接口的默认实现类DefaultBoundSetOperations不是public的,只有Default的,所以外界根本不能访问,完全隐藏了实现。这一点在我们自己设计框架的时候,还是值得学习的

  1. 将事务操作封装,由容器控制。
  2. 针对数据的“序列化/反序列化”,提供了多种可选择策略(RedisSerializer)(RedisTemplate的序列化方式其实有很多坑,下面博文已详细介绍)

JdkSerializationRedisSerializer:POJO对象的存取场景,使用JDK本身序列化机制(所以必须实现序列化接口),将pojo类通过ObjectInputStream/ObjectOutputStream进行序列化操作,最终redis-server中将存储字节序列。是目前最常用的序列化策略。 StringRedisSerializer:Key或者value为字符串的场景,根据指定的charset对数据的字节序列编码成string,是“new String(bytes, charset)”和“string.getBytes(charset)”的直接封装。是最轻量级和高效的策略。 【小家Spring】RedisTemplate的序列化方式大解读,含FastJsonRedisSerializer、Genericjackson2jsonredisserializer序列化的坑 . . .

在SpringBoot中的使用(整合)

如果是在单纯的Spring环境下使用,可能还会有一些配置的活。但在Boot环境下,简直不要太方便:

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

然后加上对应的连接配置:application.properties

# Redis数据库索引(默认为0)
spring.redis.database=0  
# Redis服务器地址
spring.redis.host=127.0.0.1
# Redis服务器连接端口
spring.redis.port=6379  
# Redis服务器连接密码(默认为空)
spring.redis.password=

##############下面参数一般可选,但我建议配置,更加可控###################

# 连接池最大连接数(使用负值表示没有限制)
spring.redis.pool.max-active=8  
# 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.pool.max-wait=-1  
# 连接池中的最大空闲连接
spring.redis.pool.max-idle=8  
# 连接池中的最小空闲连接
spring.redis.pool.min-idle=0  
# 连接超时时间(毫秒)
spring.redis.timeout=0

就这么简单,完美了。看看demo

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    @Test
    public void contextLoads() {
        System.out.println(redisTemplate); //org.springframework.data.redis.core.StringRedisTemplate@4d354a3e
    }

RedisTemplate的注入问题

看到上面的demo,大家是否疑问:为什么我什么都没做。就能够@Autowired注入RedisTemplate呢?

用了SpringBoot,一切都变得简单。看看对应的自动配置类:一目了然

	@Bean
	@ConditionalOnMissingBean(name = "redisTemplate")
	public RedisTemplate<Object, Object> redisTemplate(
			RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
		RedisTemplate<Object, Object> template = new RedisTemplate<>();
		template.setConnectionFactory(redisConnectionFactory);
		return template;
	}

	@Bean
	@ConditionalOnMissingBean
	public StringRedisTemplate stringRedisTemplate(
			RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
		StringRedisTemplate template = new StringRedisTemplate();
		template.setConnectionFactory(redisConnectionFactory);
		return template;
	}

所以很显然,Boot默认为我们自动注入了两个Bean,因此我们也能实现自动注入。

RedisTemplate<String, String>和StringRedisTemplate是否相同呢?

基于这个疑问,做出如下测试:

    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Test
    public void contextLoads() {
        System.out.println(redisTemplate == stringRedisTemplate); //true
    }

答案:实际注入的就是stringRedisTemplate对象。因为Spring的Bean默认都是单例的,所以返回true。源码一探究竟:

public class StringRedisTemplate extends RedisTemplate<String, String> {}

相信大家看到此处,就不用小编做过多的解释了。

RedisTemplate注入泛型为任意类型的实例,怎么办呢?

问题来了,我们从源码可以看出,Boot默认只为我们注入两个Bean,一个StringRedisTempate专门处理最常用的key和value都是String类型的。另外一个是RedisTemplate<Object, Object>,虽然能处理一切类型,但有时候使用起来确实不方便,我们希望在编译期就能知道泛型类型,写出更健壮的代码。

我们试试这么来:洗希望我的value对象是Person类型

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Person implements Serializable {

    private String name;
    private Integer age;
}

代码如下:

    @Autowired
    private RedisTemplate<String, Person> redisTemplate;

    @Test
    public void contextLoads() {
        System.out.println(redisTemplate);
    }

启动发现报错:

org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'com.fsx.run2.Run2ApplicationTests': Unsatisfied dependency expressed through field 'redisTemplate'; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'org.springframework.data.redis.core.RedisTemplate<java.lang.String, com.fsx.run2.bean.Person>' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}
	at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:596) ~[spring-beans-5.1.2.RELEASE.jar:5.1.2.RELEASE]
	at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:90) ~[spring-beans-5.1.2.RELEASE.jar:5.1.2.RELEASE]

报错原因也容易看懂:找不到类型为RedisTemplate<String, Person>的Bean

那么这个问题该如何解决呢?不妨在使用RedisTemplate< K, V>时不指定具体的类型,修改代码如下:注入时不指定K、V的类型

    @Autowired
    private RedisTemplate redisTemplate;

    @Test
    public void contextLoads() {
        System.out.println(redisTemplate); //org.springframework.data.redis.core.RedisTemplate@2c2c3947
    }

重启服务,发现没有报错了,注入成功。(这种解决方案其实也是最常用的解决方案)

RedisTemplate为什么会注入失败呢?

想到RedisTemplate在SpringBoot框架中是自动配置的,容器中默认的就是RedisTemplate的实例。想到这里,就需要翻下官网的文档,看看官网文档有没有什么说明。

我框出来的这句话特别重要,大概中文翻译如下:

如果你自己在配置类里面注入了一个Bean,那么将会替换Boot默认注册的Bean(您注入的bean的名称只有是restTemplate才会替换默认的,否则不替换

请参加@ConditionalOnMissingBean这个注解,足以看见Spring的设计者的设计思想还是非常不错的:对修改关闭,对扩展开放。

我们做如下实验一:(bean的名字不要叫redisTemplate,否则Boot的Bean就不会再注入了) 自己注册一个bean

    @Bean("myRedisTemplate")
    public RedisTemplate<Object, Object> redisTemplate(
            RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }

测试代码如下:

    @Autowired
    private ApplicationContext applicationContext;
    @Autowired
    private RedisTemplate redisTemplate;

    @Test
    public void contextLoads() {
        Object redisTemplate1 = applicationContext.getBean("redisTemplate");
        Object myRedisTemplate2 = applicationContext.getBean("myRedisTemplate");

        System.out.println(redisTemplate1); //RedisTemplate@5ec6a1b6
        System.out.println(myRedisTemplate2); //RedisTemplate@40013051

        //我们发现这样注入进来的是Boot默认注册的那个bean
        System.out.println(this.redisTemplate); //RedisTemplate@5ec6a1b6
    }

惊奇一:Spring容器内出现了多个类型一样的Bean,但Autowaired竟然成功了(这个绝提原因,读者可以去了解一下Spring4开始的泛型依赖注入,Spring在这方面有处理) 惊奇二:Spring自动注入,注入的为Boot给自动注入进去的Bean,而不是我们自己注入的(我感觉这应该和先后顺序有管。并没有强制的识别“系统”,虽然我没看源码,但若有研究过的,可以跟我讨论)

我们改一下,把我们自己注入的bean泛型改一下:

    @Bean("myRedisTemplate")
    public RedisTemplate<String, Person> redisTemplate(
            RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Person> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }

测试代码:

    @Test
    public void contextLoads() {
        Object redisTemplate1 = applicationContext.getBean("redisTemplate");
        Object myRedisTemplate2 = applicationContext.getBean("myRedisTemplate");

        System.out.println(redisTemplate1); //RedisTemplate@2c2c3947
        System.out.println(myRedisTemplate2); //RedisTemplate@4a62062a

        System.out.println(this.redisTemplate); //RedisTemplate@2c2c3947
        System.out.println(this.personRedisTemplate); //edisTemplate@4a62062a
    }

由此我们看出,Spring很好的实现了泛型依赖注入。很强大有木有,太方便了。虽然类型都一样,但是泛型不一样,强大的Spring还是能够很好的区分开。

那么在实际的开发中,如果我们需要操作上面Person类型,怎么做呢?

建议:注入restTemplate的时候不指定类型,而在在获取处理器的时候,手动指定泛型即可

    @Autowired
    private RedisTemplate redisTemplate;

    @Test
    public void contextLoads() {
        ValueOperations<String, Person> valueOperations = redisTemplate.opsForValue();
        valueOperations.set("aaa", new Person("fsx", 24));

        Person p = valueOperations.get("aaa"); //Person(name=fsx, age=24)
        System.out.println(p);
    }

我们发现不需要特意的去指定restTemplate的泛型,而是在获得处理器的时候指定就好了。因为restTemplate默认可以处理一切类型。(Spring内部做了类型强转,所以不会出问题)

当然,你自己注册一个指定类型,也是没毛病的

StringRedisTemplate与RedisTemplate的区别

  1. 两者的关系是StringRedisTemplate继承RedisTemplate。
  2. 两者的数据是不共通的;也就是说StringRedisTemplate只能管理StringRedisTemplate里面的数据,RedisTemplate只能管理RedisTemplate中的数据。
  3. 默认采用的序列化策略有两种,一种是String的序列化策略,一种是JDK的序列化策略。
  4. StringRedisTemplate默认采用的是String的序列化策略,保存的key和value都是采用此策略序列化保存的。RedisTemplate默认采用的是JDK的序列化策略,保存的key和value都是采用此策略序列化保存的。

设置默认的缓存管理器(CacheManager)

缓存管理器,为Spring抽象出来管理缓存的。若我们没有手动注册过CacheManager这个Bean,那么Boot容器会自动给我们注册一个。

@Bean
	public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory,
			ResourceLoader resourceLoader) {}
//注意:它执行的条件都为@ConditionalOnMissingBean(CacheManager.class),都必须为在容器中没有发现Bean,才会自动自动注册哟

然后,当我们一个项目中使用了多种缓存的时候(比如Redis、Ehcache、Caffeine等),并且自己注册了多个CacheManager的时候,并且我们开启了缓存注解@EnableCaching,我们就需要配置默认的缓存管理器了,否则就会启动失败~~ Spring内置了一些常用的缓存管理器的支持:

这个时候如果我们在上面Redis的基础上,再导入EhCache:

        <dependency>
            <groupId>net.sf.ehcache</groupId>
            <artifactId>ehcache</artifactId>
            <version>2.10.5</version>
        </dependency>

小Tips:Spring现在默认支持的ehcache版本为2.x版,3.x版本不支持。若要使用3.x版本进行集成,请参考相关博文。spring-boot-starter-cache此组件能提供支持

这个时候我们配置类如下:

@Configuration
@EnableCaching
public class CacheConfig {

    @Bean
    public EhCacheCacheManager ehCacheManager() {
        EhCacheCacheManager ehCacheCacheManager = new EhCacheCacheManager();
        return ehCacheCacheManager;
    }

    //备注:这是boot2.x的配置。1.x的配置可以直接new即可  会少很多代码
    //RedisCacheManager cacheManager = new RedisCacheManager(redisTemplate);
    @Bean
    public RedisCacheManager redisCacheManager(RedisConnectionFactory factory,
                                          ResourceLoader resourceLoader) {
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();  // 生成一个默认配置,通过config对象即可对缓存进行自定义配置
        config = config.entryTtl(Duration.ofMinutes(1))     // 设置缓存的默认过期时间,也是使用Duration设置
                .disableCachingNullValues();     // 不缓存空值

        // 设置一个初始化的缓存空间set集合
        Set<String> cacheNames = new HashSet<>();
        cacheNames.add("my-redis-cache1");
        cacheNames.add("my-redis-cache2");

        // 对每个缓存空间应用不同的配置
        Map<String, RedisCacheConfiguration> configMap = new HashMap<>();
        configMap.put("my-redis-cache1", config);
        configMap.put("my-redis-cache2", config.entryTtl(Duration.ofSeconds(120)));

        RedisCacheManager cacheManager = RedisCacheManager.builder(factory)     // 使用自定义的缓存配置初始化一个cacheManager
                .initialCacheNames(cacheNames)  // 注意这两句的调用顺序,一定要先调用该方法设置初始化的缓存名,再初始化相关的配置
                .withInitialCacheConfigurations(configMap)
                .build();
        return cacheManager;
    }

}

报错:

java.lang.IllegalStateException: No CacheResolver specified, and no unique bean of type CacheManager found. Mark one as primary or declare a specific CacheManager to use.
	at org.springframework.cache.interceptor.CacheAspectSupport.afterSingletonsInstantiated(CacheAspectSupport.java:223) ~[spring-context-5.1.2.RELEASE.jar:5.1.2.RELEASE]
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:863) ~[spring-beans-5.1.2.RELEASE.jar:5.1.2.RELEASE]

源码看原因,其实很简单。就是要为缓存注解分配一个默认的缓存管理器,如果你给定两个,我肯定就报错了嘛

@Override
	public void afterSingletonsInstantiated() {
		if (getCacheResolver() == null) {
			// Lazily initialize cache resolver via default cache manager...
			Assert.state(this.beanFactory != null, "CacheResolver or BeanFactory must be set on cache aspect");
			try {
				setCacheManager(this.beanFactory.getBean(CacheManager.class));
			}
			catch (NoUniqueBeanDefinitionException ex) {}

解决方案:在你希望的默认缓存管理器上加是上@Primary注解即可(一般都标注在RedisCacheManager上面)

这样,我们就实现了同时使用多个缓存的情况,可以和谐共处了。

设置缓存的过期时间(通过缓存管理器统一设置)

这个需求经常遇到,最灵活的肯定是使用RedisTemplate的expire方法进行设置。而本处再介绍一个全局方法(也适用于缓存注解),来管理一些频繁使用的key的过期时间。

CacheManager功能其实很简单就是管理cache,接口只有两个方法,根据容器名称获取一个Cache。还有就是返回所有的缓存名称。

//根据名称获取一个Cache(在实现类里面是如果有这个Cache就返回,没有就新建一个Cache放到Map容器中)
Cache getCache(String name);
// 返回所有的缓存名称
Collection<String> getCacheNames();

关于自定义拦截,让缓存注解也支持过期时间的书写,可以提供思路:打断点跟踪拦截器:org.springframework.cache.interceptor.CacheInterceptor来分析

为了方便,本文以Boot1.x为例:

        RedisCacheManager cacheManager = new RedisCacheManager(redisTemplate);
        //设置默认的过期时间(不设置缓存不过期) 单位:秒
        cacheManager.setDefaultExpiration(3600L);
			
			//针对具体的key  设置过期时间   所以我们完全可以定制化
			//备注:此过期时间没更新一次,都会跟新成最新的值的过期时间的
			Map<String, Long> expires = new HashMap<String, Long>();
         expires.put("news", 60L);
        
        redisCacheManager.setExpires(expires);

			//是否启用前缀 默认为false
        cacheManager.setUsePrefix(true);

下一篇博文,我会重点分析RedisTemplate的六大序列化方式,以及使用时候我们常见的坑(有的是巨坑) 【小家Spring】RedisTemplate的序列化方式大解读,含FastJsonRedisSerializer、Genericjackson2jsonredisserializer序列化的坑

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 玩转Spring Cache --- @Cacheable/@CachePut/@CacheEvict注解的原理深度剖析和使用【享学Spring】

    上篇文章介绍了@EnableCaching,用它来开启Spring对缓存注解的支持。本篇文章将继续分析Spring Cache,并且讲解的是我们最为关心的:缓存...

    BAT的乌托邦
  • 玩转Spring Cache --- 开启基于注解的缓存功能@EnableCaching原理了解【享学Spring】

    缓存现已成为了项目的标配,更是面必问的知识点。若你说你的项目中还没有使用到缓存,估计你都不太好意思介绍你的项目。

    BAT的乌托邦
  • [享学Netflix] 八、Apache Commons Configuration2.x相较于1.x使用上带来哪些差异?

    Commons Configuration作为一个优秀的配置管理库,凭借着优秀的设计以及提供了热加载等使用功能,被不少其它组件作为基础配置管理组件使用,流行度较...

    BAT的乌托邦
  • ASP.NET Core ResponseCache进行缓存操作

    本章将介绍客户端缓存将介绍浏览器缓存和服务端缓存,使用浏览器缓存将减少对web服务器的请求次数,同时可以提升性能,避免重复的运算浪费。

    HueiFeng
  • 面试常问,缓存三大问题及解决方案!

    随着互联网系统发展的逐步完善,提高系统的qps,目前的绝大部分系统都增加了缓存机制从而避免请求过多的直接与数据库操作从而造成系统瓶颈,极大的提升了用户体验和系统...

    Java技术栈
  • 百度熊掌号: 2018-2020年, 你不容错过的流量红利!

    自百家官方号上线到熊掌号落地以来,百度搜索一直在颠覆原有的搜索生态,不断的推陈出新与改革,净化网络环境,扶植优质原创内容,让搜索回归本质,从而适应快速迭代的移动...

    蝙蝠侠IT
  • 缓存三大问题及解决方案

    随着互联网系统发展的逐步完善,提高系统的qps,目前的绝大部分系统都增加了缓存机制从而避免请求过多的直接与数据库操作从而造成系统瓶颈,极大的提升了用户体验和系统...

    lyb-geek
  • 面试常问,缓存三大问题及解决方案!

    随着互联网系统发展的逐步完善,提高系统的qps,目前的绝大部分系统都增加了缓存机制从而避免请求过多的直接与数据库操作从而造成系统瓶颈,极大的提升了用户体验和系统...

    lyb-geek
  • scp 非22端口传输

    1、将本地/data/web/下的所有文件传输到远程192.168.2.12主机的/home/test目录下,192.168.2.12的SSH端口为62222

    二狗不要跑
  • Maven 私服 Nexus 总是弹出错误框?禁用 Outreach 服务吧

    打开 chrome 检查,分析网络请求,发现路径中包含 outreach

    donghui

扫码关注云+社区

领取腾讯云代金券