问题背景
- RedisTemplate默认序列化器造成乱码:
使用redisTemplate连接redis数据库,在保存中文时,发现redisTemplate默认使用的是jdkXXX序列化器,它存进去的key和value有乱码,也就是有\xa\xc…之类的前缀,虽然使用redisTemplate读、写redis时不会有问题,但如果通过命令行直连到redis库,就会发现数据带了一些乱码。点击这篇文章可以查看如何通过修改序列化器,解决了新数据的编码问题。 - 旧的乱码数据又不能舍弃:
需要把旧的乱码数据,转换成新序列化器的不乱码的数据。
解决方案1:刷库(适用于数据较少)
废话不多说,直接上代码:
1.在RedisTemplate的配置文件中。设置两个redisTemplate实例,一个是旧的编码方式,另一个是新的编码方式。
通过@Bean(name = “xxx”)来区分是哪种编码方式
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisTemplateConfig {
@Bean(name = "defaultRedisTemplate")
public RedisTemplate<String,Object> defaultRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate redisTemplate = new RedisTemplate();
redisTemplate.setConnectionFactory(redisConnectionFactory);
return redisTemplate;
}
// 设置redisTemplate实例的序列化方式,不然在用命令行查看redis时,会在前缀出现乱码
@Bean(name = "encodedRedisTemplate")
public RedisTemplate<String,Object> encodedRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate redisTemplate = new RedisTemplate();
redisTemplate.setConnectionFactory(redisConnectionFactory);
RedisSerializer stringSerializer = new StringRedisSerializer();
redisTemplate.setDefaultSerializer(stringSerializer);
return redisTemplate;
}
}
- 用旧编码取出redis库中的所有key,然后用新编码吧数据保存回去。备注:我用的log来打印日志,没用log的可以用system.out.println(xxx)代替
@Autowired
@Qualifier("defaultRedisTemplate")
RedisTemplate defaultRedisTemplate;
@Autowired
@Qualifier("encodedRedisTemplate")
RedisTemplate encodedRedisTemplate;
// 使用SCAN迭代地获取redis中的所有的key
public List<String> getRedisKeyList() {
List<String> nameList = new ArrayList<>();
RedisConnection redisConnection = null;
try {
redisConnection = encodedRedisTemplate.getConnectionFactory().getConnection();
// 不要使用keys("*"),因为它会造成死锁,导致线上服务暂时不可用。
// 应该用scan,比如每次遍历50条,这样会对线上服务影响小
ScanOptions options = ScanOptions.scanOptions().match("*").count(50).build();
Cursor<byte[]> c = redisConnection.scan(options);
while (c.hasNext()) { // 不断SCAN,直到获取到redis的所有key
byte[] bytes = null;
String name = "";
try {
bytes = c.next();
// 因为scan获得的key包括2种:一种是无乱码的新key,另一种是有乱码的老key,老key要进行反序列化,不然得到的是乱码
name = (String) defaultRedisTemplate.getDefaultSerializer().deserialize(bytes);
log.info("得到的老key = {}", name);
nameList.add(name);
} catch (SerializationException serializationException) {
nameList.add(new String(bytes));
log.info("有一个无法反序列化的key = {},暂时跳过", new String(bytes));
}
}
} finally {
redisConnection.close(); //Ensure closing this connection.
}
// 打印输出所有的key:
StringBuilder builder = new StringBuilder();
for(String name : nameList) {
// 因为有回车、换行符,不去掉的话,有回车换行符的元素会把之前的元素覆盖掉
builder.append(name.replaceAll("[\\n\\r]", "") + ",");
}
log.info("redis的所有key:");
log.info(builder.toString());
return nameList;
}
解决方案2:不刷库,读旧编码key时,复制一份保存到新编码key:
代码思路:
- 优先读新:查询redis库时,优先以新编码方式查询,新编码查询不到时,才查旧编码
- 读旧时写新:新编码的key没命中,读旧编码key的时候,会往新编码的key里保存一份:
- 双写:保存数据时,双写到新编码、旧编码的key里,这样能保证旧编码的key里的数据一定是最新的,能防止万一写到新key时出了问题导致数据丢失。
- 做好测试:要针对以上3种情况,设计好测试case,毕竟数据库里的数据还是蛮重要的。
- 等系统稳定运行个一年半年后,就只有新编码key来读、写。旧编码的key就舍弃掉(说明这些key已经一年半年没用到了,仅仅适用于丢失一小部分数据不会有影响的情况)
上代码:
1)和上面一样,设置2种编码方式的RedisTemplate实例:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisTemplateConfig {
@Bean(name = "defaultRedisTemplate")
public RedisTemplate<String,Object> defaultRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate redisTemplate = new RedisTemplate();
redisTemplate.setConnectionFactory(redisConnectionFactory);
return redisTemplate;
}
// 设置redisTemplate实例的序列化方式,不然在用命令行查看redis时,会在前缀出现乱码
@Bean(name = "encodedRedisTemplate")
public RedisTemplate<String,Object> encodedRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate redisTemplate = new RedisTemplate();
redisTemplate.setConnectionFactory(redisConnectionFactory);
RedisSerializer stringSerializer = new StringRedisSerializer();
redisTemplate.setDefaultSerializer(stringSerializer);
return redisTemplate;
}
}
2)主逻辑(拿我的获取用户个性配置信息的代码为例子):
// 声明2种编码方式的RedisTemplate
@Autowired
@Qualifier("defaultRedisTemplate")
RedisTemplate defaultRedisTemplate;
@Autowired
@Qualifier("encodedRedisTemplate")
RedisTemplate encodedRedisTemplate;
//
/**
* 设置用户配置信息
* 双写到老的和新编码的key中:我存的是map,朋友们根据自己需要存什么都可以
* @param userName:用户名,是我redis库的key,我以服务名+用户名作为redis库的key
* @param req 配置信息
*/
public void setUserProfile(String userName, SetProfileRequest req) {
Map<String, String> map = new HashMap<String, String>();
map.put(req.getProfileName(), req.getProfileValue());
defaultRedisTemplate.opsForHash().putAll(userName, map);
encodedRedisTemplate.opsForHash().putAll(getEncodedKey(userName), map); // 新的key
log.info("存了一次旧编码key = {}", userName);
log.info("存了一次新编码key = {}", getEncodedKey(userName));
log.info("setUserProfile userName = {}, profile = {}", userName, req);
}
/**
* 获取用户的特定配置信息
* PS:读旧编码的key,用新编码的key保存,然后返回新编码key的查询结果
* @param userName
* @param profileName 配置信息的key,
* @return
*/
@Override
public Map<String, String> getUserProfile(String userName , String profileName) {
Map<String,String> res = new HashMap<>();
res.put("userName", userName);
res.put("profileName", profileName);
/**
* 1. 查新编码的key有没有。有则直接返回结果
* 刷库时,对一些乱码的用户名如:123,opsForHash().get()会抛出异常,所以用try-catch包住
* 有则返回结果
* 没有则查旧的key
*/
try {
if(encodedRedisTemplate.opsForHash().get(userName, profileName) != null) {
res.put(PROFILE_VALUE, String.valueOf(encodedRedisTemplate.opsForHash().get(userName, profileName)));
log.info("新编码的key命中,key= {}", userName);
}
/**
* 2. 查旧的key有没有
* 有则把这个用户的配置信息存一份到新的正确的key里,然后返回查到的结果
* 没有就返回null
*/
else if(defaultRedisTemplate.opsForHash().get(userName, profileName) != null){
encodedRedisTemplate.opsForHash().putAll(userName, defaultRedisTemplate.opsForHash().entries(userName));
res.put(PROFILE_VALUE, String.valueOf(encodedRedisTemplate.opsForHash().get(userName, profileName)));
log.info("新编码的key没命中,旧编码的key命中,旧编码的key= {}。用新编码的key保存了一份,并返回了新编码key的结果", userName);
} else {
res.put(PROFILE_VALUE, null);
}
} catch (Exception e) {
res.put(PROFILE_VALUE, null);
log.error("获取用户 = {} 的特定配置信息 ={} 时出错了:{}", userName, profileName, e.getMessage());
}
log.info("getUserProfile userName = {}, profileName = {}, profileValue= {}", userName, profileName, res.get(PROFILE_VALUE));
return res;
}