博客地址:http://www.javashuo.com/article/p-rowvnzdw-mp.html 转载请注明出处,谢谢html
Redis的Java客户端有不少,Jedis是其中使用比较普遍和性能比较稳定的一个。而且其API和RedisAPI命名风格相似,推荐你们使用java
能够经过Maven的方式直接引入,目前最新版本是3.2.0git
<dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>3.2.0</version> </dependency>
引入Jedis以后,项目能够经过 new 的方式获取 Jedis 使用。程序员
@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链接的网络开销,速度很是快。而且经过合理的配置也能实现合理的管理链接,分配链接。github
@Test public void testConnectionWithPool(){ // 建立链接池 JedisPool jedisPool = new JedisPool(host, port); Jedis jedis = jedisPool.getResource(); // doSomething // 归还链接 jedis.close(); }
这里虽然最后使用的 close() 方法,字面意思看起来好像是关闭链接,实际上点进去能够发现,若是dataSource(链接池)不为空,将执行归还链接的方法redis
@Override public void close() { if (dataSource != null) { if (client.isBroken()) { this.dataSource.returnBrokenResource(this); } else { this.dataSource.returnResource(this); } } else { client.close(); } }
上面归还链接的方法有没有问题呢?试想一下,若是在执行任务的时候,报了异常,那么势必是不能执行 close() 方法的,长此以往池中的 Jedis 链接就会耗尽,整个服务可能就不能在使用了。这个问题在开发和测试环境下通常不容易发现,而生产环境因为使用量增多,就会暴露出来。spring
JedisPool中默认的最大链接数是8个,默认的从池中获取链接超时时间是 -1(表示一直等待)数据库
为了演示不归还链接产生的错误,我写了下面的代码apache
@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 |
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深度历险:核心原理和应用实践》 --- 钱文品