本文首发于微信公众号【WriteOnRead】,欢送关注。
前言
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 的参数细节,不能想当然。
漫漫踩坑路,且踩且珍惜。大家一起踩。