Redis遇到的那些坑
前言
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
的字符串(可能略有不同),而且还有乱码。如图:
乱码问题处理
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;
}
}
@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;
}
}
类型转换问题
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;
}
}
public class RedisService {
// ...
public <T> void add2Set(String key, T... values) {
redisTemplate.opsForSet().add(key, values);
}
}
PS: 可变长度类型参数是 Java 中的一种语法糖,其实它本质上是一个数组。
public class CityService {
public Set<String> findCityByCode(String cityCode) {
// ...
// 【问题出在这里】转成数组,即 toArray 方法
redisService.add2Set(cacheKey, cityList.toArray());
return cityList;
}
}
Redis key/value 乱码问题。原因是 RedisTemplate 的序列化问题,注意配置。 HashSet 和 String 类型转换问题。主要是在操作 Redis 的 set 时(其他类型亦然),注意 API 的参数细节,不能想当然。
有道无术,术可成;有术无道,止于术
欢迎大家关注Java之道公众号
好文章,我在看❤️