Redis丨Spring Boot 整合

首页 / 默认分类 / 正文

前言

学完 Redis 的基本操作命令后,就可以学习如何使用编程语言 Java 去操作 Redis 数据库(我们使用实现了 JDBC 规范的框架去操作 MySQL 数据库)。打开 Redis 官网,找到对应的 Java 客户端, 可以看到从上往下依次是 JedisLettuceRedisson...

Jedis

Redis 官网对其的描述:A blazingly small and sane Redis Java client,意为:一个非常小且稳健的 Redis Java 客户端。那么按照它的使用说明文档来测试一下。

引入依赖

Maven 项目中在 pom.xml 文件添加以下依赖:

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>4.0.1</version>
</dependency>

连接池

我们很少直接使用 Jedis,而是要用到 Jedis 的连接池 —— JedisPool。因为 Jedis 对象并不是线程安全的,当我们要使用 Jedis 对象时,需要从连接池中拿出一个 Jedis 对象独占,使用完毕后再将这个对象还给连接池。
你可以使用 try-catch-finally 代码块处理,在 finally 块中关闭连接,归还资源。

BufferedWriter writer = null;
try {
    writer = new BufferedWriter(new FileWriter(fileName));
    // 执行操作
    writer.write(str);
} catch (IOException e) {
   // 捕获异常
} finally {
    // finally 块中释放资源
    try {
        if (writer != null)
            writer.close();
    } catch (IOException e) {
       // 捕获异常
    }
}

这里只是举例子,不探讨 finally 块中的代码是否一定执行。
try-with-resources 的语法几乎与 try-catch-finally 的语法相同。唯一的区别是括号后的 try,我们在其中声明将使用的资源:

try(BufferedWriter writer = new BufferedWriter(new FileWriter(fileName))) {
    // 执行操作
    writer.write(str);
}catch(IOException e){
    // 捕获异常
}

可以理解为:在 try 块中完成用户操作后,会立即调用它们的 .close() 方法,关闭资源是自动完成的。所以,Jedis 的文档示例可以这么使用:

try (Jedis jedis = pool.getResource()) {
  jedis.set("clientName", "Jedis");
}

命令测试

这里测试连接的是 Linux 端的 Redis,需要注意的是:JedisPool 有多个构造方法,我这里使用的只是其中一个。

public JedisPool(String host, int port, String user, String password) {
    this(new GenericObjectPoolConfig(), host, port, user, password);
}

如果出现了“WRONGPASS invalid username-password pair or user is disabled.”这个错误,那么检查一下用户名和密码是否正确,默认的用户名不是 root,而是 default!!!

@SpringBootTest
public class RedisDemoApplicationTest {

    /**
     * 测试 Redis 的链接
     *
     * @author yyt
     * @date 2022/1/22 20:24
     */
    @Test
    void setup() {
        // user 不是 root,而是 default
        JedisPool pool = new JedisPool("xxx.xx.xxx.xxx", 6379, "default", "******");
        // Jedis 连接池,用完会自动 close
        try (Jedis jedis = pool.getResource()) {
            // 如果 Redis 服务连接需要密码,就设置密码
            jedis.auth("******");
            if ("PONG".equals(jedis.ping())) {
                System.out.println("Redis 连接成功!!!");
                // 操作字符串
                testStringCommands(jedis);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

客户端 Jedis 的方法名和 Redis 的操作命令基本一致,我这里以操作 String 类型为例:

public void testStringCommands(Jedis jedis) throws InterruptedException {
    System.out.println("清空当前使用的数据库:" + jedis.flushDB());
    System.out.println("【SET key value】新增键值对:" + jedis.set("year", "2021"));
    System.out.println("新增<'month','12'>的键值对:" + jedis.set("month", "12"));
    System.out.println("新增<'newyear','2022'>的键值对:" + jedis.set("newyear", "2022"));
    System.out.println("新增<'clientName','Jedis'>的键值对:" + jedis.set("clientName", "Jedis"));
    System.out.println("新增<'sentence','xxx'>的键值对:"
                + jedis.set("sentence", "The quick brown fox jumps over a lazy dog"));
    System.out.println("【MSET key value】批量设置键值对:"
                + jedis.mset("key1", "mytext", "key2", "mynewtext"));
    // 前提是 value 可以正确转换为 64 位有符号 integer 类型
    System.out.println("【DECR key】对指定 key 自减 1:" + jedis.decr("year"));
    // 如果不指定步长 increment,就从 0 开始
    System.out.println("【DECRBY key decrement】对指定 key 减去 decrement:"
                + jedis.decrBy("year", 20));
    System.out.println("【INCR key】对指定 key 自增 1:" + jedis.incr("month"));
    System.out.println("【INCRBY key increment】对指定 key 增加 increment:"
                + jedis.incrBy("month", 100));
    // redis 没有 double 类型,实际上是将字符串转为双精度的数字,计算之后转为字符串类型进行赋值
    System.out.println("【INCRBYFLOAT key increment】对指定 key 增加浮点数 increment:"
                + jedis.incrByFloat("month", 10.24));
    // 前提是 key 是存在的,而且 value 是 String 类型
    // windows 本地的 redis 的好像不支持这个命令,从 6.2.0 开始新增的
    System.out.println("【GETDEL key】获取 key 的值后,就删除 key:" + jedis.getDel("year"));
    // GetExParams params 对 key 设置过期时间:秒,毫秒,微妙,纳秒
    System.out.println("【GETEX key】获取 key 的值,并选择设置其过期时间:"
                + jedis.getEx("newyear", new GetExParams().exAt(60L)));
    System.out.println("查看键 newyear 的剩余过期时间:" + jedis.ttl("newyear"));
    System.out.println("【GETRANGE key start end】获取 key 从 start 到 end 区间的字符串:"
                + jedis.getrange("sentence", 10, 18));
    System.out.println("判断某个键是否存在:" + jedis.exists("clientName"));
    System.out.println("【GET key】获取指定 key 的值:" + jedis.get("clientName"));
    System.out.println("【MGET key】批量获取键值对:" + jedis.mget("key1", "key2"));
    // 注意,这个算法不同于最长公共字符串算法,因为它匹配的部分不需要是相邻的
    // strAlgoLCSKeys 返回的是一个 LCSMatchResult 类型对象
    System.out.println("【LCS key1 key2】最长的公共子序列算法:"
                + jedis.strAlgoLCSKeys("key1", "key2", new StrAlgoLCSParams().len()).getLen());
    // 如果 key 不存在,则效果等同于 set(key, value)
    System.out.println("【APPEND key value】在指定 key 末尾拼接 value:"
                + jedis.append("clientName", "NB"));
    // 当存在一个相同的 key,所有的键值对都修改失败,返回 1 说明设置成功,返回 0 说明未设置成功
    System.out.println("【MSETNX key value】将给定的 key 设置各自的值:"
                + jedis.msetnx("key3", "Hello", "key4", "World"));
    System.out.println("【PSETEX key milliseconds value】对 key 设置毫秒级别的有效时间:"
                + jedis.psetex("key5", 1000, "todo"));
    System.out.println("【SETRANGE key offset value】覆盖 key 原来的 value,覆盖的位置从 offset 开始:"
                + jedis.setrange("sentence", 0, "现在是2022年1月18日 22:17:38"));
    System.out.println("【STRLEN key】返回 key 的值的长度:" + jedis.strlen("sentence"));
    System.out.println("-----以下是过期弃用的方法-----");
    // 注意,从 6.2.0 开始,此命令被视为已弃用
    System.out.println("【GETSET key value】获取旧的 value,并用新的 value 进行覆盖:"
                + jedis.getSet("month", "1"));
    // 注意,从 2.0.0 开始,此命令被视为已弃用,使用 GETRANGE 命令代替
    System.out.println("【SUBSTR key start end】返回 key 从 start 到 end 区间的字符串:" +
                jedis.substr("sentence", 0, 12));
}

测试执行事务:

public void testTransaction(Jedis jedis) throws InterruptedException {
    // 开启事务
    Transaction transaction = jedis.multi();
    // 事务操作命令
    try {
        transaction.set("in", "6000");
        transaction.set("out", "0");
        transaction.decrBy("in", 2100L);
        transaction.incrBy("out", 2100L);
        // 执行事务
        transaction.exec();
    } catch (Exception e) {
        // 抛出异常就说明事务执行失败,就放弃
        transaction.discard();
        e.printStackTrace();
    } finally {
        System.out.println("【测试事务】:" + jedis.get("in"));
        System.out.println("【测试事务】:" + jedis.get("out"));
    }
}

Lettuce

Spring Boot 框架中已经集成了 Redis,在 1.X 版本时默认使用的 Jedis 客户端,现在是 2.X 版本默认使用的 Lettuce 客户端。
默认使用 lettuce
在 Spring Boot 项目的 pom.xml 文件中添加以下依赖:

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

按住 Ctrl 键单击 spring-boot-starter-data-redis 这个依赖,就会发现它包含了 Spring Data RedisLettuce 等关键依赖。

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

    <dependency>
      <groupId>org.springframework.data</groupId>
      <artifactId>spring-data-redis</artifactId>
      <version>2.6.0</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>

区别

因为 Jedis 的缺点很明显:

  • 使用阻塞的 I/O,且其方法调用都是同步的,程序流需要等到 sockets 处理完 I/O 才能执行,不支持异步;
  • Jedis 是直连模式,在多个线程间共享一个 Jedis 实例时是线程不安全的,于是需要维护一个连接池,每个线程需要时从连接池取出连接实例,完成操作后或者遇到异常归还实例。当连接数随着业务不断上升时,对物理连接的消耗也会成为性能和稳定性的潜在风险点。

而 Lettuce 的 API 是线程安全的,底层基于 Netty,连接实例可以在多个线程间共享,还支持异步模式。如果不是执行阻塞和事务操作,如 BLPOP、MULTI 或 EXEC,多个线程就可以共享一个连接。
Spring Boot 框架默认使用 Lettuce,就说明它对比 Jedis 有独到之处。看下 Spring Data Redis 帮助文档给出的对比表格:
对比

其中 X 标记的表示支持的意思,不是错误的意思。

这么一对比,Jedis 支持的 Lettuce 都支持甚至更多;Jedis 不支持的 Lettuce 也支持。

配置属性

如下图所示,在名为 org.springframework.boot:spring-boot-autoconfigure 的 JAR 文件中,打开 META-INF 下自动配置的列表 spring.factories,使用组合键 Ctrl + F 找到 Spring Data Redis 的自动配置类 RedisAutoConfiguration,按住 Ctrl 键单击它就会跳转到对应的 RedisAutoConfiguration.class 文件。
RedisAutoConfiguration
RedisAutoConfiguration 自身主要的作用就是确保需要的 Bean 都在容器中,所以通过使用 @ConditionalOnMissingBean 注解配置了默认的 Bean:RedisTemplate 和 StringRedisTemplate。

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

    // 默认提供一个 RedisTemplate
    @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;
    }

    // 因为 String 类型很常用,所以单独提供一个 StringRedisTemplate
    @Bean
    @ConditionalOnMissingBean
    @ConditionalOnSingleCandidate(RedisConnectionFactory.class)
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
        return new StringRedisTemplate(redisConnectionFactory);
    }
}

注意 @EnableConfigurationProperties 注解自动映射一个 POJO 到 Spring Boot 配置文件的属性集,给自动配置提供具体的配置参数,那么打开 RedisProperties.class 文件,可以看到配置的前缀名是 spring.redis,可配置的属性有 database、url、host、username 等等,这些都可以在 application.properties 配置文件中体现。

@ConfigurationProperties(prefix = "spring.redis")
public class RedisProperties {
    private int database = 0;
    private String url;
    private String host = "localhost";
    private String username;
    private String password;
    private int port = 6379;
    private boolean ssl;
    private Duration timeout;
    private Duration connectTimeout;
    private String clientName;
    private RedisProperties.ClientType clientType;
    private RedisProperties.Sentinel sentinel;
    private RedisProperties.Cluster cluster;
    private final RedisProperties.Jedis jedis = new RedisProperties.Jedis();
    private final RedisProperties.Lettuce lettuce = new RedisProperties.Lettuce();
    
    ...
}

设置配置文件:当然你也可以使用 RedisURI 来连接。

spring:
  # 配置 Redis
  redis:
    # 默认使用的数据库
    database: 0
    # IP
    host: xxx.xx.xxx.xxx
    # 用户名
    username: default
    # 密码
    password: ******
    # 端口号
    port: 6379
    # 不用 Jedis pool,使用 Lettuce pool
    lettuce:
      pool:
        enabled: true

命令测试

为了简单(偷懒),这里只是简单测试一下基础数据类型,那些同步、异步、响应式的 API 的使用延后吧。

@SpringBootTest
public class RedisSpringBootTest {

    @Autowired
    private RedisTemplate<Object, Object> redisTemplate;

    @Test
    void contextLoaded() {
        // 获取 redis 连接对象
        RedisConnection connection = Objects.requireNonNull(redisTemplate.getConnectionFactory()).getConnection();
        // 清空数据
        connection.flushDb();
        connection.flushDb();
        // 测试操作不同的数据类型
        StrTest();
        // 关闭连接
        connection.close();
    }

    // 以 opsFor 为开头(OperationsFor),表示操作 String 类型数据
    void StrTest() {
        // set(K key, V value) 设置键值对
        redisTemplate.opsForValue().set("ClientName", "Lettuce");
        System.out.println(redisTemplate.opsForValue().get("ClientName"));
    }

    // 操作 List 类型数据
    void ListTest() {
        // leftPush(K key, V value) 在集合左边添加元素值
        redisTemplate.opsForList().leftPush("listKey1", "value1");

    }

    // 操作 Hash 类型数据
    void HashTest() {
        // put() 添加值
        redisTemplate.opsForHash().put("myhash", "field1", "value1");
    }

    // 操作 Set 类型数据
    void SetTest() {
        // add() 添加值
        redisTemplate.opsForSet().add("myset", "Hello", "World", "2022");
    }

    // 操作 ZSet 类型数据
    void ZSetTest() {
        // add() 添加值
        redisTemplate.opsForZSet().add("myzset", "996", 100.0);
    }
}

参考

Jedis 文档
Lettuce 文档
Spring Data Redis 文档
打赏
评论区
头像
文章目录