鏈接池對外提供接口:java
並暴露客戶端可配置的參數:redis
在內部則實現功能:數據庫
鏈接創建安全
鏈接心跳保持bash
鏈接管理markdown
空閒鏈接回收網絡
鏈接可用性檢測多線程
鏈接池結構示意圖併發
業務中常常也會用到各類鏈接池:性能
在使用三方客戶端進行網絡通訊時,首要肯定客戶端SDK是不是基於鏈接池技術實現的。
若客戶端SDK沒有使用鏈接池,而直接是TCP鏈接,就須要考慮每次創建TCP鏈接的開銷,由於TCP基於字節流,若在多線程下對同一鏈接操做,就有線程安全隱患。
有一個XXXPool類負責鏈接池實現:
XXXPool必須是線程安全的,可併發獲取和歸還鏈接,而XXXConnection是非線程安全的。 對應到鏈接池結構示意圖,XXXPool就是右邊鏈接池那個框,左邊客戶端是咱們本身的代碼。
對外提供一個XXXClient類,經過該類可直接請求服務端。該類內部維護了鏈接池,SDK使用者無需考慮鏈接的獲取和歸還問題。 XXXClient是線程安全的。對應到鏈接池結構示意圖中,整個API就是藍框。
通常命名爲XXXConnection,以區分其是基於鏈接池or單鏈接,而不建議命名爲XXXClient。直接鏈接方式的API基於單一鏈接,每次使用都須要建立和斷開鏈接,性能通常,且一般不是線程安全的。對應到鏈接池的結構示意圖中,這種形式至關於沒有右邊鏈接池那個框,客戶端直接鏈接服務端建立鏈接。
不排除有一些客戶端特立獨行,所以在使用三方SDK時,必定要
鏈接池自己通常是線程安全的,可複用。每次使用須要從鏈接池獲取鏈接,使用後歸還,歸還的工做由使用者負責。
大多數中間件、數據庫的客戶端SDK都會支持鏈接池,SDK負責鏈接的獲取和歸還,使用的時候直接複用客戶端。
那一般不是線程安全的,並且短鏈接的方式性能不會很高,使用的時候須要考慮是否本身封裝一個鏈接池。
下面看Jedis類到底屬於哪一種類型的API,直接在多線程環境下複用一個鏈接會產生什麼問題,以及如何用最佳實踐來修復這個問題。
首先,向Redis初始化2組數據,Key=a、Value=1,Key=b、Value=2:
@PostConstruct
public void init() {
try (Jedis jedis = new Jedis("127.0.0.1", 6379)) {
Assert.isTrue("OK".equals(jedis.set("a", "1")), "set a = 1 return OK");
Assert.isTrue("OK".equals(jedis.set("b", "2")), "set b = 2 return OK");
}
}
複製代碼
而後,啓動兩個線程,共享操做同一個Jedis實例,每個線程循環1000次,分別讀取Key爲a和b的Value,判斷是否分別爲1和2:
Jedis jedis = new Jedis("127.0.0.1", 6379);
new Thread(() -> {
for (int i = 0; i < 1000; i++) {
String result = jedis.get("a");
if (!result.equals("1")) {
log.warn("Expect a to be 1 but found {}", result);
return;
}
}
}).start();
new Thread(() -> {
for (int i = 0; i < 1000; i++) {
String result = jedis.get("b");
if (!result.equals("2")) {
log.warn("Expect b to be 2 but found {}", result);
return;
}
}
}).start();
TimeUnit.SECONDS.sleep(5);
複製代碼
執行程序屢次,能夠看到日誌中出現了各類奇怪的異常信息,有的是讀取Key爲b的Value讀取到了1,有的是流非正常結束,還有的是鏈接關閉異常:
//錯誤1
[14:56:19.069] [Thread-28] [WARN ] [.t.c.c.redis.JedisMisreuseController:45 ] - Expect b to be 2 but found 1
//錯誤2
redis.clients.jedis.exceptions.JedisConnectionException: Unexpected end of stream.
at redis.clients.jedis.util.RedisInputStream.ensureFill(RedisInputStream.java:202)
at redis.clients.jedis.util.RedisInputStream.readLine(RedisInputStream.java:50)
at redis.clients.jedis.Protocol.processError(Protocol.java:114)
at redis.clients.jedis.Protocol.process(Protocol.java:166)
at redis.clients.jedis.Protocol.read(Protocol.java:220)
at redis.clients.jedis.Connection.readProtocolWithCheckingBroken(Connection.java:318)
at redis.clients.jedis.Connection.getBinaryBulkReply(Connection.java:255)
at redis.clients.jedis.Connection.getBulkReply(Connection.java:245)
at redis.clients.jedis.Jedis.get(Jedis.java:181)
at org.geekbang.time.commonmistakes.connectionpool.redis.JedisMisreuseController.lambda$wrong$1(JedisMisreuseController.java:43)
at java.lang.Thread.run(Thread.java:748)
//錯誤3
java.io.IOException: Socket Closed
at java.net.AbstractPlainSocketImpl.getOutputStream(AbstractPlainSocketImpl.java:440)
at java.net.Socket$3.run(Socket.java:954)
at java.net.Socket$3.run(Socket.java:952)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.Socket.getOutputStream(Socket.java:951)
at redis.clients.jedis.Connection.connect(Connection.java:200)
... 7 more
複製代碼
讓咱們分析一下Jedis類的源碼,搞清楚其中原因吧。
BinaryClient封裝了各類Redis命令 都是調用其父類Connection方法,使用Protocol類發送命令 Protocol類的sendCommand方法 發送命令時是直接操做RedisOutputStream寫字節。
在多線程環境下複用Jedis對象,其實就是在複用RedisOutputStream。若是多個線程在執行操做,那麼既沒法確保整條命令以一個原子操做寫入Socket,也沒法確保寫入後、讀取前沒有其餘數據寫到遠端。 這就能解釋了爲什麼多線程下使用Jedis對象操做Redis會出現各類問題:
那麼如何修復呢? 使用Jedis提供的線程安全的類JedisPool來得到Jedis的實例。JedisPool做爲鏈接池,能夠聲明爲static 被多線程共享。注意使用try-with-resources模式。 這樣使用後代碼再也不有線程安全問題。最好再經過shutdownhook,在程序退出以前關閉JedisPool:
@PostConstruct
public void init() {
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
jedisPool.close();
}));
}
複製代碼
若Jedis是從鏈接池獲取的話,則close方法會調用鏈接池的return方法歸還鏈接:
若是不是,則直接關閉鏈接,其最終調用Connection類的disconnect方法來關閉TCP鏈接: 可見Jedis可獨立使用,也可配合鏈接池(JedisPool)
因此JedisPool的鏈接池其實就是直接複用的GenericObjectPool,並無本身實現一套池子。
綜上,Jedis API屬於鏈接池和鏈接分離的API,JedisPool是線程安全的鏈接池,Jedis是非線程安全的單一鏈接。