SpringBoot項目中應用Jedis和一些常見配置

優雅的使用Jedis

博客地址:http://www.javashuo.com/article/p-rowvnzdw-mp.html 轉載請註明出處,謝謝html

Redis的Java客戶端有不少,Jedis是其中使用比較普遍和性能比較穩定的一個。而且其API和RedisAPI命名風格相似,推薦你們使用java

在項目中引入Jedis

能夠經過Maven的方式直接引入,目前最新版本是3.2.0git

<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鏈接的網絡開銷,速度很是快。而且經過合理的配置也能實現合理的管理鏈接,分配鏈接。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

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深度歷險:核心原理和應用實踐》 --- 錢文品

相關文章
相關標籤/搜索