SpringBoot 整合 redis 踩坑日記

SpringBoot 中除了了對(duì)常用的關(guān)系型數(shù)據(jù)庫(kù)提供了優(yōu)秀的自動(dòng)化測(cè)試以外,對(duì)于很多 NoSQL 數(shù)據(jù)庫(kù)一樣提供了自動(dòng)化配置的支持,包括:Redis, MongoDB, Elasticsearch, Solr 和 Cassandra。

01 整合redis

Redis是一個(gè)速度非常快的非關(guān)系型數(shù)據(jù)庫(kù)(non-relational database),它可以存儲(chǔ)鍵(key)與5種不同類(lèi)型的值(value)之間的映射(mapping),可以將存儲(chǔ)在內(nèi)存的鍵值對(duì)數(shù)據(jù)持久化到硬盤(pán)??梢允褂脧?fù)制特性來(lái)擴(kuò)展讀性能,還可以使用客戶(hù)端分片來(lái)擴(kuò)展寫(xiě)性能。

redis官網(wǎng)

redis中文社區(qū)


1.1 引入依賴(lài)

Spring Boot 提供了對(duì) Redis 集成的組件包:spring-boot-starter-data-redis,spring-boot-starter-data-redis 依賴(lài)于spring-data-redis 和 lettuce 。

org.springframework.boot

spring-boot-starter-data-redis

1.2 參數(shù)配置

在 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

1.3 測(cè)試訪(fǎng)問(wèn)

通過(guò)編寫(xiě)測(cè)試用例,舉例說(shuō)明如何訪(fǎng)問(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 類(lèi)型,如果有用過(guò) spring-data-redis 的開(kāi)發(fā)者一定熟悉RedisTemplate<K,V>接口,StringRedisTemplate 就相當(dāng)于RedisTemplate<String, String>的實(shí)現(xiàn)。

除了 String 類(lèi)型,實(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 踩的坑做下記錄。

02 踩坑記錄

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;

? ? }

}

2.2 踩坑2:redis 獲取緩存異常

報(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 類(lèi)中黙認(rèn)的 ObjectMapper,看源代碼可以知道,Jackson2JsonRedisSerializer 中的 ObjectMapper 是直接使用new ObjectMapper() 創(chuàng)建的,這樣 ObjectMapper 會(huì)將 redis 中的字符串反序列化為 java.util.LinkedHashMap類(lèi)型,導(dǎo)致后續(xù) Spring 對(duì)其進(jìn)行轉(zhuǎn)換成報(bào)錯(cuò)。其實(shí)我們只要它返回 Object 類(lèi)型就可以了。

使用以下方法,構(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;

? ? }

2.3 踩坑3:類(lèi)轉(zhuǎn)移路徑

異常打印:

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 緩存中獲取用戶(hù)信息UserInfoExt,用戶(hù)信息是在用戶(hù)登錄的時(shí)候存入到 redis 緩存中的。根據(jù)獲取到的用戶(hù)信息來(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 中記錄類(lèi)的 @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),如果用戶(hù)一直處在登錄狀態(tài),是以 com.pa.market.common.util.UserInfoExt 這個(gè)路徑進(jìn)行的序列化操作。但是移動(dòng)了 UserInfoExt 的類(lèi)路徑后,包全名變了。所以會(huì)拋出 no such class found 的異常。這樣在判斷用戶(hù)是否存在的地方就拋出了異常,故而所有的請(qǐng)求都失敗了,已經(jīng)登錄的用戶(hù)沒(méi)法進(jìn)行任何操作。

ok 把踩的坑都記錄下來(lái),終于呼出了最后一口氣,以后遇到這種坑都能從容的避開(kāi)了,但是 redis 中的坑還有很多,可能以后還是會(huì)輕輕松松的跳進(jìn)去

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容