­

SpringBoot项目中应用Jedis和一些常见配置

  • 2020 年 3 月 31 日
  • 笔记

优雅的使用Jedis

博客地址:https://www.cnblogs.com/keatsCoder/p/12609109.html 转载请注明出处,谢谢

Redis的Java客户端有很多,Jedis是其中使用比较广泛和性能比较稳定的一个。并且其API和RedisAPI命名风格类似,推荐大家使用

在项目中引入Jedis

可以通过Maven的方式直接引入,目前最新版本是3.2.0

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

直连及使用连接池

Jedis直连

引入Jedis之后,项目可以通过 new 的方式获取 Jedis 使用。

  • 首先在yml中配置好 redis 的地址和端口
@SpringBootTest(classes = RedisCliApplication.class)  @RunWith(SpringRunner.class)  public class JedisConnectionDemo {      @Value("${redis.host}")      private String host;      @Value("${redis.port}")      private int port;          @Test      public void testConnection(){          // 建立连接          Jedis jedis = new Jedis(host, port);          // 添加 key-value。添加成功则返回OK          String setResult = jedis.set("name", "keats");          Assert.assertEquals("OK", setResult);          // 通过 key 获取 value          String value = jedis.get("name");          Assert.assertEquals("keats", value);          // 关闭连接          jedis.close();      }  }  

使用连接池

直连的话每次都会新建TCP连接和断开TCP连接,这个过程是很耗时的,对于Redis这种需要频繁访问和高效访问的软件显然是不合适的。并且也不方便对连接进行管理。类似数据库连接池思想,Jedis也提供了JedisPool连接池进行连接池管理。所有的Jedis对象预先放在JedisPool中,客户端需要使用的时候从池中借用,用完后归还到池中。这样避免了频繁建立和断开TCP连接的网络开销,速度非常快。并且通过合理的配置也能实现合理的管理连接,分配连接。

@Test  public void testConnectionWithPool(){      // 创建连接池      JedisPool jedisPool = new JedisPool(host, port);        Jedis jedis = jedisPool.getResource();        // doSomething        // 归还连接      jedis.close();  }  

这里虽然最后使用的 close() 方法,字面意思看起来好像是关闭连接,实际上点进去可以发现,如果dataSource(连接池)不为空,将执行归还连接的方法

@Override  public void close() {      if (dataSource != null) {          if (client.isBroken()) {              this.dataSource.returnBrokenResource(this);          } else {              this.dataSource.returnResource(this);          }      } else {          client.close();      }  }  

连接池使用的一个常见问题

上面归还连接的方法有没有问题呢?试想一下,如果在执行任务的时候,报了异常,那么势必是不能执行 close() 方法的,久而久之池中的 Jedis 连接就会耗尽,整个服务可能就不能在使用了。这个问题在开发和测试环境下一般不容易发现,而生产环境由于使用量增多,就会暴露出来。

JedisPool中默认的最大连接数是8个,默认的从池中获取连接超时时间是 -1(表示一直等待)

为了演示不归还连接产生的错误,我写了下面的代码

@Test  public void testConnectionNotClose(){      // 创建连接池      JedisPoolConfig poolConfig = new JedisPoolConfig();      poolConfig.setMaxWaitMillis(5000L); // 等待Jedis连接超时时间      JedisPool jedisPool = new JedisPool(poolConfig, host, port);        try {          for (int i = 1; i <= 10; i++) {              Jedis jedis = jedisPool.getResource();              System.out.println(i);              // doSomething          }      } catch (Exception e) {          e.printStackTrace();      }  }  

循环前8次,分别从池中获取一个连接进行使用而不归还。第9次的时候想要获取连接已经没有了。默认情况下会一直等待。而我更改了配置是5S,等待5S就会报错,错误信息如下

1  2  3  4  5  6  7  8  redis.clients.jedis.exceptions.JedisException: Could not get a resource from the pool  	at redis.clients.util.Pool.getResource(Pool.java:51)  	at redis.clients.jedis.JedisPool.getResource(JedisPool.java:99)  	at cn.keats.rediscli.jedis.JedisConnectionDemo.testConnectionNotClose(JedisConnectionDemo.java:64)  	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)  	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)  	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)  	at java.lang.reflect.Method.invoke(Method.java:498)  	at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)  	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)  	at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)  	at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)  	at org.springframework.test.context.junit4.statements.RunBeforeTestExecutionCallbacks.evaluate(RunBeforeTestExecutionCallbacks.java:74)  	at org.springframework.test.context.junit4.statements.RunAfterTestExecutionCallbacks.evaluate(RunAfterTestExecutionCallbacks.java:84)  	at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:75)  	at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:86)  	at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:84)  	at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)  	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:251)  	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:97)  	at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)  	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)  	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)  	at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)  	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)  	at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)  	at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70)  	at org.junit.runners.ParentRunner.run(ParentRunner.java:363)  	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:190)  	at org.junit.runner.JUnitCore.run(JUnitCore.java:137)  	at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)  	at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)  	at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)  	at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)  Caused by: java.util.NoSuchElementException: Timeout waiting for idle object  	at org.apache.commons.pool2.impl.GenericObjectPool.borrowObject(GenericObjectPool.java:439)  	at org.apache.commons.pool2.impl.GenericObjectPool.borrowObject(GenericObjectPool.java:349)  	at redis.clients.util.Pool.getResource(Pool.java:49)  	... 32 more  

无论是报错还是一直等待,这在生产环境中无异于宕机。所以这个操作一定是要避免掉的。那么我在执行代码的最后一句写上 close() 是不是就高枕无忧了呢?认真从前面都过来的同学肯定会说不是的。因为当代码一旦抛出异常。是不能执行到 close() 方法的。

@Test  public void testConnectionWithException() {      // 创建连接池      JedisPoolConfig poolConfig = new JedisPoolConfig();      poolConfig.setMaxWaitMillis(5000L); // 等待Jedis连接超时时间      JedisPool jedisPool = new JedisPool(poolConfig, host, port);        for (int i = 1; i <= 8; i++) {          System.out.println(i);          try {              new Thread(() -> {                  Jedis jedis = jedisPool.getResource();                  // doSomething                  // 模拟一个错误                  int j = 1 / 0;                    jedis.close();              }).run();          } catch (Exception e) {              // 服务器运行过程中出现了8次异常,没有执行到close方法          }        }      // 第9次无法获取连接      Jedis jedis = jedisPool.getResource();  }  

这样还会报和上面一样的错误。推荐使用 Java7 之后的 try with resources 写法来完成连接归还。

try (Jedis jedis = jedisPool.getResource()) {      new Thread(() -> {          // doSomething          // 模拟一个错误          int j = 1 / 0;            jedis.close();      }).run();  } catch (Exception e) {      // 异常处理  }  

这样相当于写了 finally。在正常执行/出错时都会执行 close() 方法关闭连接。除非代码中写了死循环。

这样写还有一个弊端就是有的小伙伴可能忘记归还,《Redis深度历险:核心原理和应用实践》作者老钱介绍了一种强制归还的连接池管理办法:

通过一个特殊的自定义的 RedisPool 对象将 JedisPool 对象隐藏起来,避免程序员直接使用它的 getResource 方法而忘记了归还。程序员使用 RedisPool 对象时需要提供一个
回调类来才能使用 Jedis 对象。结合 Java8 的 Lambda 表达式。使用起来也还可以。但是因此产生了闭包的问题,Lambda中的匿名内部类无法访问外部的变量。他又采用了 Hodler 来将变量包装以达到其被访问的目的。大佬的方法很厉害。但是个人愚见,这样代码的复杂度提高了很多。对于一个使用完Resource完后忘记归还的程序员来说写起来可能比较复杂。所以就不在博客中贴出了。感兴趣的伙伴可以读一下老钱的书或者从我的GITHUB中查阅老钱的代码:优雅的Jedis-老钱

连接池配置详解

除了使用默认构造方法初始化连接池外,Jedis还提供了配置类来初始化

JedisPoolConfig poolConfig = new JedisPoolConfig();  poolConfig.setMaxWaitMillis(5000L); // 等待Jedis连接超时时间  JedisPool jedisPool = new JedisPool(poolConfig, host, port);  

配置类常用的参数解释如下:

参数名 含义 默认值
maxActive 连接池中的最大连接数 8
maxIdle(minIdle) 连接池中的最大(小)空闲连接数 8(0)
maxWaitMillis 当链接池没有连接时,调用者的最大等待时间,单位是毫秒。不建议使用默认值 -1 表示一直等
jmxEnabled 是否开启jmx监控
minEvictableIdleTimeMillis 连接的最小空闲时间,达到此值后空闲连接将被移除 1800000L 30分钟
numTestsPerEvictionRun 做空闲连接检测时,每次的采样数 3
testOnBorrow 向连接池借用连接时是否做连接有效性检测(Ping)无效连接将会被删除 false
testOnReturn 是否做周期性空闲检测 false
testWhileIdle 向连接池借用连接时是否做空闲检测,空闲超时的将会被移除 false
timeBetweenEvictionRunsMillis 空闲连接的检测周期,单位为毫秒 -1 不做检测
blockWhenExhausted 当连接池资源耗尽时,调用者是否需要等待。和maxWaitMillis对应,当它为true时,maxWaitMillis生效 true

PipeLine一次执行多个命令

Redis虽然提供了 mset、mget 等方法。但是并未提供 mdel 方法。我们在业务中如果遇到一次 mget 后,有多个需要删除的 key,可以通过 PipeLine 来模拟 mdel。虽然操作不是原子性的,但大多数情况下也能满足要求:

@Test  public void testPipeline() {      // 创建连接池      JedisPool jedisPool = new JedisPool(host, port);      try (Jedis jedis = jedisPool.getResource()){          Pipeline pipelined = jedis.pipelined();          // doSomething 获取 keys          List<String> keys = new ArrayList<>();            // pipelined 添加命令          for (String key : keys) {              pipelined.del(key);          }          // 执行命令          pipelined.sync();      }  }  

项目代码

在学习Redis的过程中,我将博客中的代码都在Github中上传,以便小伙伴们核对。项目地址:https://github.com/keatsCoder/redis-cli

参考文献

《Redis开发与运维》 — 付磊 张益军

《Redis深度历险:核心原理和应用实践》 — 钱文品