Redis的Java客户端

Redis 的 Java 客户端

  • Jedis
    • 优点:以 Redis 命令作为方法名称,学习成本低廉,简单且实用
    • 缺点:Jedis 的实例是线程不安全的,在多线程的环境下需要基于线程池来使用
  • lettuce(spring 官方默认)
    • 基于 Netty 实现的,支持同步、异步和响应式编程方式,并且是线程安全的。支持 Redis 的哨兵模式、集群模式、管道模式
  • Redisson(适用于分布式的环境)
    • 基于 Redis 实现的分布式、可伸缩的 Java 数据结构的集合。包含 Map、Queue、Lock、Semaphore、AtomicLong 等强大的功能

Jedis

Jedis 基本使用步骤

  1. 引入依赖
  2. 创建 Jedis 对象,建立连接
  3. 使用 Jedis,方法名与 Redis 命令一致
  4. 释放资源

测试 Jedis 相关方法

如果 @BeforeEach报错,记得在 pom 文件里面引入 Junit API 包的依赖

<!-- junit-jupiter-api -->
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-api</artifactId>
    <version>5.8.2</version>
    <scope>test</scope>
</dependency>

(这里以 String 和 Hash 两种类型为例)

package com.lcha.test;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import redis.clients.jedis.Jedis;

import java.util.Map;

public class JedisTest {

<span class="hljs-keyword">private</span> Jedis jedis;

<span class="hljs-meta">@BeforeEach</span>
<span class="hljs-keyword">void</span> <span class="hljs-title function_">setUp</span><span class="hljs-params">()</span>{
    <span class="hljs-comment">//1.建立连接</span>
    jedis = <span class="hljs-keyword">new</span> <span class="hljs-title class_">Jedis</span>(<span class="hljs-string">"xxxxxxxxxx"</span>,<span class="hljs-number">6379</span>);
    <span class="hljs-comment">//2.设置密码</span>
    jedis.auth(<span class="hljs-string">"xxxxxxxxx"</span>);
    <span class="hljs-comment">//3.选择库</span>
    jedis.select(<span class="hljs-number">0</span>);
}

<span class="hljs-meta">@Test</span>
<span class="hljs-keyword">void</span> <span class="hljs-title function_">testStr</span><span class="hljs-params">()</span>{
    <span class="hljs-comment">//4.存入数据</span>
    <span class="hljs-type">String</span> <span class="hljs-variable">result</span> <span class="hljs-operator">=</span> jedis.set(<span class="hljs-string">"name"</span>, <span class="hljs-string">"胡歌"</span>);
    System.out.println(<span class="hljs-string">"result = "</span> + result);
    <span class="hljs-comment">//5.获取数据</span>
    <span class="hljs-type">String</span> <span class="hljs-variable">name</span> <span class="hljs-operator">=</span> jedis.get(<span class="hljs-string">"name"</span>);
    System.out.println(<span class="hljs-string">"name = "</span> + name);
}

<span class="hljs-meta">@Test</span>
<span class="hljs-keyword">void</span> <span class="hljs-title function_">testHash</span><span class="hljs-params">()</span>{
    jedis.hset(<span class="hljs-string">"user:1"</span>,<span class="hljs-string">"name"</span>,<span class="hljs-string">"Jack"</span>);
    jedis.hset(<span class="hljs-string">"user:1"</span>,<span class="hljs-string">"age"</span>,<span class="hljs-string">"21"</span>);

    Map&lt;String, String&gt; map = jedis.hgetAll(<span class="hljs-string">"user:1"</span>);
    System.out.println(map);
}

<span class="hljs-meta">@AfterEach</span>
<span class="hljs-keyword">void</span> <span class="hljs-title function_">tearDown</span><span class="hljs-params">()</span>{
    <span class="hljs-comment">//6.释放连接</span>
    <span class="hljs-keyword">if</span>(jedis != <span class="hljs-literal">null</span>){
        jedis.close();
    }
}

}

Jedis 连接池

Jedis 本身是线程不安全的,并且频繁的创建和销毁连接会有性能损耗,因此我们推荐大家使用 Jedis 连接池代替 Jedis 的直连方式。

首先创建一个 Jedis 连接池工具类

package com.lcha.jedis.util;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

public class JedisConnectionFactory {
private static final JedisPool jedisPool;

<span class="hljs-keyword">static</span> {
    <span class="hljs-comment">//配置连接池</span>
    <span class="hljs-type">JedisPoolConfig</span> <span class="hljs-variable">poolConfig</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">JedisPoolConfig</span>();
    poolConfig.setMaxTotal(<span class="hljs-number">8</span>);  <span class="hljs-comment">//最大连接数:8</span>
    poolConfig.setMaxIdle(<span class="hljs-number">8</span>);   <span class="hljs-comment">//最大空闲连接</span>
    poolConfig.setMinIdle(<span class="hljs-number">0</span>);
    poolConfig.setMaxWaitMillis(<span class="hljs-number">1000</span>);
    <span class="hljs-comment">//创建连接池对象</span>
    jedisPool = <span class="hljs-keyword">new</span> <span class="hljs-title class_">JedisPool</span>(poolConfig,<span class="hljs-string">"xxxx"</span>,<span class="hljs-number">6379</span>,
            <span class="hljs-number">1000</span>,<span class="hljs-string">"xxxx"</span>);
}

<span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> Jedis <span class="hljs-title function_">getJedis</span><span class="hljs-params">()</span>{
    <span class="hljs-keyword">return</span> jedisPool.getResource();
}

}

更改之前 Jedis 的连接方式,采用连接池连接的方式

package com.lcha.test;

import com.lcha.jedis.util.JedisConnectionFactory;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import redis.clients.jedis.Jedis;

import java.util.Map;

public class JedisTest {

<span class="hljs-keyword">private</span> Jedis jedis;

<span class="hljs-meta">@BeforeEach</span>
<span class="hljs-keyword">void</span> <span class="hljs-title function_">setUp</span><span class="hljs-params">()</span>{
    <span class="hljs-comment">//1.建立连接</span>
    <span class="hljs-comment">//jedis = new Jedis("xxxx",6379);</span>
    jedis = JedisConnectionFactory.getJedis();
    <span class="hljs-comment">//2.设置密码</span>
    jedis.auth(<span class="hljs-string">"xxxx"</span>);
    <span class="hljs-comment">//3.选择库</span>
    jedis.select(<span class="hljs-number">0</span>);
}

<span class="hljs-meta">@Test</span>
<span class="hljs-keyword">void</span> <span class="hljs-title function_">testStr</span><span class="hljs-params">()</span>{
    <span class="hljs-comment">//4.存入数据</span>
    <span class="hljs-type">String</span> <span class="hljs-variable">result</span> <span class="hljs-operator">=</span> jedis.set(<span class="hljs-string">"name"</span>, <span class="hljs-string">"胡歌"</span>);
    System.out.println(<span class="hljs-string">"result = "</span> + result);
    <span class="hljs-comment">//5.获取数据</span>
    <span class="hljs-type">String</span> <span class="hljs-variable">name</span> <span class="hljs-operator">=</span> jedis.get(<span class="hljs-string">"name"</span>);
    System.out.println(<span class="hljs-string">"name = "</span> + name);
}

<span class="hljs-meta">@Test</span>
<span class="hljs-keyword">void</span> <span class="hljs-title function_">testHash</span><span class="hljs-params">()</span>{
    jedis.hset(<span class="hljs-string">"user:1"</span>,<span class="hljs-string">"name"</span>,<span class="hljs-string">"Jack"</span>);
    jedis.hset(<span class="hljs-string">"user:1"</span>,<span class="hljs-string">"age"</span>,<span class="hljs-string">"21"</span>);

    Map&lt;String, String&gt; map = jedis.hgetAll(<span class="hljs-string">"user:1"</span>);
    System.out.println(map);
}

<span class="hljs-meta">@AfterEach</span>
<span class="hljs-keyword">void</span> <span class="hljs-title function_">tearDown</span><span class="hljs-params">()</span>{
    <span class="hljs-keyword">if</span>(jedis != <span class="hljs-literal">null</span>){
        jedis.close();
    }
}

}

注意:当使用连接池连接时,代码最后的 if(jedis != null){jedis.close();}不会真正的销毁连接,而是将本连接归还到连接池中

源码如下:

public void close() {
        if (this.dataSource != null) {
            Pool<Jedis> pool = this.dataSource;
            this.dataSource = null;
            if (this.isBroken()) {
                pool.returnBrokenResource(this);
            } else {
                pool.returnResource(this); // 注意这里!!!!
            }
        } else {
            this.connection.close();
        }
}

SpringDataRedis

SpringData 是 Spring 中数据操作的模块,包含对各种数据库的集成,其中对 Redis 的集成模块就叫做 SpringDataRedis

官网地址:https://spring.io/projects/spring-data-redis

  • 提供了对不同 Redis 客户端的整合(Lettuce 和 Jedis)
  • 提供了 RedisTemplate 统一 API 来操作 Redis
  • 支持 Redis 的发布订阅模型
  • 支持 Redis 哨兵和 Redis 集群
  • 支持基于 Lettuce 的响应式编程
  • 支持基于 JDK、JSON、字符串、Spring 对象的数据序列化及反序列化
  • 支持基于 Redis 的 JDKCollection 实现

RedisTemplate 工具类

API 返回值类型 说明
RedisTemplate.opsForValue() ValueOperations 操作 String 类型数据
RedisTemplate.opsForHash() HashOperations 操作 Hash 类型数据
RedisTemplate.opsForList() ListOperations 操作 List 类型数据
RedisTemplate.opsForSet() SetOperations 操作 Set 类型数据
RedisTemplate.opsForZSet() ZSetOperations 操作 SortedSort 类型数据
RedisTemplate 通用命令

使用步骤

  1. 引入 spring-boot-starter-data-redis 依赖

    <!-- redis依赖 -->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-data-redis</artifactId>
            </dependency>
            <!-- common-pool -->
            <dependency>
                <groupId>org.apache.commons</groupId>
                <artifactId>commons-pool2</artifactId>
            </dependency>
    
  2. 在 application.yml 文件中配置 Redis 信息

    spring:
      redis:
        host: xxxx
        port: 6379
        password: xxxx
        lettuce:
          pool:
            max-active: 8
            max-idle: 8
            min-idle: 0
            max-wait: 100ms
    
  3. 注入 RedisTemplate 并使用

package com.lcha;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;

@SpringBootTest
class RedisDemoApplicationTests {

<span class="hljs-meta">@Autowired</span>
<span class="hljs-keyword">private</span> RedisTemplate redisTemplate;

<span class="hljs-meta">@Test</span>
<span class="hljs-keyword">void</span> <span class="hljs-title function_">testString</span><span class="hljs-params">()</span> {
    <span class="hljs-comment">//写入一条String数据</span>
    redisTemplate.opsForValue().set(<span class="hljs-string">"name"</span>, <span class="hljs-string">"胡歌"</span>);
    <span class="hljs-comment">//获取string数据</span>
    <span class="hljs-type">Object</span> <span class="hljs-variable">name</span> <span class="hljs-operator">=</span> redisTemplate.opsForValue().get(<span class="hljs-string">"name"</span>);
    System.out.println(<span class="hljs-string">"name = "</span> + name);
}

}

序列化问题

RedisTemplate 可以接收任意 Object 作为值写入 Redis,只不过写入前会把 Object 序列化为字节形式,默认是采用 JDK 序列化,得到的结果是这样的:

缺点:

  1. 可读性差
  2. 内存占用较大

解决方法:改变序列化器

自定义 RedisTemplate 序列化方式

package com.lcha.redis.config;

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.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfig {

<span class="hljs-meta">@Bean</span>
<span class="hljs-keyword">public</span> RedisTemplate&lt;String, Object&gt; <span class="hljs-title function_">redisTemplate</span><span class="hljs-params">(RedisConnectionFactory connectionFactory)</span> {
    <span class="hljs-comment">//创建 RedisTemplate 对象</span>
    RedisTemplate&lt;String, Object&gt; template = <span class="hljs-keyword">new</span> <span class="hljs-title class_">RedisTemplate</span>&lt;&gt;();
    <span class="hljs-comment">//设置连接工厂</span>
    template.setConnectionFactory(connectionFactory);
    <span class="hljs-comment">//创建 JSON 序列化工具</span>
    <span class="hljs-type">GenericJackson2JsonRedisSerializer</span> <span class="hljs-variable">jsonRedisSerializer</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">GenericJackson2JsonRedisSerializer</span>();
    <span class="hljs-comment">//设置 Key 的序列化</span>
    template.setKeySerializer(RedisSerializer.string());
    template.setHashKeySerializer(RedisSerializer.string());
    <span class="hljs-comment">//设置 Value 的序列化</span>
    template.setValueSerializer(jsonRedisSerializer);
    template.setHashValueSerializer(jsonRedisSerializer);
    <span class="hljs-comment">//返回</span>
    <span class="hljs-keyword">return</span> template;
}

}

重新运行刚才的代码,结果如下图所示:

存储对象数据时也是一样的

  1. 创建一个对象类

    package com.lcha.redis.pojo;
    

    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public class User {
    private String name;
    private Integer age;
    }

  2. 编写测试方法

  3. 	@Test
        void testSaveUser(){
            redisTemplate.opsForValue().set("user:100", new User("胡歌",21));
            User o = (User) redisTemplate.opsForValue().get("user:100");
            System.out.println("o =" + o);
        }
    
  4. 打印结果

JSON 方式依然存在的缺陷

尽管 JSON 的序列化方式可以满足我们的需求,但是依然存在一些问题。

为了在反序列化时知道对象的类型,JSON 序列化器会将类的 class 类型写入 json 结果中,存入 Redis,会带来额外的内存开销。

如何解决

为了节省内存空间,我们并不会使用 JSON 序列化器来处理 value,而是统一使用 String 序列化器,要求只能存储 String 类型的 key 和 value。当需要存储 Java 对象时,手动完成对象的序列化和反序列化。

  1. 直接使用 StringRedisTemplate 即可

    package com.lcha;
    

    import com.fasterxml.jackson.core.JsonProcessingException;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import com.lcha.redis.pojo.User;
    import org.junit.jupiter.api.Test;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.data.redis.core.StringRedisTemplate;

    @SpringBootTest
    class RedisStringTests {

    <span class="hljs-meta">@Autowired</span>
    <span class="hljs-keyword">private</span> StringRedisTemplate stringRedisTemplate;
    
    <span class="hljs-meta">@Test</span>
    <span class="hljs-keyword">void</span> <span class="hljs-title function_">testString</span><span class="hljs-params">()</span> {
        <span class="hljs-comment">//写入一条String数据</span>
        stringRedisTemplate.opsForValue().set(<span class="hljs-string">"name"</span>, <span class="hljs-string">"胡歌"</span>);
        <span class="hljs-comment">//获取string数据</span>
        <span class="hljs-type">Object</span> <span class="hljs-variable">name</span> <span class="hljs-operator">=</span> stringRedisTemplate.opsForValue().get(<span class="hljs-string">"name"</span>);
        System.out.println(<span class="hljs-string">"name = "</span> + name);
    }
    
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> <span class="hljs-type">ObjectMapper</span> <span class="hljs-variable">mapper</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">ObjectMapper</span>();
    
    <span class="hljs-meta">@Test</span>
    <span class="hljs-keyword">void</span> <span class="hljs-title function_">testSaveUser</span><span class="hljs-params">()</span> <span class="hljs-keyword">throws</span> JsonProcessingException {
        <span class="hljs-comment">//创建对象</span>
        <span class="hljs-type">User</span> <span class="hljs-variable">user</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">User</span>(<span class="hljs-string">"虎哥"</span>,<span class="hljs-number">21</span>);
        <span class="hljs-comment">//手动序列化</span>
        <span class="hljs-type">String</span> <span class="hljs-variable">json</span> <span class="hljs-operator">=</span> mapper.writeValueAsString(user);
        <span class="hljs-comment">//写入数据</span>
        stringRedisTemplate.opsForValue().set(<span class="hljs-string">"user:200"</span>, json);
        <span class="hljs-comment">//获取数据</span>
        <span class="hljs-type">String</span> <span class="hljs-variable">jsonUser</span> <span class="hljs-operator">=</span> stringRedisTemplate.opsForValue().get(<span class="hljs-string">"user:200"</span>);
        <span class="hljs-type">User</span> <span class="hljs-variable">user1</span> <span class="hljs-operator">=</span> mapper.readValue(jsonUser, User.class);
        System.out.println(<span class="hljs-string">"user1 = "</span> + user1);
    }
    

    }

  2. 结果如下

对 Hash 类型的操作

  1. 编写方法

    @Test
        void testHash(){
            stringRedisTemplate.opsForHash().put("user:300", "name", "张三");
            stringRedisTemplate.opsForHash().put("user:300", "age", "18");
    
        Map&lt;Object, Object&gt; entries = stringRedisTemplate.opsForHash().entries(<span class="hljs-string">"user:300"</span>);
        System.out.println(<span class="hljs-string">"entries = "</span> + entries);
    
    }
    

  2. 结果如下