前言
Redis 作為當前最流行的 NoSQL 之一,想必很多人都用過。
Redis 有五種常見的資料型別:string、list、hash、set、zset。講真,我以前只用過 Redis 的 string 型別。
由於業務需求,用到了 Redis 的集合 set。這不,一上來就踩到坑了。
前幾天有個需求提測,測試小哥提了個 bug,並給了我一個日誌截圖:
問題排查從堆疊資訊定位到了專案的程式碼,大致如下:
public class CityService private void setStatus(CityRequest request) { // 根據城市碼查詢城市資訊 Set<String> cityList = cityService.findByCityCode(request.getCityCode()); if (CollectionUtils.isEmpty(cityList)) { return; } // 遍歷,做一些操作(報錯就在這這一行) for (String city : cityList) { // ... } } // 一些無關的程式碼...}
報錯的程式碼就在 for 迴圈那一行。
這一行看起來似乎沒什麼錯誤,跟 HashSet 和 String 轉換有什麼關係呢?往前翻一翻 cityList 是怎麼來的。
cityList 會根據城市碼查詢城市資訊,這個方法有如下三步:
從本地快取查詢,若存在則直接返回;否則進行第二步。從 Redis 查詢,若存在,存入本地快取並返回;否則進行第三步。從 MySQL 查詢,若存在,存入本地快取和 Redis(set 型別)並返回;若不存在返回空。聯絡報錯資訊,再看這幾步的程式碼,1、3 可能性較小;第二步因為之前沒有直接用過 set 這種資料結構,嫌疑較大。
於是想先透過 Redis 客戶端看下快取資訊。
這一看不當緊,更疑惑了:Redis 的 key/value 前面有類似\xAC\xED\x00\x05t\x00\x1B 的字串(可能略有不同),而且還有亂碼。如圖:
亂碼問題處理網上查了一番,原來是 spring-data-redis 的 RedisTemplate 序列化的問題。
RedisTemplate 的預設配置如下:
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; }}
RedisTemplate 在操作 Redis 時預設使用 JdkSerializationRedisSerializer 來進行序列化的。
對於這個問題,修改下配置就可以了,示例程式碼如下:
@Configuration@AutoConfigureAfter(RedisAutoConfiguration.class)public class RedisConfig { @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) { RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(redisConnectionFactory); // 使用 Jackson2JsonRedisSerialize 替換預設序列化 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); // 設定 key/value 的序列化規則 redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setValueSerializer(jackson2JsonRedisSerializer); redisTemplate.setHashKeySerializer(new StringRedisSerializer()); redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer); redisTemplate.afterPropertiesSet(); return redisTemplate; }}
這個配置改過之後,亂碼的情況就沒了。
型別轉換問題繼續跟進前面的型別轉換問題。
透過客戶端檢視 Redis 的值,如下:
這是什麼鬼?明顯不對勁兒啊!
我們想儲存的是 set 型別,正常應該是三條資料,這裡怎麼只有一條?
想了想應該是向 Redis 儲存值的時候有什麼問題,於是翻到程式碼看了看怎麼存的:
public class CityService { public Set<String> findCityByCode(String cityCode) { // ... // 查詢MySQL List<CityDO> cityDoList = cityRepository.findByCityCode(cityCode); // 封裝資料 Set<String> cityList = new HashSet<>(); cityDoList.forEach(record -> { String city = String.format("%s-%s", record.getType(), record.getCity()); cityList.add(city); }); // 【問題出在這裡】 redisService.add2Set(cacheKey, cityList); return cityList; }}
RedisService#add2Set 方法:
public class RedisService { // ... public <T> void add2Set(String key, T... values) { redisTemplate.opsForSet().add(key, values); }}
乍一看好像沒什麼問題。
但是再一看,RedisService#add2Set 方法中,values 是可變長度型別的引數,如果把整個 cityList(java.util.Set 型別)作為一個引數傳給可變長度型別的引數會怎麼樣呢?
PS: 可變長度型別引數是 Java 中的一種語法糖,其實它本質上是一個數組。
打個斷點看下:
可以看到這裡的 Set 型別,也就是傳入的 cityList 被當成了陣列中的一個元素,怪不得會報錯。
那這種情況該怎麼處理呢?
其實也很簡單,把 cityList 轉成陣列就可以了:
public class CityService { public Set<String> findCityByCode(String cityCode) { // ... // 【問題出在這裡】轉成陣列,即 toArray 方法 redisService.add2Set(cacheKey, cityList.toArray()); return cityList; }}
這樣入參就按照想要的方式來了:
再觀察 Redis 的快取值,可以看到也是想要的結果:
到這裡,問題算是搞定了。
結語本文主要覆盤了 Redis 使用過程中遇到的兩個問題:
Redis key/value 亂碼問題。原因是 RedisTemplate 的序列化問題,注意配置。HashSet 和 String 型別轉換問題。主要是在操作 Redis 的 set 時(其他型別亦然),注意 API 的引數細節,不能想當然。漫漫踩坑路,且踩且珍惜。大家一起踩。
作者|WriteOnRead|掘金