问题背景

  1. RedisTemplate默认序列化器造成乱码
    使用redisTemplate连接redis数据库,在保存中文时,发现redisTemplate默认使用的是jdkXXX序列化器,它存进去的key和value有乱码,也就是有\xa\xc…之类的前缀,虽然使用redisTemplate读、写redis时不会有问题,但如果通过命令行直连到redis库,就会发现数据带了一些乱码。点击这篇文章可以查看如何通过修改序列化器,解决了新数据的编码问题。
  2. 旧的乱码数据又不能舍弃
    需要把旧的乱码数据,转换成新序列化器的不乱码的数据。

解决方案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;
    }
}
  1. 用旧编码取出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:

代码思路:

  1. 优先读新:查询redis库时,优先以新编码方式查询,新编码查询不到时,才查旧编码
  2. 读旧时写新:新编码的key没命中,读旧编码key的时候,会往新编码的key里保存一份:
  3. 双写:保存数据时,双写到新编码、旧编码的key里,这样能保证旧编码的key里的数据一定是最新的,能防止万一写到新key时出了问题导致数据丢失。
  4. 做好测试:要针对以上3种情况,设计好测试case,毕竟数据库里的数据还是蛮重要的。
  5. 等系统稳定运行个一年半年后,就只有新编码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;
  }