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種不同類型的值(value)之間的映射(mapping),可以將存儲(chǔ)在內(nèi)存的鍵值對(duì)數(shù)據(jù)持久化到硬盤。可以使用復(fù)制特性來(lái)擴(kuò)展讀性能,還可以使用客戶端分片來(lái)擴(kuò)展寫(xiě)性能。

redis官網(wǎng)

redis中文社區(qū)


1.1 引入依賴

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

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è)試訪問(wèn)

通過(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 踩的坑做下記錄。

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 類中黙認(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;

? ? }

2.3 踩坑3:類轉(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 緩存中獲取用戶信息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)去

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,443評(píng)論 6 532
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,530評(píng)論 3 416
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 176,407評(píng)論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 62,981評(píng)論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,759評(píng)論 6 410
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 55,204評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,263評(píng)論 3 441
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 42,415評(píng)論 0 288
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,955評(píng)論 1 336
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,782評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,983評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評(píng)論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,222評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 34,650評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 35,892評(píng)論 1 286
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,675評(píng)論 3 392
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,967評(píng)論 2 374