Spring Boot 3 配置 Redis 兼容单例和集群

配置项

Spring Boot 3.x 的 redis 配置和 Spring Boot 2.x 是不一样的, 路径多了一个data

spring:
  ...
  data:
    redis:
      host: @redis.host@
      port: @redis.port@
      password: @redis.password@
      database: @redis.database@

兼容单例和集群的配置

开发时一般用一个Redis单例就足够, 测试和生产环境再换成集群, 但是在application.yml中默认的 Redis 单例和集群配置格式是不同的, 如果要用同一套格式兼容两种配置, 需要自定义 RedisConnectionFactory 这个bean的初始化.

@Configuration
public class RedisConfig {

    @Value("${spring.data.redis.host}")
    public String host;
    @Value("${spring.data.redis.port}")
    public int port;
    @Value("${spring.data.redis.password}")
    public String password;
    @Value("${spring.data.redis.database}")
    public int database;

    @Bean
    public RedisTemplate<String, String> redisStringTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, String> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        redisTemplate.setDefaultSerializer(new StringRedisSerializer());
        return redisTemplate;
    }

    @Bean
    public RedisTemplate<String, byte[]> redisBytesTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, byte[]> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);

        RedisSerializer<String> redisKeySerializer = new StringRedisSerializer();
        redisTemplate.setKeySerializer(redisKeySerializer);
        redisTemplate.setHashKeySerializer(redisKeySerializer);
        redisTemplate.setValueSerializer(RedisSerializer.byteArray());
        redisTemplate.setHashValueSerializer(RedisSerializer.byteArray());

        return redisTemplate;
    }

    @Bean
    public RedisConnectionFactory lettuceConnectionFactory() {
        if (host.contains(",")) {
            RedisClusterConfiguration config = new RedisClusterConfiguration(Arrays.asList(host.split(",")));
            config.setMaxRedirects(3);
            if (password != null && !password.isEmpty()) {
                config.setPassword(RedisPassword.of(password));
            }
            LettuceConnectionFactory factory = new LettuceConnectionFactory(config);
            factory.afterPropertiesSet();
            return factory;

        } else {
            RedisStandaloneConfiguration config = new RedisStandaloneConfiguration();
            config.setHostName(host);
            config.setPort(port);
            config.setDatabase(database);
            if (password != null && !password.isEmpty()) {
                config.setPassword(RedisPassword.of(password));
            }
            LettuceConnectionFactory factory = new LettuceConnectionFactory(config);
            factory.afterPropertiesSet();
            return factory;
        }
    }

}

这样, 当配置改为集群时, 只需要修改 spring.data.redis.host 的内容为 1.1.1.1:6379,1.1.1.2:6379,1.1.1.3:6379这样的格式就可以了.

使用 Byte 作为值存储

ByteUtil.java

public class ByteUtil {

    public static byte[] toByte(String str) {
        if (str == null) return null;
        return str.getBytes();
    }

    public static byte[][] toByte(String[] strs) {
        if (strs == null) return null;
        byte[][] arr = new byte[strs.length][];
        for (int i = 0; i < strs.length; i++) {
            arr[i] = strs[i].getBytes();
        }
        return arr;
    }

    public static String toString(byte[] bytes) {
        return bytes == null ? null : new String(bytes);
    }

    public static Set<String> toString(Set<byte[]> byteset) {
        if (byteset == null) return null;
        return byteset.stream()
                .map(String::new)
                .collect(Collectors.toSet());
    }

    public static List<String> toStrings(List<byte[]> byteslist) {
        if (byteslist == null) return null;
        return byteslist.stream()
                .map(String::new)
                .collect(Collectors.toList());
    }

    public static byte[] toByte(int x) {
        ByteBuffer buffer = ByteBuffer.allocate(Integer.BYTES);
        buffer.putInt(x);
        return buffer.array();
    }
    public static int toInteger(byte[] bytes) {
        ByteBuffer buffer = ByteBuffer.allocate(Integer.BYTES);
        buffer.put(bytes);
        buffer.flip();//need flip
        return buffer.getInt();
    }

    public static byte[] toByte(long x) {
        ByteBuffer buffer = ByteBuffer.allocate(Long.BYTES);
        buffer.putLong(x);
        return buffer.array();
    }
    public static long toLong(byte[] bytes) {
        ByteBuffer buffer = ByteBuffer.allocate(Long.BYTES);
        buffer.put(bytes);
        buffer.flip();//need flip
        return buffer.getLong();
    }

    public static byte[] toByte(Object object) {
        if (object == null) return null;
        try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
             ObjectOutputStream oos = new ObjectOutputStream(baos)) {
            oos.writeObject(object);
            return baos.toByteArray();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public static <T> List<T> toObjs(List<byte[]> byteslist) {
        if (byteslist == null) return null;
        List<T> list = new ArrayList<>();
        for (byte[] bytes : byteslist) {
            T t = toObj(bytes);
            list.add(t);
        }
        return list;
    }

    @SuppressWarnings("unchecked")
    public static <T> T toObj(byte[] bytes) {
        if (bytes == null || bytes.length < 8) return null;
        try (ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
             ObjectInputStream ois = new ObjectInputStream(bais)) {
            return (T)ois.readObject();
        } catch (IOException|ClassNotFoundException e) {
            throw new RuntimeException(e);
        }
    }
}

在服务中的调用方式


@Autowired
private RedisTemplate<String, byte[]> redisBytesTemplate;

@Override
public Boolean hasKey(String key) {
    return redisBytesTemplate.hasKey(key);
}

@Override
public Boolean hashHasKey(String key, String field) {
    return redisBytesTemplate.opsForHash().hasKey(key,field);
}

@Override
public Integer hashGetInt(String key, String field) {
    HashOperations<String, String, byte[]> opsForHash = redisBytesTemplate.opsForHash();
    byte[] bytes = opsForHash.get(key, field);
    return bytes == null? null : ByteUtil.toInteger(bytes);
}

@Override
public void hashSetInt(String key, String field, int value) {
    HashOperations<String, String, byte[]> opsForHash = redisBytesTemplate.opsForHash();
    opsForHash.put(key, field, ByteUtil.toByte(value));
}

@Override
public <T> T hashGetObj(String key, String field) {
    HashOperations<String, String, byte[]> opsForHash = redisBytesTemplate.opsForHash();
    return ByteUtil.toObj(opsForHash.get(key, field));
}

@Override
public <T> void hashSetObj(String key, String field, T value) {
    HashOperations<String, String, byte[]> opsForHash = redisBytesTemplate.opsForHash();
    opsForHash.put(key, field, ByteUtil.toByte(value));
}

/**
 * @param timeout seconds to block
 */
@Override
public <T> T bLPopObj(int timeout, String key) {
    ListOperations<String, byte[]> opsForList = redisBytesTemplate.opsForList();
    byte[] bytes = opsForList.leftPop(key, timeout, TimeUnit.SECONDS);
    return ByteUtil.toObj(bytes);
}

@Override
public <T> Long rPush(String key, T value) {
    ListOperations<String, byte[]> opsForList = redisBytesTemplate.opsForList();
    return opsForList.rightPush(key, ByteUtil.toByte(value));
}

参考