RedisTemplate 已经乱码的数据 转换 成不乱码的数据 —— 刷库、不刷库 2种方法

    科技2024-02-20  99

    问题背景

    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; }
    Processed: 0.010, SQL: 8