SpringBoot專案中應用Jedis和一些常見配置
阿新 • • 發佈:2020-04-01
# 優雅的使用Jedis
部落格地址:https://www.cnblogs.com/keatsCoder/p/12609109.html 轉載請註明出處,謝謝
Redis的Java客戶端有很多,Jedis是其中使用比較廣泛和效能比較穩定的一個。並且其API和RedisAPI命名風格類似,推薦大家使用
## 在專案中引入Jedis
可以通過Maven的方式直接引入,目前最新版本是3.2.0
```xml
redis.clients
jedis
3.2.0
```
## 直連及使用連線池
### Jedis直連
引入Jedis之後,專案可以通過 new 的方式獲取 Jedis 使用。
- 首先在yml中配置好 redis 的地址和埠
```java
@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連線的網路開銷,速度非常快。並且通過合理的配置也能實現合理的管理連線,分配連線。
```java
@Test
public void testConnectionWithPool(){
// 建立連線池
JedisPool jedisPool = new JedisPool(host, port);
Jedis jedis = jedisPool.getResource();
// doSomething
// 歸還連線
jedis.close();
}
```
這裡雖然最後使用的 close() 方法,字面意思看起來好像是關閉連線,實際上點進去可以發現,如果dataSource(連線池)不為空,將執行歸還連線的方法
```java
@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(表示一直等待)
為了演示不歸還連線產生的錯誤,我寫了下面的程式碼
```java
@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就會報錯,錯誤資訊如下**:
```java
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() 方法的。
```java
@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 寫法來完成連線歸還。
```java
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-老錢](https://github.com/keatsCoder/redis-cli/tree/master/src/test/java/cn/keats/rediscli/laoqian)
## 連線池配置詳解
除了使用預設構造方法初始化連線池外,Jedis還提供了配置類來初始化
```java
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。雖然操作不是原子性的,但大多數情況下也能滿足要求:
```java
@Test
public void testPipeline() {
// 建立連線池
JedisPool jedisPool = new JedisPool(host, port);
try (Jedis jedis = jedisPool.getResource()){
Pipeline pipelined = jedis.pipelined();
// doSomething 獲取 keys
List keys = new ArrayList<>();
// pipelined 新增命令
for (String key : keys) {
pipelined.del(key);
}
// 執行命令
pipelined.sync();
}
}
```
## 專案程式碼
在學習Redis的過程中,我將部落格中的程式碼都在Github中上傳,以便小夥伴們核對。專案地址:https://github.com/keatsCoder/redis-cli
## 參考文獻
《Redis開發與運維》 --- 付磊 張益軍
《Redis深度歷險:核心原理和應用實踐》 ---