最近專案中使用SpringBoot整合Redis,踩到了一個坑:從Redis中獲取資料為null,但實際上Redis中是存在對應的資料的。是什麼原因導致此坑的呢?
本文就帶大家從SpringBoot整合Redis、所踩的坑以及自動配置原始碼分析來學習一下SpringBoot中如何正確的使用Redis。
SpringBoot整合Redis在SpringBoot專案中只需在pom檔案中引入Redis對應的starter,配置Redis連線資訊即可進行使用了。pom依賴引入:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId></dependency>
對應application配置檔案配置:
spring: redis: host: 127.0.0.1 port: 6379 database: 1 password: 123456 timeout: 5000
透過以上兩項配置即完成了Redis的整合,下面便是具體的使用,這裡以單元測試的形式呈現。
@SpringBootTest@RunWith(SpringRunner.class)public class TokenTest { @Autowired private RedisTemplate redisTemplate; @Test public void getValue() { Object value = redisTemplate.opsForValue().get("1"); System.out.println("value:" + value); }}
可以看到直接透過@Autowired注入RedisTemplate之後,即可呼叫RedisTemplate提供的方法操作。RedisTemplate提供了豐富的Redis操作方法,具體使用檢視相應的API即可,這裡不再拓展。
專案中遇到的坑迴歸到最開始的問題:從Redis中獲取資料為null,但實際上Redis中是存在對應的資料的。
其實問題表象很詭異,但問題的原因很簡單,就是Redis中存資料和取資料時採用了不同的RedisTemplate導致的。
在SpringBoot中,針對Redis的自動配置類預設會初始化兩個RedisTemplate,先來看一下RedisAutoConfiguration中原始碼:
@Configuration@ConditionalOnClass({RedisOperations.class})@EnableConfigurationProperties({RedisProperties.class})@Import({LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class})public class RedisAutoConfiguration { @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; }}
可以看到RedisAutoConfiguration中初始化了兩個RedisTemplate的bean。第一個Bean型別為RedisTemplate<Object, Object>,Bean的名稱為redisTemplate,而且是當容器中不存在對應的Bean name時才會進行初始化。第二Bean型別為StringRedisTemplate,Bean的名稱為stringRedisTemplate,該類繼承自RedisTemplate<String, String>。
也就說一個Bean是針對Object物件處理的,一個是針對String物件進行處理的。
導致出現坑的原因便是set時注入的是RedisTemplate<Object, Object>,而獲取時注入的是StringRedisTemplate。這麼明顯的錯誤應該很容易排查的啊?
問題為什麼隱藏的那麼深?如果直接是因為兩處型別不一致導致的,的確很好排查,看一下注入的RedisTemplate即可。
但問題難以排查,還因為另外一個因素:@Resource和@Autowired注入的問題。
預設情況下@Resource採用先根據bean名稱注入,找不到再根據型別注入,而@Autowired預設採用根據型別注入。專案獲取資料時採用了@Resource注入方式,如下:
@Resourceprivate RedisTemplate<String, String> redisTemplate;
而儲存時採用的是@Autowired注入的:
@Autowiredprivate RedisTemplate<String, String> redisTemplate;
上面兩種形式的注入,在只存在單個例項時好像並不是什麼問題,要麼其中一個直接報錯,要麼注入成功。但當像上述場景,出現了兩個RedisTemplate時,問題就變得隱蔽了。
當採用@Autowired時,根據型別注入,直接注入了RedisTemplate<String, String>的bean,因為它們的型別都是String的。
而當使用@Resource注入時,預設採用的是根據名稱匹配,原始碼中可以看到redisTemplate對應的型別為RedisTemplate<Object, Object>。因此,兩處注入了不同的RedisTemplate,於是就導致了獲取時獲取不到值的問題。
解決方案找到問題的根源之後,解決問題便容易多了。
方案一,將@Resource的注入改為@Autowired。
方案二:將@Resource注入的bean名稱由redisTemplate改為stringRedisTemplate。當然根據具體業務場景還有其他解決方案。
小結關於SpringBoot整合Redis其實很簡單,SpringBoot已經幫我們做了大多數的事情,但因為預設初始化了兩個RedisTemplate,再加上@Autowired和@Resource註解的區別就導致了問題的複雜度。因此,在使用的過程中儘量保持各處採用一致的規範,阿里Java開發手冊推薦使用@Resource註解。同時,當然少不了對原始碼、註解等的使用的深入學習和了解。