SpringBoot 中除了了對(duì)常用的關(guān)系型數(shù)據(jù)庫(kù)提供了優(yōu)秀的自動(dòng)化測(cè)試以外,對(duì)于很多 NoSQL 數(shù)據(jù)庫(kù)一樣提供了自動(dòng)化配置的支持,包括:Redis, MongoDB, Elasticsearch, Solr 和 Cassandra。
Redis是一個(gè)速度非常快的非關(guān)系型數(shù)據(jù)庫(kù)(non-relational database),它可以存儲(chǔ)鍵(key)與5種不同類型的值(value)之間的映射(mapping),可以將存儲(chǔ)在內(nèi)存的鍵值對(duì)數(shù)據(jù)持久化到硬盤。可以使用復(fù)制特性來(lái)擴(kuò)展讀性能,還可以使用客戶端分片來(lái)擴(kuò)展寫(xiě)性能。
redis官網(wǎng)
redis中文社區(qū)
Spring Boot 提供了對(duì) Redis 集成的組件包:spring-boot-starter-data-redis,spring-boot-starter-data-redis 依賴于spring-data-redis 和 lettuce 。
org.springframework.boot
spring-boot-starter-data-redis
在 application.properties 中加入Redis服務(wù)端的相關(guān)配置 :
#redis配置
#Redis服務(wù)器地址
spring.redis.host=127.0.0.1
#Redis服務(wù)器連接端口
spring.redis.port=6379
#Redis數(shù)據(jù)庫(kù)索引(默認(rèn)為0)
spring.redis.database=0
#連接池最大連接數(shù)(使用負(fù)值表示沒(méi)有限制)
spring.redis.jedis.pool.max-active=50
#連接池最大阻塞等待時(shí)間(使用負(fù)值表示沒(méi)有限制)
spring.redis.jedis.pool.max-wait=3000ms
#連接池中的最大空閑連接
spring.redis.jedis.pool.max-idle=20
#連接池中的最小空閑連接
spring.redis.jedis.pool.min-idle=2
#連接超時(shí)時(shí)間(毫秒)
spring.redis.timeout=5000ms
其中 spring.redis.database 的配置通常使用0即可,Redis 在配置的時(shí)候可以設(shè)置數(shù)據(jù)庫(kù)數(shù)量,默認(rèn)為16,可以理解為數(shù)據(jù)庫(kù)的 schema
通過(guò)編寫(xiě)測(cè)試用例,舉例說(shuō)明如何訪問(wèn)Redis。
@RunWith(SpringRunner.class)
@SpringBootTest
publicclassFirstSampleApplicationTests{
@Autowired
? ? StringRedisTemplate stringRedisTemplate;
@Test
publicvoidtest()throwsException{
// 保存字符串
stringRedisTemplate.opsForValue().set("name","chen");
Assert.assertEquals("chen", stringRedisTemplate.opsForValue().get("name"));
? ? }
}
上面的案例通過(guò)自動(dòng)配置的StringRedisTemplate對(duì)象進(jìn)行 redis 的對(duì)寫(xiě)操作,從對(duì)象命名就可注意到支持的是 string 類型,如果有用過(guò) spring-data-redis 的開(kāi)發(fā)者一定熟悉RedisTemplate<K,V>接口,StringRedisTemplate 就相當(dāng)于RedisTemplate<String, String>的實(shí)現(xiàn)。
除了 String 類型,實(shí)戰(zhàn)中經(jīng)常會(huì)在 redis 中儲(chǔ)存對(duì)象,我們就要在儲(chǔ)存對(duì)象時(shí)對(duì)對(duì)象進(jìn)行序列化。下面通過(guò)一個(gè)實(shí)例來(lái)完成對(duì)象的對(duì)寫(xiě)操作。
創(chuàng)建 User 實(shí)體
@Data
publicclassUserimplementsSerializable{
privateString userName;
privateInteger age;
}
配置針對(duì)對(duì)象的RedisTemplate實(shí)例
@Configuration
@EnableCaching
publicclassRedisConfigurationextendsCachingConfigurerSupport{
/**
? ? * 采用RedisCacheManager作為緩存管理器
*@paramconnectionFactory
? ? */
@Bean
publicCacheManagercacheManager(RedisConnectionFactory connectionFactory){
? ? ? ? RedisCacheManager redisCacheManager = RedisCacheManager.create(connectionFactory);
returnredisCacheManager;
? ? }
@Bean
publicRedisTemplateredisTemplate(RedisConnectionFactory factory){
//解決鍵、值序列化問(wèn)題
StringRedisTemplate template =newStringRedisTemplate(factory);
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer =newJackson2JsonRedisSerializer(Object.class);
ObjectMapper om =newObjectMapper();
? ? ? ? om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
? ? ? ? om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
? ? ? ? jackson2JsonRedisSerializer.setObjectMapper(om);
? ? ? ? template.setValueSerializer(jackson2JsonRedisSerializer);
? ? ? ? template.afterPropertiesSet();
returntemplate;
}
完成了配置工作后,編寫(xiě)測(cè)試用例實(shí)驗(yàn)效果
@RunWith(SpringRunner.class)
@SpringBootTest
@Slf4j
publicclassFirstSampleApplicationTests{
@Autowired
? ? RedisTemplate redisTemplate;
@Test
publicvoidtest()throwsException{
//保存對(duì)象
User user =newUser();
user.setUserName("chen");
user.setAge(22);
? ? ? ? redisTemplate.opsForValue().set(user.getUserName(), user);
Home("result:{}",redisTemplate.opsForValue().get("chen"));
? ? }
}
這樣我們就能對(duì)對(duì)象進(jìn)行緩存了。但是在對(duì) redis 更深入的了解中,一不小心就踩進(jìn)坑里去了,下面對(duì) redis 踩的坑做下記錄。
2.1 踩坑1:cacheable注解引發(fā)的亂碼問(wèn)題
@RestController
@RequestMapping("/chen/user")
@Slf4j
publicclassUserController{
@Autowired
? ? IUserService userService;
@GetMapping("/hello")
@Cacheable(value ="redis_key",key ="#name",unless ="#result == null")
publicUser hello(@RequestParam("name")String name){
? ? ? ? User user = new User();
? ? ? ? user.setName(name);
user.setAge(22);
user.setEmail("chen_ti@outlook.com");
returnuser;
? ? }
}
在使用 SpringBoot1.x 的時(shí)候,通過(guò)簡(jiǎn)單的配置 RedisTemplete 就可以了,升級(jí)到 SpringBoot2.0,spring-boot-starter-data-redis 也跟著升起來(lái)了,@Cacheable 就出現(xiàn)了亂碼的情況,可以通過(guò)將上面的配置文件 RedisConfiguration 做如下更改解決 :
@Configuration
@EnableCaching
publicclassRedisConfigurationextendsCachingConfigurerSupport{
@Bean(name="redisTemplate")
publicRedisTemplateredisTemplate(RedisConnectionFactory factory){
RedisTemplate template =newRedisTemplate<>();
RedisSerializer redisSerializer =newStringRedisSerializer();
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer =newJackson2JsonRedisSerializer(Object.class);
ObjectMapper om =newObjectMapper();
? ? ? ? om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
? ? ? ? om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
? ? ? ? jackson2JsonRedisSerializer.setObjectMapper(om);
? ? ? ? template.setConnectionFactory(factory);
//key序列化方式
? ? ? ? template.setKeySerializer(redisSerializer);
//value序列化
? ? ? ? template.setValueSerializer(jackson2JsonRedisSerializer);
//value hashmap序列化
? ? ? ? template.setHashValueSerializer(jackson2JsonRedisSerializer);
returntemplate;
? ? }
@Bean
publicCacheManagercacheManager(RedisConnectionFactory factory){
RedisSerializer redisSerializer =newStringRedisSerializer();
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer =newJackson2JsonRedisSerializer(Object.class);
// 配置序列化
? ? ? ? RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
? ? ? ? RedisCacheConfiguration redisCacheConfiguration = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))? ? ? ? ? ? .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer));
? ? ? ? RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
? ? ? ? ? ? ? ? .cacheDefaults(redisCacheConfiguration)
? ? ? ? ? ? ? ? .build();
returncacheManager;
? ? }
}
報(bào)錯(cuò)信息:
java.lang.ClassCastException:java.util.LinkedHashMapcannotbecasttocom.tuhu.twosample.chen.entity.User
Redis獲取緩存異常:java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to XXX。
出現(xiàn)這種異常,我們需要自定義ObjectMapper,設(shè)置一些參數(shù),而不是直接使用 Jackson2JsonRedisSerializer 類中黙認(rèn)的 ObjectMapper,看源代碼可以知道,Jackson2JsonRedisSerializer 中的 ObjectMapper 是直接使用new ObjectMapper() 創(chuàng)建的,這樣 ObjectMapper 會(huì)將 redis 中的字符串反序列化為 java.util.LinkedHashMap類型,導(dǎo)致后續(xù) Spring 對(duì)其進(jìn)行轉(zhuǎn)換成報(bào)錯(cuò)。其實(shí)我們只要它返回 Object 類型就可以了。
使用以下方法,構(gòu)建一個(gè)Jackson2JsonRedisSerializer對(duì)象,將其注入 RedisCacheManager 即可。
/**
? ? * 通過(guò)自定義配置構(gòu)建Redis的Json序列化器
*@returnJackson2JsonRedisSerializer對(duì)象
? ? */
privateJackson2JsonRedisSerializer jackson2JsonRedisSerializer(){
? ? ? ? Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer =
newJackson2JsonRedisSerializer<>(Object.class);
ObjectMapper objectMapper =newObjectMapper();
? ? ? ? objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.configure(MapperFeature.USE_ANNOTATIONS,false);
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES,false);
objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS,false);
// 此項(xiàng)必須配置,否則會(huì)報(bào)java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to XXX
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
? ? ? ? jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
returnjackson2JsonRedisSerializer;
? ? }
異常打印:
19:32:47INFO? - Started Applicationin10.932seconds (JVM runningfor12.296)
19:32:50INFO? - get data from redis, key =10d044f9-0e94-420b-9631-b83f5ca2ed30
19:32:50WARN? - /market/renewal/homePage/index
org.springframework.data.redis.serializer.SerializationException: CouldnotreadJSON: Couldnotresolvetypeid'com.pa.market.common.util.UserInfoExt'into a subtypeof[simpletype,classjava.lang.Object]: no suchclassfound
at [Source: [B@641a684c; line:1, column:11]; nested exceptioniscom.fasterxml.jackson.databind.exc.InvalidTypeIdException: Couldnotresolvetypeid'com.pa.market.common.util.UserInfoExt'into a subtypeof[simpletype,classjava.lang.Object]: no suchclassfound at [Source: [B@641a684c; line:1, column:11]
項(xiàng)目中使用了攔截器,對(duì)每個(gè) http 請(qǐng)求進(jìn)行攔截。通過(guò)前端傳遞過(guò)來(lái)的 token,去 redis 緩存中獲取用戶信息UserInfoExt,用戶信息是在用戶登錄的時(shí)候存入到 redis 緩存中的。根據(jù)獲取到的用戶信息來(lái)判斷是否存是登錄狀態(tài)。 所以除白名單外的 url,其他請(qǐng)求都需要進(jìn)行這個(gè)操作。通過(guò)日志打印,很明顯出現(xiàn)在 UserInfoExt 對(duì)象存儲(chǔ)到 redis 中序列化和反序列化的操作步驟。
解決辦法:
@Bean
publicRedisTemplateredisTemplate(){
RedisTemplate redisTemplate =newRedisTemplate();
? ? redisTemplate.setConnectionFactory(jedisConnectionFactory());
redisTemplate.setKeySerializer(newStringRedisSerializer());
redisTemplate.setValueSerializer(newGenericJackson2JsonRedisSerializer());
returnredisTemplate;
}
查看 redis 的 Bean 定義發(fā)現(xiàn),對(duì) key 的序列化使用的是 StringRedisSerializer 系列化,value 值的序列化是GenericJackson2JsonRedisSerializer的序列化方法。
其中GenericJackson2JsonRedisSerializer序列化方法會(huì)在 redis 中記錄類的 @class 信息,如下所示:
{
"@class":"com.pa.market.common.util.UserInfoExt",
"url":"百度一下,你就知道",
"name":"baidu"
}
"@class": "com.pa.market.common.util.UserInfoExt",每個(gè)對(duì)象都會(huì)有這個(gè) id 存在(可以通過(guò)源碼看出為嘛有這個(gè) @class),如果用戶一直處在登錄狀態(tài),是以 com.pa.market.common.util.UserInfoExt 這個(gè)路徑進(jìn)行的序列化操作。但是移動(dòng)了 UserInfoExt 的類路徑后,包全名變了。所以會(huì)拋出 no such class found 的異常。這樣在判斷用戶是否存在的地方就拋出了異常,故而所有的請(qǐng)求都失敗了,已經(jīng)登錄的用戶沒(méi)法進(jìn)行任何操作。
ok 把踩的坑都記錄下來(lái),終于呼出了最后一口氣,以后遇到這種坑都能從容的避開(kāi)了,但是 redis 中的坑還有很多,可能以后還是會(huì)輕輕松松的跳進(jìn)去