Spring Boot - 集成 Redis

前言

很久之前,有写过一篇博文介绍下 Redis 相关内容及操作:Redis 学习笔记

本篇博文主要介绍下如何在 Spring Boot 中集成 Redis。

依赖导入

Spring Boot 中集成 Redis,第一步就是导入相关依赖,如下所示:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

在 IDEA 中,点击spring-boot-starter-data-redis可以进入该依赖详情配置,可以看到如下内容:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
        <version>2.5.6</version>
        <scope>compile</scope>
    </dependency>

    <dependency>
        <groupId>org.springframework.data</groupId>
        <artifactId>spring-data-redis</artifactId>
        <version>2.5.6</version>
        <scope>compile</scope>
    </dependency>

    <dependency>
        <groupId>io.lettuce</groupId>
        <artifactId>lettuce-core</artifactId>
        <version>6.1.5.RELEASE</version>
        <scope>compile</scope>
    </dependency>
</dependencies>

所以其实spring-boot-starter-data-redis起步依赖实际上就是导入了三个依赖:spring-boot-starterspring-data-redislettuce-core。这几个依赖主要就是加载了 Redis 以及对 Redis 的自动装配进行了使能,具体分析请参考后文。

自动装配原理

导入spring-boot-starter-data-redis后,其实就可以在 Spring Boot 项目中使用 Redis 了,因为 Spring Boot 默认就对 Redis 进行了自动装配,可以查看自动配置文件spring-boot-autoconfigure.jar/META-INF/spring.factories,其内与 Redis 相关的自动配置类有:

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration,\
org.springframework.boot.autoconfigure.data.redis.RedisReactiveAutoConfiguration,\
org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration,\

我们主要关注的自动配置类为RedisAutoConfiguration,其源码如下所示:

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)
@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
public class RedisAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean(name = "redisTemplate")
    @ConditionalOnSingleCandidate(RedisConnectionFactory.class)
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }

    @Bean
    @ConditionalOnMissingBean
    @ConditionalOnSingleCandidate(RedisConnectionFactory.class)
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
        StringRedisTemplate template = new StringRedisTemplate();
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }
}

RedisAutoConfiguration自动配置类主要做了如下几件事:

  1. @ConditionalOnClass(RedisOperations.class):当项目中存在RedisOperations类时,使能自动配置类RedisAutoConfiguration

    其中,RedisOperations类存在于依赖org.springframework.data:spring-data-redis,也就是上文导入依赖spring-boot-starter-data-redis时,就会自动使能RedisAutoConfiguration自动配置类。

  2. @EnableConfigurationProperties(RedisProperties.class):装载 Redis 配置文件。

    其中,RedisProperties源码如下所示:

    @ConfigurationProperties(prefix = "spring.redis")
    public class RedisProperties {
    
        /**
         * Database index used by the connection factory.
         */
        private int database = 0;
    
        /**
         * Connection URL. Overrides host, port, and password. User is ignored. Example:
         * redis://user:password@example.com:6379
         */
        private String url;
    
        /**
         * Redis server host.
         */
        private String host = "localhost";
    
        /**
         * Login username of the redis server.
         */
        private String username;
    
        /**
         * Login password of the redis server.
         */
        private String password;
    
        /**
         * Redis server port.
         */
        private int port = 6379;
    
        /**
         * Whether to enable SSL support.
         */
        private boolean ssl;
    
        /**
         * Read timeout.
         */
        private Duration timeout;
    
        /**
         * Connection timeout.
         */
        private Duration connectTimeout;
    
        /**
         * Client name to be set on connections with CLIENT SETNAME.
         */
        private String clientName;
    
        /**
         * Type of client to use. By default, auto-detected according to the classpath.
         */
        private ClientType clientType;
    
        private Sentinel sentinel;
    
        private Cluster cluster;
    
        private final Jedis jedis = new Jedis();
    
        private final Lettuce lettuce = new Lettuce();
        ...
        /**
         * Type of Redis client to use.
         */
        public enum ClientType {
    
            /**
             * Use the Lettuce redis client.
             */
            LETTUCE,
    
            /**
             * Use the Jedis redis client.
             */
            JEDIS
    
        }
    
        /**
         * Pool properties.
         */
        public static class Pool {
            ...
        }
    
        /**
         * Cluster properties.
         */
        public static class Cluster {
            ...
        }
    
        /**
         * Redis sentinel properties.
         */
        public static class Sentinel {
            ...
        }
    
        /**
         * Jedis client properties.
         */
        public static class Jedis {
            ...
        }
    
        /**
         * Lettuce client properties.
         */
        public static class Lettuce {
            ...
        }
    }
    

    RedisProperties会自动加载前缀为spring.redis的配置选项,Redis 所有可配置项查看RedisProperties对应属性即可,某些选项未设置则使用缺省值:

    private int database = 0;
    private String host = "localhost";
    private int port = 6379;
    
  3. @Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class }):自动导入两个配置类LettuceConnectionConfigurationJedisConnectionConfiguration。这两个配置类会自动各自加载LettuceJedis相关配置。

    其中,LettuceJedis都是 Redis 的客户端,它们都可以连接到 Redis 服务器,并封装了对 Redis 的相关操作。

    在多线程环境下,使用单一Jedis实例,可能会存在线程安全问题,原因是Jedis#connect()方法中,最终会调用到Connection#connect()方法,而该方法内是通过Socket去连接 Redis Server,源码如下所示:

    public class Connection implements Closeable {
       ...
      // socket 非线程安全
      private Socket socket;
      ...
      public void connect() throws JedisConnectionException {
        ...
        socket = socketFactory.createSocket();
        ...
      }
    }
    

    Jedis源码导入方法请查看:附录 - 导入 Jedis

    socketFactory实际运行时对象为DefaultJedisSocketFactory,因此最终会调用到DefaultJedisSocketFactory#createSocket()

    public class DefaultJedisSocketFactory implements JedisSocketFactory {
        ...
        @Override
        public Socket createSocket() throws JedisConnectionException {
            Socket socket = null;
            socket = new Socket();
            ...
        }
    

    Jedis在执行每个命令之前,都会先进行连接(即调用Jedis#connect()),多线程环境下,此时Socket创建就可能存在并发安全问题。

    :还有其他情况也可能导致Jedis并发安全问题,关于Jedis并发安全更详细分析,可参考文章:jedis和lettuce的对比

    解决Jedis并发安全问题的一个方法就是使用连接池(Jedis Pool),为每条线程都单独创建一个对应的Jedis实例。缺点就是连接数增加,开销变大。

    其实,在 Spring Boot 2.0 之后,默认使用的都是Lettuce,所以可以看到,上文起步依赖spring-boot-starter-data-redis,其内部导入的客户端连接依赖是lettuce-core

    相比较于JedisLettuce底层采用的是 Netty,在多线程环境下,也可以保证只创建一个连接,所有线程共享该Lettuce连接,无须使用连接池,并且该方式具备线程安全。可以说,Lettuce既轻量又安全。

    Lettuce的自动配置类如下所示:

    @Configuration(proxyBeanMethods = false)
    // 存在类 RedisClient 时,自动装配
    @ConditionalOnClass(RedisClient.class)
    @ConditionalOnProperty(name = "spring.redis.client-type", havingValue = "lettuce", matchIfMissing = true)
    class LettuceConnectionConfiguration extends RedisConnectionConfiguration {
    
        @Bean(destroyMethod = "shutdown")
        // 不存在自定义 DefaultClientResources 时,自动装配
        @ConditionalOnMissingBean(ClientResources.class)
        DefaultClientResources lettuceClientResources() {
            return DefaultClientResources.create();
        }
    
        @Bean
        // 不存在自定义 LettuceConnectionFactory 时,自动装配
        @ConditionalOnMissingBean(RedisConnectionFactory.class)
        LettuceConnectionFactory redisConnectionFactory(
                ObjectProvider<LettuceClientConfigurationBuilderCustomizer> builderCustomizers,
                ClientResources clientResources) {
            LettuceClientConfiguration clientConfig = getLettuceClientConfiguration(builderCustomizers, clientResources,
                    getProperties().getLettuce().getPool());
            return createLettuceConnectionFactory(clientConfig);
        }
        ...
    }
    

    主要就是自动装配了两个BeanDefaultClientResourcesLettuceConnectionFactory

  4. RedisAutoConfiguration:主要就是自动装配了两个BeanRedisTemplateStringRedisTemplate

    其中,RedisTemplate的类型是RedisTemplate<Object,Object>,它能操作所有类型数据。

    StringRedisTemplate继承RedisTemplate

    public class StringRedisTemplate extends RedisTemplate<String, String> {...}
    

    StringRedisTemplate就是一个特例,用于操作键为String,值也为String类型的数据,这也是 Redis 操作使用最多的场景。

基本使用

下面介绍下在 Spring Boot 项目中使用 Redis,操作步骤如下:

  1. 首先启动一个 Redis Server,此处采用 Docker 开启一个 Redis Server:

    # 启动 Redis Server
    $ docker run --name redis -d -p 6379:6379 redis
    
    # 进入 Redis 容器
    $ docker exec -it redis bash
    
    # 启动 redis-cli
    $ root@c0f7159a3081:/data# redis-cli
    # ping 一下 Redis Serve
    $ 127.0.0.1:6379> ping
    PONG # 返回响应
    

    上面我们首先启动了一个 Redis 容器,此时 Redis Server 也会自动启动,我们在容器内通过redis-cli可以启动一个 Redis 命令行客户端,并通过命令ping一下 Redis Serve,得到了响应PONG,说明 Redis Server 启动成功。

  2. 配置 Redis 相关信息:

    # application.properties
    # Redis Server 地址
    spring.redis.host=localhost
    # Redis Server 端口
    spring.redis.port=6379
    # Redis 数据库索引
    spring.redis.database=0
    # 链接超时时间 单位 ms(毫秒)
    spring.redis.timeout=1000
    ################ Redis 线程池设置 ##############
    # 连接池最大连接数(使用负值表示没有限制)
    spring.redis.pool.max-active=200  
    # 连接池最大阻塞等待时间(使用负值表示没有限制)
    spring.redis.pool.max-wait=-1  
    # 连接池中的最大空闲连接
    spring.redis.pool.max-idle=10 
    # 连接池中的最小空闲连接
    spring.redis.pool.min-idle=0  
    

    主要配置项是hostport

  3. 注入RedisTemplate,操作 Redis:

    @SpringBootTest
    class RedisDemoApplicationTests {
    
        // 注入 RedisTemplate
        @Autowired
        private RedisTemplate redisTemplate;
    
        @Test
        public void testRedis() {
            redisTemplate.opsForValue().set("username", "Whyn");
            String username = (String) redisTemplate.opsForValue().get("username");
            Assertions.assertEquals("Whyn",username);
        }
    }
    

    运行以上程序,可以观察到测试运行结果正确。

以上,就可以在 Spring Boot 中使用 Redis 了,可以看到,非常方便。

自定义配置

前面章节我们成功往 Redis 设置了username:Whyn的键值数据,但此时我们在终端查看下存储内容:

$ 127.0.0.1:6379> keys *
1) "\xac\xed\x00\x05t\x00\busername"

$ 127.0.0.1:6379> get username
(nil)

此处可以看到,数据确实存入了,但是编码方式似乎不对,实际存储的数据我们无法直接观测到具体内容(但是代码获取是可以获取到实际数据的),原因是数据存入 Redis 时,进行了序列化,且RedisTemplate默认使用的序列化方式为JdkSerializationRedisSerializer,其会将存储数据的keyvalue都序列化为字节数组,因此终端看到的就是数据的字节序列。相关源码如下所示:

public class RedisTemplate<K, V> extends RedisAccessor implements RedisOperations<K, V>, BeanClassLoaderAware {
    ...
    private @Nullable RedisSerializer<?> defaultSerializer;
    ...
    @SuppressWarnings("rawtypes") private @Nullable RedisSerializer keySerializer = null;
    @SuppressWarnings("rawtypes") private @Nullable RedisSerializer valueSerializer = null;
    ...
    // 初始化 Bean(即 RedisTemplate)的时候执行,用于对 RedisTemplate 进行配置
    @Override
    public void afterPropertiesSet() {
        ...
        if (defaultSerializer == null) {
            // 默认序列化工具:JdkSerializationRedisSerializer
            defaultSerializer = new JdkSerializationRedisSerializer(
                    classLoader != null ? classLoader : this.getClass().getClassLoader());
        }

        if (enableDefaultSerializer) {

            if (keySerializer == null) {
                // 默认的 key 序列化
                keySerializer = defaultSerializer;
                defaultUsed = true;
            }
            if (valueSerializer == null) {
                // 默认的 value 序列化
                valueSerializer = defaultSerializer;
                defaultUsed = true;
            }
            if (hashKeySerializer == null) {
                hashKeySerializer = defaultSerializer;
                defaultUsed = true;
            }
            if (hashValueSerializer == null) {
                hashValueSerializer = defaultSerializer;
                defaultUsed = true;
            }
        }
        ...
    }
    ...
}

public class JdkSerializationRedisSerializer implements RedisSerializer<Object> {

    // 序列化:将 Object 序列化为 byte[]
    private final Converter<Object, byte[]> serializer;
    // 反序列化:将 byte[] 反序列化为 Object
    private final Converter<byte[], Object> deserializer;
    ...
    public JdkSerializationRedisSerializer(@Nullable ClassLoader classLoader) {
        // 实际序列化工具为:SerializingConverter
        // 实际反序列化工具为:DeserializingConverter
        this(new SerializingConverter(), new DeserializingConverter(classLoader));
    }

    public JdkSerializationRedisSerializer(Converter<Object, byte[]> serializer, Converter<byte[], Object> deserializer) {
        ...
        this.serializer = serializer;
        this.deserializer = deserializer;
    }

    ...
}

// 序列化
public class SerializingConverter implements Converter<Object, byte[]> {
    private final Serializer<Object> serializer;

    public SerializingConverter() {
        // 实际序列化工具为:DefaultSerializer
        this.serializer = new DefaultSerializer();
    }
    ...
    public byte[] convert(Object source) {
        ...
        return this.serializer.serializeToByteArray(source);
    }
}

public class DefaultSerializer implements Serializer<Object> {
    ...
    public void serialize(Object object, OutputStream outputStream) throws IOException {
        ...
        // 最终通过 ObjectOutputStream 进行序列化
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);
        objectOutputStream.writeObject(object);
        objectOutputStream.flush();
    }
}

@FunctionalInterface
public interface Serializer<T> {
    void serialize(T object, OutputStream outputStream) throws IOException;

    // 最终调用的序列化方法
    default byte[] serializeToByteArray(T object) throws IOException {
        ByteArrayOutputStream out = new ByteArrayOutputStream(1024);
        // 最终回调到 DefaultDeserializer.serialize 
        this.serialize(object, out);
        return out.toByteArray();
    }
}

// 反序列化
public class DeserializingConverter implements Converter<byte[], Object> {
    private final Deserializer<Object> deserializer;

    public DeserializingConverter(ClassLoader classLoader) {
        // 实际反序列化工具为:DefaultDeserializer
        this.deserializer = new DefaultDeserializer(classLoader);
    }

    public Object convert(byte[] source) {
        ByteArrayInputStream byteStream = new ByteArrayInputStream(source);
        ...
        return this.deserializer.deserialize(byteStream);
    }
    ...
}

public class DefaultDeserializer implements Deserializer<Object> {
    ...
    // 最终调用的反序列化方法
    public Object deserialize(InputStream inputStream) throws IOException {
        ConfigurableObjectInputStream objectInputStream = new ConfigurableObjectInputStream(inputStream, this.classLoader);
        ...
        return objectInputStream.readObject();
    }
}

public class ConfigurableObjectInputStream extends ObjectInputStream {...}

主要就是在自动配置了RedisTemplate后,就会回调RedisTemplate#afterPropertiesSet()方法,对RedisTemplate实例进行初始化操作,其内就设置了keyvalue默认采用的序列化工具为JdkSerializationRedisSerializer,其底层最终都是通过ObjectOutputStream/ObjectInputStream进行序列化/反序列化。

StringRedisTemplate采用的序列化工具为StringRedisSerializer,它其实就是直接将keyvalue转换为对应的字节数组。相关源码如下所示:

public class StringRedisTemplate extends RedisTemplate<String, String> {

    public StringRedisTemplate() {
        // key 序列化
        setKeySerializer(RedisSerializer.string());
        // value 序列化
        setValueSerializer(RedisSerializer.string());
        setHashKeySerializer(RedisSerializer.string());
        setHashValueSerializer(RedisSerializer.string());
    }
    ...
}

public interface RedisSerializer<T> {

    @Nullable
    byte[] serialize(@Nullable T t) throws SerializationException;

    @Nullable
    T deserialize(@Nullable byte[] bytes) throws SerializationException;

    static RedisSerializer<String> string() {
        // 实际使用的是 StringRedisSerializer.UTF_8
        return StringRedisSerializer.UTF_8;
    }
    ...
}

public class StringRedisSerializer implements RedisSerializer<String> {

    private final Charset charset;

    // UTF_8 序列化其实就是直接将对应的字符串转换为字节数组
    public static final StringRedisSerializer UTF_8 = new StringRedisSerializer(StandardCharsets.UTF_8);

    // 反序列化
    @Override
    public String deserialize(@Nullable byte[] bytes) {
        return (bytes == null ? null : new String(bytes, charset));
    }

    // 序列化
    @Override
    public byte[] serialize(@Nullable String string) {
        return (string == null ? null : string.getBytes(charset));
    }
}

默认的序列化/反序列化方式可能不是我们所期望的,因此,通常我们都会自己创建一个配置类,注入一个RedisTemplate,并设置自定义配置:

@Configuration
public class RedisConfiguration {

    @Bean("redisTemplate")
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        // 泛型改成 String Object,方便使用
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        // Json序列化配置
        // 使用 json解析对象
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om = new ObjectMapper();
        // 通过 ObjectMapper进行转义
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.activateDefaultTyping(om.getPolymorphicTypeValidator(), ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);

        // String 的序列化
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        // key 采用 String的序列化方式
        redisTemplate.setKeySerializer(stringRedisSerializer);
        // hash 的 key 也采用 String的序列化方式
        redisTemplate.setHashKeySerializer(stringRedisSerializer);
        // value 的序列化方式采用 jackson
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        // hash 的 value 序列化也采用 jackson
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);

        // 初始化 redisTemplate
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }
}

此时我们在运行先前的测试程序,然后在终端查看,如下所示:

$ 127.0.0.1:6379> keys *
1) "username"
$ 127.0.0.1:6379> get username
"\"Whyn\""

可以看到实际字符内容了,证明我们自定义配置的序列化器生效了。

工具封装

RedisTemplate的操作相对繁琐,通常我们都会抽取出一个工具类,封装简化RedisTemplate调用。

RedisTemplate具体操作 API,可参考文章:如何使用RedisTemplate访问Redis数据结构

以下是本人对RedisTemplate常用操作进行的封装,简化调用,使用时注入该服务即可:

@Service
public class RedisService {
    private KeysOps<String, Object> keysOps;
    private StringOps<String, Object> stringOps;
    private HashOps<String, String, Object> hashOps;
    private ListOps<String, Object> listOps;
    private SetOps<String, Object> setOps;
    private ZSetOps<String, Object> zsetOps;

    @Autowired
    public RedisService(RedisTemplate<String, Object> redisTemplate) {
        this.keysOps = new KeysOps<>(redisTemplate);
        this.stringOps = new StringOps<>(redisTemplate);
        this.hashOps = new HashOps<>(redisTemplate);
        this.listOps = new ListOps<>(redisTemplate);
        this.setOps = new SetOps<>(redisTemplate);
        this.zsetOps = new ZSetOps<>(redisTemplate);
    }


    /* ##### Key 操作 ##### */
    public KeysOps<String, Object> keys() {
        return this.keysOps;
    }


    /* ##### String 操作 ##### */
    public StringOps<String, Object> string() {
        return this.stringOps;
    }

    /* ##### List 操作 ##### */
    public ListOps<String, Object> list() {
        return this.listOps;
    }

    /* ##### Hash 操作 ##### */
    public HashOps<String, String, Object> hash() {
        return this.hashOps;
    }

    /* ##### Set 操作 ##### */
    public SetOps<String, Object> set() {
        return this.setOps;
    }

    /* ##### Zset 操作 ##### */
    public ZSetOps<String, Object> zset() {
        return this.zsetOps;
    }

    public static class KeysOps<K, V> {
        private final RedisTemplate<K, V> redis;

        private KeysOps(RedisTemplate<K, V> redis) {
            this.redis = redis;
        }

        /**
         * 删除一个 key
         *
         * @param key
         * @return
         */
        public Boolean delete(K key) {
            return this.redis.delete(key);
        }

        /**
         * 批量删除 key
         *
         * @param keys
         * @return
         */
        public Long delete(K... keys) {
            return this.redis.delete(Arrays.asList(keys));
        }

        /**
         * 批量删除 key
         *
         * @param keys
         * @return
         */
        public Long delete(Collection<K> keys) {
            return this.redis.delete(keys);
        }

        /**
         * 设置 key 过期时间
         *
         * @param key     键值
         * @param timeout 过期时间
         * @return
         */
        public Boolean expire(K key, long timeout, TimeUnit timeunit) {
            return this.redis.expire(key, timeout, timeunit);
        }

        /**
         * 设置 key 过期时间
         *
         * @param key
         * @param date 指定过期时间
         * @return
         */
        public Boolean expireAt(K key, Date date) {
            return this.redis.expireAt(key, date);
        }

        /**
         * 获取 key 过期时间
         *
         * @param key 键值
         * @return key 对应的过期时间(单位:毫秒)
         */
        public Long getExpire(K key, TimeUnit timeunit) {
            return this.redis.getExpire(key, timeunit);
        }

        /**
         * 判断 key 是否存在
         *
         * @param key
         * @return key 存在返回 TRUE
         */
        public Boolean hasKey(K key) {
            return this.redis.hasKey(key);
        }

        /**
         * 模糊匹配 key
         *
         * @param pattern 匹配模式(可使用通配符)
         * @return 返回匹配的所有键值
         */
        public Set<K> keys(K pattern) {
            return this.redis.keys(pattern);
        }

        /**
         * 返回数据库所有键值
         *
         * @return
         */
        public Set<K> keys() {
            return this.keys((K) "*");
        }

        /**
         * 序列化 key
         *
         * @param key
         * @return 返回 key 序列化的字节数组
         */
        public byte[] dump(K key) {
            return this.redis.dump(key);
        }

        /**
         * 移除 key 过期时间,相当于持久化 key
         *
         * @param key
         * @return
         */
        public Boolean persist(K key) {
            return this.redis.persist(key);
        }

        /**
         * 从当前数据库中随机返回一个 key
         *
         * @return
         */
        public K random() {
            return this.redis.randomKey();
        }

        /**
         * 重命名 key
         *
         * @param oldKey
         * @param newKey
         */
        public void rename(K oldKey, K newKey) {
            this.redis.rename(oldKey, newKey);
        }

        /**
         * 仅当 newKey 不存在时,才将 oldKey 重命名为 newKey
         *
         * @param oldKey
         * @param newKey
         * @return
         */
        public Boolean renameIfAbsent(K oldKey, K newKey) {
            return this.redis.renameIfAbsent(oldKey, newKey);
        }

        /**
         * 返回 key 存储值对应的类型
         *
         * @param key
         * @return
         */
        public DataType type(K key) {
            return this.redis.type(key);
        }
    }

    public static class StringOps<K, V> {
        private final ValueOperations<K, V> valueOps;

        private StringOps(RedisTemplate<K, V> redis) {
            this.valueOps = redis.opsForValue();
        }

        /**
         * 设置 key 对应的 value
         *
         * @param key
         * @param value
         */
        public void set(K key, V value) {
            this.valueOps.set(key, value);
        }

        /**
         * 设置键值,附带过期时间
         *
         * @param key
         * @param value
         * @param timeout
         * @param unit
         */
        public void set(K key, V value, long timeout, TimeUnit unit) {
            this.valueOps.set(key, value, timeout, unit);
        }

        /**
         * 只有当 key 不存在时,才进行设置
         *
         * @param key
         * @param value
         * @return
         */
        public Boolean setIfAbsent(K key, V value) {
            return this.valueOps.setIfAbsent(key, value);
        }

        /**
         * 当 key 不存在时,进行设置,同时指定其过期时间
         * @param key
         * @param value
         * @param timeout
         * @param unit
         * @return
         */
        public Boolean setIfAbsent(K key, V value, long timeout, TimeUnit unit) {
            return this.valueOps.setIfAbsent(key, value, timeout, unit);
        }

        /**
         * 获取 key 对应的 value
         *
         * @param key
         * @return
         */
        public V get(K key) {
            return this.valueOps.get(key);
        }

        /**
         * 批量添加
         *
         * @param map
         */
        public void multiSet(Map<K, V> map) {
            this.valueOps.multiSet(map);
        }

        /**
         * 批量添加键值对(只有当 key 不存在时,才会进行添加)
         *
         * @param map
         * @return
         */
        public Boolean multiSetIfAbsent(Map<K, V> map) {
            return this.valueOps.multiSetIfAbsent(map);
        }

        /**
         * 批量获取 key 对应的 value
         *
         * @param keys
         * @return key 对应的 value(按访问顺序排列)
         */
        public List<V> multiGet(K... keys) {
            return this.valueOps.multiGet(Arrays.asList(keys));
        }

        /**
         * 批量获取 key 对应的 value
         *
         * @param keys
         * @return
         */
        public List<V> multiGet(Collection<K> keys) {
            return this.valueOps.multiGet(keys);
        }

        /**
         * 将指定 key 的值设为 value,并返回 key 的旧值
         *
         * @param key
         * @param value
         * @return key 的旧值
         */
        public V getAndSet(K key, V value) {
            return this.valueOps.getAndSet(key, value);
        }

        /**
         * 将 key 对应的 value 添加一个步进 delta(value 仍以字符串存储)
         *
         * @param key
         * @param delta
         */
        public void increment(K key, long delta) {
            this.valueOps.increment(key, delta);
        }
    }

    public static class HashOps<K, HK, HV> {
        private final HashOperations<K, HK, HV> hashOps;

        private HashOps(RedisTemplate<K, ?> redis) {
            this.hashOps = redis.opsForHash();
        }

        /**
         * 获取 key 对应的哈希表
         *
         * @param key
         * @return
         */
        public Map<HK, HV> getMap(K key) {
            return this.hashOps.entries(key);
        }

        /**
         * 从 key 对应的哈希表中查找 hashKey 的值
         *
         * @param key
         * @param hashKey
         * @return
         */
        public HV get(K key, HK hashKey) {
            return this.hashOps.get(key, hashKey);
        }

        /**
         * 从 key 对应哈希表中批量获取给定字段的值
         *
         * @param key
         * @param hashKeys
         * @return
         */
        public List<HV> multiGet(K key, HK... hashKeys) {
            return this.hashOps.multiGet(key, Arrays.asList(hashKeys));
        }

        /**
         * 从 key 对应哈希表中批量获取给定字段的值
         *
         * @param key
         * @param hashKeys
         * @return
         */
        public List<HV> multiGet(K key, Collection<HK> hashKeys) {
            return this.hashOps.multiGet(key, hashKeys);
        }

        /**
         * 插入 (hashKey,value) 到 key 对应的哈希表中
         *
         * @param key
         * @param hashKey
         * @param value
         */
        public void put(K key, HK hashKey, HV value) {
            this.hashOps.put(key, hashKey, value);
        }

        /**
         * 只有当 key 对应的哈希表不存在 hashKey 时,才进行插入
         *
         * @param key
         * @param hashKey
         * @param value
         * @return
         */
        public Boolean putIfAbsent(K key, HK hashKey, HV value) {
            return this.hashOps.putIfAbsent(key, hashKey, value);
        }

        /**
         * 批量插入到 key 对应的哈希表中
         *
         * @param key
         * @param map
         */
        public void putAll(K key, Map<? extends HK, ? extends HV> map) {
            this.hashOps.putAll(key, map);
        }

        /**
         * 删除一个或多个哈希字段
         *
         * @param key
         * @param hashKeys
         * @return
         */
        public Long delete(K key, HK... hashKeys) {
            return this.hashOps.delete(key, hashKeys);
        }

        /**
         * 哈希表是否存在指定字段
         *
         * @param key
         * @param hashKey
         * @return
         */
        public Boolean exists(K key, HK hashKey) {
            return this.hashOps.hasKey(key, hashKey);
        }

        /**
         * 获取哈希表中的所有字段
         *
         * @param key
         * @return
         */
        public Set<HK> keys(K key) {
            return this.hashOps.keys(key);
        }

        /**
         * 获取哈希表中的所有值
         *
         * @param key
         * @return
         */
        public List<HV> values(K key) {
            return this.hashOps.values(key);
        }


        /**
         * 查看 key 对应哈希表大小
         *
         * @param key
         * @return 哈希表大小
         */
        public Long size(K key) {
            return this.hashOps.size(key);
        }

    }

    public static class ListOps<K, V> {
        private final ListOperations<K, V> listOps;

        private ListOps(RedisTemplate<K, V> redis) {
            this.listOps = redis.opsForList();
        }

        /**
         * 获取列表索引对应元素
         *
         * @param key
         * @param index
         * @return
         */
        public V get(K key, long index) {
            return this.listOps.index(key, index);
        }

        /**
         * 获取列表指定范围内的元素
         *
         * @param key
         * @param start
         * @param end
         * @return
         */
        public List<V> range(K key, long start, long end) {
            return this.listOps.range(key, start, end);
        }

        /**
         * 获取列表所有元素
         *
         * @param key
         * @return
         */
        public List<V> getList(K key) {
            return this.range(key, 0, -1);
        }

        /**
         * 插入数据到列表头部
         *
         * @param key
         * @param value
         * @return
         */
        public Long leftPush(K key, V value) {
            return this.listOps.leftPush(key, value);
        }

        /**
         * value 插入到值 pivot 前面
         *
         * @param key
         * @param pivot
         * @param value
         * @return
         */
        public Long leftPush(K key, V pivot, V value) {
            return this.listOps.leftPush(key, pivot, value);
        }

        /**
         * 批量插入数据到列表头部
         *
         * @param key
         * @param values
         * @return
         */
        public Long leftPushAll(K key, V... values) {
            return this.listOps.leftPushAll(key, values);
        }

        /**
         * 批量插入数据到列表头部
         *
         * @param key
         * @param values
         * @return
         */
        public Long leftPushAll(K key, Collection<V> values) {
            return this.listOps.leftPushAll(key, values);
        }

        /**
         * 插入数据到列表尾部
         *
         * @param key
         * @param value
         * @return
         */
        public Long push(K key, V value) {
            return this.listOps.rightPush(key, value);
        }

        /**
         * value 插入到值 pivot 后面
         *
         * @param key
         * @param pivot
         * @param value
         * @return
         */
        public Long rightPush(K key, V pivot, V value) {
            return this.listOps.rightPush(key, pivot, value);
        }

        /**
         * 设置元素到指定索引位置
         *
         * @param key
         * @param index
         * @param value
         */
        public void set(K key, long index, V value) {
            this.listOps.set(key, index, value);
        }

        /**
         * 移除列表头部元素
         *
         * @param key
         * @return 返回移除的头部元素
         */
        public V leftPop(K key) {
            return this.listOps.leftPop(key);
        }

        /**
         * 移除列表尾部元素
         *
         * @param key
         * @return 返回移除的尾部元素
         */
        public V pop(K key) {
            return this.listOps.rightPop(key);
        }

        /**
         * 删除值为 value 的 count 个元素
         *
         * @param key
         * @param count count = 0: 删除列表所有值为 value 的元素
         *              count > 0: 从头到尾,删除 count 个值为 value 的元素
         *              count < 0: 从尾到头,删除 count 个值为 value 的元素
         * @param value
         * @return 实际删除的元素个数
         */
        public Long remove(K key, long count, V value) {
            return this.listOps.remove(key, count, value);
        }

        /**
         * 删除列表值为 value 的所有元素
         *
         * @param key
         * @param value
         * @return
         */
        public Long removeAll(K key, V value) {
            return this.remove(key, 0, value);
        }

        /**
         * 裁剪列表,只保留 [start, end] 区间的元素
         *
         * @param key
         * @param start
         * @param end
         */
        public void trim(K key, long start, long end) {
            this.listOps.trim(key, start, end);
        }

        /**
         * 获取列表长度
         *
         * @param key
         * @return
         */
        public Long size(K key) {
            return this.listOps.size(key);
        }
    }

    public static class SetOps<K, V> {

        private final SetOperations<K, V> setOps;

        private SetOps(RedisTemplate<K, V> redis) {
            this.setOps = redis.opsForSet();
        }

        /**
         * 集合添加元素
         *
         * @param key
         * @param value
         * @return
         */
        public Long add(K key, V value) {
            return this.setOps.add(key, value);
        }

        /**
         * 弹出元素
         *
         * @param key
         * @return 返回弹出的元素
         */
        public V pop(K key) {
            return this.setOps.pop(key);
        }

        /**
         * 批量移除元素
         *
         * @param key
         * @param values
         * @return
         */
        public Long remove(K key, V... values) {
            return this.setOps.remove(key, values);
        }

        /**
         * 获取集合所有元素
         *
         * @param key
         * @return
         */
        public Set<V> getSet(K key) {
            return this.setOps.members(key);
        }

        /**
         * 获取集合大小
         *
         * @param key
         * @return
         */
        public Long size(K key) {
            return this.setOps.size(key);
        }

        /**
         * 判断集合是否包含指定元素
         *
         * @param key
         * @param value
         * @return
         */
        public Boolean contains(K key, Object value) {
            return this.setOps.isMember(key, value);
        }

        /**
         * 获取 key 集合和其他 key 指定的集合之间的交集
         *
         * @param key
         * @param otherKeys
         * @return
         */
        public Set<V> intersect(K key, Collection<K> otherKeys) {
            return this.setOps.intersect(key, otherKeys);
        }

        /**
         * 获取多个集合的交集
         *
         * @param key
         * @param otherKeys
         * @return
         */
        public Set<V> intersect(K key, K... otherKeys) {
            return this.intersect(key, Stream.of(otherKeys).collect(Collectors.toSet()));
        }

        /**
         * 获取 key 集合和其他 key 指定的集合之间的并集
         *
         * @param key
         * @param otherKeys
         * @return
         */
        public Set<V> union(K key, Collection<K> otherKeys) {
            return this.setOps.union(key, otherKeys);
        }

        /**
         * 获取多个集合之间的并集
         *
         * @param key
         * @param otherKeys
         * @return
         */
        public Set<V> union(K key, K... otherKeys) {
            return this.union(key, Stream.of(otherKeys).collect(Collectors.toSet()));
        }

        /**
         * 获取 key 集合和其他 key 指定的集合间的差集
         *
         * @param key
         * @param otherKeys
         * @return
         */
        public Set<V> difference(K key, Collection<K> otherKeys) {
            return this.setOps.difference(key, otherKeys);
        }

        /**
         * 获取多个集合间的差集
         *
         * @param key
         * @param otherKeys
         * @return
         */
        public Set<V> difference(K key, K... otherKeys) {
            return this.difference(key, Stream.of(otherKeys).collect(Collectors.toSet()));
        }
    }

    public static class ZSetOps<K, V> {
        private final ZSetOperations<K, V> zsetOps;

        private ZSetOps(RedisTemplate<K, V> redis) {
            this.zsetOps = redis.opsForZSet();
        }

        /**
         * 添加元素(有序集合内部按元素的 score 从小到达进行排序)
         *
         * @param key
         * @param value
         * @param score
         * @return
         */
        public Boolean add(K key, V value, double score) {
            return this.zsetOps.add(key, value, score);
        }

        /**
         * 批量删除元素
         *
         * @param key
         * @param values
         * @return
         */
        public Long remove(K key, V... values) {
            return this.zsetOps.remove(key, values);
        }

        /**
         * 增加元素 value 的 score 值
         *
         * @param key
         * @param value
         * @param delta
         * @return 返回元素增加后的 score 值
         */
        public Double incrementScore(K key, V value, double delta) {
            return this.zsetOps.incrementScore(key, value, delta);
        }

        /**
         * 返回元素 value 在有序集合中的排名(按 score 从小到大排序)
         *
         * @param key
         * @param value
         * @return 0 表示排名第一,依次类推
         */
        public Long rank(K key, V value) {
            return this.zsetOps.rank(key, value);
        }

        /**
         * 返回元素 value 在有序集合中的排名(按 score 从大到小排序)
         *
         * @param key
         * @param value
         * @return
         */
        public Long reverseRank(K key, V value) {
            return this.zsetOps.reverseRank(key, value);
        }

        /**
         * 获取有序集合指定范围 [start, end] 之间的元素(默认按 score 由小到大排序)
         *
         * @param key
         * @param start
         * @param end
         * @return
         */
        public Set<V> range(K key, long start, long end) {
            return this.zsetOps.range(key, start, end);
        }

        /**
         * 获取有序集合所有元素(默认按 score 由小到大排序)
         *
         * @param key
         * @return
         */
        public Set<V> getZSet(K key) {
            return this.range(key, 0, -1);
        }

        /**
         * 获取有序集合指定区间 [start, end] 内的所有元素,同时携带对应的 score 值。
         *
         * @param key
         * @param start
         * @param end
         * @return
         */
        public Set<ZSetOperations.TypedTuple<V>> rangeWithScores(K key, long start, long end) {
            return this.zsetOps.rangeWithScores(key, start, end);
        }

        /**
         * 获取 score 介于 [min, max] 之间的所有元素(按 score 由小到大排序)
         *
         * @param key
         * @param min
         * @param max
         * @return
         */
        public Set<V> rangeByScore(K key, double min, double max) {
            return this.zsetOps.rangeByScore(key, min, max);
        }

        /**
         * 获取 score 介于 [min, max] 之间的所有元素,同时携带其 score 值
         *
         * @param key
         * @param min
         * @param max
         * @return
         */
        public Set<ZSetOperations.TypedTuple<V>> rangeByScoreWithScores(K key, double min, double max) {
            return this.zsetOps.rangeByScoreWithScores(key, min, max);
        }

        /**
         * 返回有序集合指定区间 [start, end] 内的所有元素(按元素 score 值从大到小排列)
         *
         * @param key
         * @param start
         * @param end
         * @return
         */
        public Set<V> reverseRange(K key, long start, long end) {
            return this.zsetOps.reverseRange(key, start, end);
        }

        /**
         * 获取有序集合指定区间 [start, end] 内的所有元素,包含其 score 值,且按 score 值由大到小排列
         *
         * @param key
         * @param start
         * @param end
         * @return
         */
        public Set<ZSetOperations.TypedTuple<V>> reverseRangeWithScore(K key, long start, long end) {
            return this.zsetOps.reverseRangeWithScores(key, start, end);
        }

        /**
         * 获取 score 介于 [min, max] 之间的所有元素(按 score 由大到小排序)
         *
         * @param key
         * @param min
         * @param max
         * @return
         */
        public Set<V> reverseRangeByScore(K key, double min, double max) {
            return this.zsetOps.reverseRangeByScore(key, min, max);
        }

        /**
         * 获取 score 介于 [min, max] 之间的所有元素,同时携带其 score 值,元素按 score 值由大到小排序
         *
         * @param key
         * @param min
         * @param max
         * @return
         */
        public Set<ZSetOperations.TypedTuple<V>> reverseRangeByScoreWithScores(K key, double min, double max) {
            return this.zsetOps.reverseRangeByScoreWithScores(key, min, max);
        }


        /**
         * 获取 score 值介于 [min, max] 之间的元素数量
         *
         * @param key
         * @param min
         * @param max
         * @return
         */
        public Long count(K key, double min, double max) {
            return this.zsetOps.count(key, min, max);
        }

        /**
         * 获取有序集合大小
         *
         * @param key
         * @return
         */
        public Long size(K key) {
            return this.zsetOps.size(key);
        }

        /**
         * 获取指定元素 value 的 score 值
         *
         * @param key
         * @param value
         * @return
         */
        public Double score(K key, V value) {
            return this.zsetOps.score(key, value);
        }

        /**
         * 移除指定区间 [start, end] 的元素
         *
         * @param key
         * @param start
         * @param end
         * @return
         */
        public Long removeRange(K key, long start, long end) {
            return this.zsetOps.removeRange(key, start, end);
        }

        /**
         * 移除 score 指定区间 [min, max] 内的所有元素
         *
         * @param key
         * @param min
         * @param max
         * @return
         */
        public Long removeRangeByScore(K key, double min, double max) {
            return this.zsetOps.removeRangeByScore(key, min, max);
        }
    }
}

此工具类大致的使用方式如下:

@SpringBootTest
class RedisDemoApplicationTests {

    @Autowired
    private RedisService redisService;

    @Test
    public void testRedisService() {
        String keyString = "keyString";
        String expectString = "set String value";
        this.redisService.keys().delete(keyString);
        this.redisService.string().set(keyString, expectString);
        String actualString = (String) this.redisService.string().get("keyString");

        String keyHash = "keyHash";
        String hashKey = "hashKey";
        String expectHash = "set Hash value";
        this.redisService.keys().delete(keyHash);
        this.redisService.hash().put(keyHash, hashKey, expectHash);
        String actualHash = (String) this.redisService.hash().get(keyHash, hashKey);

        String keyList = "keyList";
        String expectList = "set List value";
        this.redisService.keys().delete(keyList);
        this.redisService.list().push(keyList, expectList);
        String actualList = (String) this.redisService.list().get(keyList, 0);

        String keySet = "keySet";
        String expectSet = "set Set value";
        this.redisService.keys().delete(keySet);
        this.redisService.set().add(keySet, expectSet);
        String actualSet = (String) this.redisService.set().pop(keySet);

        String keyZSet = "keyZSet";
        String expectZSet = "set Sorted Set value";
        this.redisService.keys().delete(keyZSet);
        this.redisService.zset().add(keyZSet, expectZSet, 1.0);
        Set<Object> actualZSet = this.redisService.zset().getZSet(keyZSet);

        assertAll("test RedisService",
                () -> Assertions.assertEquals(expectString, actualString),
                () -> Assertions.assertEquals(expectHash, actualHash),
                () -> Assertions.assertEquals(expectList, actualList),
                () -> Assertions.assertEquals(expectSet, actualSet),
                () -> assertThat(actualZSet.stream().findFirst().get(), equalTo(expectZSet))
        );
    }
}

附录

  • 示例代码:本文全部示例代码可查看:redis-demo

  • 导入 Jedis:可以将 Redis 客户端更改为Jedis,依赖导入如下所示:

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
        <exclusions>
            <!-- 排除 Lettuce 包 -->
            <exclusion>
                <groupId>io.lettuce</groupId>
                <artifactId>lettuce-core</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    
    <!-- 添加 Jedis 客户端 -->
    <dependency>
        <groupId>redis.clients</groupId>
        <artifactId>jedis</artifactId>
    </dependency>
    
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容