Redis遇到的那些坑
前言
Redis 作為當(dāng)前最流行的 NoSQL 之一,想必很多人都用過(guò)。
Redis 有五種常見的數(shù)據(jù)類型:string、list、hash、set、zset。講真,我以前只用過(guò) Redis 的 string 類型。
由于業(yè)務(wù)需求,用到了 Redis 的集合 set。這不,一上來(lái)就踩到坑了。
前幾天有個(gè)需求提測(cè),測(cè)試小哥提了個(gè) bug,并給了我一個(gè)日志截圖:

問(wèn)題排查
從堆棧信息定位到了項(xiàng)目的代碼,大致如下:
public?class?CityService
??private?void?setStatus(CityRequest?request)?{
????//?根據(jù)城市碼查詢城市信息
????Set?cityList?=?cityService.findByCityCode(request.getCityCode());
????if?(CollectionUtils.isEmpty(cityList))?{
??????return;
????}
????//?遍歷,做一些操作(報(bào)錯(cuò)就在這這一行)
????for?(String?city?:?cityList)?{
??????//?...
????}
??}
??//?一些無(wú)關(guān)的代碼...
}
報(bào)錯(cuò)的代碼就在 for 循環(huán)那一行。
這一行看起來(lái)似乎沒(méi)什么錯(cuò)誤,跟 HashSet 和 String 轉(zhuǎn)換有什么關(guān)系呢?往前翻一翻 cityList 是怎么來(lái)的。
cityList 會(huì)根據(jù)城市碼查詢城市信息,這個(gè)方法有如下三步:
從本地緩存查詢,若存在則直接返回;否則進(jìn)行第二步。 從 Redis 查詢,若存在,存入本地緩存并返回;否則進(jìn)行第三步。 從 MySQL 查詢,若存在,存入本地緩存和 Redis(set 類型)并返回;若不存在返回空。
聯(lián)系報(bào)錯(cuò)信息,再看這幾步的代碼,1、3 可能性較小;第二步因?yàn)橹皼](méi)有直接用過(guò) set 這種數(shù)據(jù)結(jié)構(gòu),嫌疑較大。
于是想先通過(guò) Redis 客戶端看下緩存信息。
這一看不當(dāng)緊,更疑惑了:Redis 的 key/value 前面有類似\xAC\xED\x00\x05t\x00\x1B 的字符串(可能略有不同),而且還有亂碼。如圖:

亂碼問(wèn)題處理
網(wǎng)上查了一番,原來(lái)是 spring-data-redis 的 RedisTemplate 序列化的問(wèn)題。
RedisTemplate 的默認(rèn)配置如下:
public?class?RedisAutoConfiguration?{
?@Bean
?@ConditionalOnMissingBean(name?=?"redisTemplate")
?public?RedisTemplate{
??RedisTemplateRedisTemplate 在操作 Redis 時(shí)默認(rèn)使用 JdkSerializationRedisSerializer 來(lái)進(jìn)行序列化的。
對(duì)于這個(gè)問(wèn)題,修改下配置就可以了,示例代碼如下:
@Configuration
@AutoConfigureAfter(RedisAutoConfiguration.class)
public?class?RedisConfig?{
??@Bean
??public?RedisTemplate?redisTemplate(RedisConnectionFactory?redisConnectionFactory)? {
????RedisTemplate?redisTemplate?=?new?RedisTemplate<>();
????redisTemplate.setConnectionFactory(redisConnectionFactory);
????//?使用?Jackson2JsonRedisSerialize?替換默認(rèn)序列化
????Jackson2JsonRedisSerializer?jackson2JsonRedisSerializer?=?new?Jackson2JsonRedisSerializer<>(Object.class);
????ObjectMapper?objectMapper?=?new?ObjectMapper();
????objectMapper.setVisibility(PropertyAccessor.ALL,?JsonAutoDetect.Visibility.ANY);
????objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
????objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES,?false);
????jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
????//?設(shè)置?key/value?的序列化規(guī)則
????redisTemplate.setKeySerializer(new?StringRedisSerializer());
????redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
????redisTemplate.setHashKeySerializer(new?StringRedisSerializer());
????redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
????redisTemplate.afterPropertiesSet();
????return?redisTemplate;
??}
}
這個(gè)配置改過(guò)之后,亂碼的情況就沒(méi)了。
類型轉(zhuǎn)換問(wèn)題
繼續(xù)跟進(jìn)前面的類型轉(zhuǎn)換問(wèn)題。
通過(guò)客戶端查看 Redis 的值,如下:

這是什么鬼?明顯不對(duì)勁兒啊!
我們想存儲(chǔ)的是 set 類型,正常應(yīng)該是三條數(shù)據(jù),這里怎么只有一條?
想了想應(yīng)該是向 Redis 存儲(chǔ)值的時(shí)候有什么問(wèn)題,于是翻到代碼看了看怎么存的:
public?class?CityService?{
??public?Set?findCityByCode(String?cityCode)? {
????//?...
????//?查詢MySQL
????List?cityDoList?=?cityRepository.findByCityCode(cityCode);
????//?封裝數(shù)據(jù)
????Set?cityList?=?new?HashSet<>();
????cityDoList.forEach(record?->?{
??????String?city?=?String.format("%s-%s",?record.getType(),?record.getCity());
??????cityList.add(city);
????});
????//?【問(wèn)題出在這里】
????redisService.add2Set(cacheKey,?cityList);
????return?cityList;
??}
}
RedisService#add2Set 方法:
public?class?RedisService?{
??//?...
??public??void?add2Set(String?key,?T...?values)?{
????redisTemplate.opsForSet().add(key,?values);
??}
}
乍一看好像沒(méi)什么問(wèn)題。
但是再一看,RedisService#add2Set 方法中,values 是可變長(zhǎng)度類型的參數(shù),如果把整個(gè) cityList(java.util.Set 類型)作為一個(gè)參數(shù)傳給可變長(zhǎng)度類型的參數(shù)會(huì)怎么樣呢?
PS: 可變長(zhǎng)度類型參數(shù)是 Java 中的一種語(yǔ)法糖,其實(shí)它本質(zhì)上是一個(gè)數(shù)組。
打個(gè)斷點(diǎn)看下:

可以看到這里的 Set 類型,也就是傳入的 cityList 被當(dāng)成了數(shù)組中的一個(gè)元素,怪不得會(huì)報(bào)錯(cuò)。
那這種情況該怎么處理呢?
其實(shí)也很簡(jiǎn)單,把 cityList 轉(zhuǎn)成數(shù)組就可以了:
public?class?CityService?{
??public?Set?findCityByCode(String?cityCode)? {
????//?...
????//?【問(wèn)題出在這里】轉(zhuǎn)成數(shù)組,即?toArray?方法
????redisService.add2Set(cacheKey,?cityList.toArray());
????return?cityList;
??}
}
這樣入?yún)⒕桶凑障胍姆绞絹?lái)了:

再觀察 Redis 的緩存值,可以看到也是想要的結(jié)果:

到這里,問(wèn)題算是搞定了。
結(jié)語(yǔ)
本文主要復(fù)盤了 Redis 使用過(guò)程中遇到的兩個(gè)問(wèn)題:
Redis key/value 亂碼問(wèn)題。原因是 RedisTemplate 的序列化問(wèn)題,注意配置。 HashSet 和 String 類型轉(zhuǎn)換問(wèn)題。主要是在操作 Redis 的 set 時(shí)(其他類型亦然),注意 API 的參數(shù)細(xì)節(jié),不能想當(dāng)然。
漫漫踩坑路,且踩且珍惜。大家一起踩。
