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

优雅的使用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就会报错,错误信息如下

(数据科学学习手札81)conda+jupyter玩转数据科学环境搭建

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深度历险:焦点原理和应用实践》 — 钱文品

原创文章,作者:28qn新闻网,如若转载,请注明出处:https://www.28qn.com/archives/3510.html