摘要:前言是應(yīng)用訪問服務(wù)的首選客戶端,本文通過分析客戶端源代碼,扒一扒客戶端設(shè)計與實現(xiàn)的常用套路連接要訪問服務(wù),首先需要與服務(wù)建立連接,因此客戶端庫首先需要對連接進行抽象和封裝,使用類來封裝與服務(wù)器的一個連接通常類還會對的讀寫進行一層簡單的封裝,
前言
Jedis 是 java 應(yīng)用訪問 Redis 服務(wù)的首選客戶端,本文通過分析 jedis 客戶端源代碼,扒一扒客戶端設(shè)計與實現(xiàn)的常用套路
連接(Connection)要訪問(Redis)服務(wù),首先需要與服務(wù)建立連接,因此客戶端庫首先需要對連接進行抽象和封裝,Jedis 使用 Connection 類來封裝與服務(wù)器的一個 socket 連接:
public class Connection implements Closable { private Socket socket; private connectionTimeout = Protocol.DEFAULT_TIMEOUT; private int soTimeout = Protocol.DEFAULT_TIMEOUT; ... public Connection() {} public Connection(final String host) { this.host = host; } public Connection(final String host, final int port) { this.host = host; this.port = port; } }
通常 Connection 類還會對 socket 的讀寫進行一層簡單的封裝,對于 Redis 客戶端就是發(fā)送命令以及獲取結(jié)果,這里的 Command 類是一個命令的枚舉類型,args 是可變長參數(shù),表示命令參數(shù)
protected Connection sendCommand(final Command cmd, final byte[]... args) { // 見下文 }
由于網(wǎng)絡(luò)通信的不穩(wěn)定,客戶端與服務(wù)器的通信通常都需要 捕獲 各種異常并進行恢復(fù)(重試,重連)
// sendCommand 實現(xiàn) try { connect(); Protocol.sendCommand(outputStream, cmd, args); pipelinedCommands++; return this; } catch (JedisConnectionException ex) { try { String errorMessage = Protocol.readErrorLineIfPossible(inputStream); if (errorMessage != null && errorMessage.length() > 0) { ex = new JedisConnectionException(errorMessage, ex.getCause()); } } catch (Exception e) { } // Any other exceptions related to connection? broken = true; throw ex; }
connect 方法建立連接,如果已經(jīng)建立連接當然就不需要,這里又涉及到 socket 的一些常用參數(shù)
reuse address
keep alive
tcp no delay
so linger
so timeout
public void connect() { if (!isConnected()) { try { socket = new Socket(); // ->@wjw_add socket.setReuseAddress(true); socket.setKeepAlive(true); // Will monitor the TCP connection is // valid socket.setTcpNoDelay(true); // Socket buffer Whetherclosed, to // ensure timely delivery of data socket.setSoLinger(true, 0); // Control calls close () method, // the underlying socket is closed // immediately // <-@wjw_add socket.connect(new InetSocketAddress(host, port), connectionTimeout); socket.setSoTimeout(soTimeout); outputStream = new RedisOutputStream(socket.getOutputStream()); inputStream = new RedisInputStream(socket.getInputStream()); } catch (IOException ex) { broken = true; throw new JedisConnectionException(ex); } } }輸入輸出流(Input, output stream)
通常客戶端都會自定義輸入輸出流來封裝協(xié)議格式以及對傳輸內(nèi)容進行緩存(預(yù)讀)來提高效率,Jedis 使用 RedisInputStream 和 RedisOutputStream 類類封裝輸入輸出流
RedisInputStreamRedisInputStream 類中定義了一個 byte 類型的字節(jié)數(shù)組 buf 來保存從 socket inputstream 讀到的數(shù)據(jù),limit 字段表示實際讀到的數(shù)據(jù)大小,count 字段表示當前已經(jīng)讀取的數(shù)據(jù)(偏移量),
public class RedisInputStream extends FilterInputStream { protected final byte[] buf; protected int count, limit; public RedisInputStream(InputStream in, int size) { super(int); if (size <= 0) { throw new IllegalArgumentException("Buffer size <= 0"); } buf = new byte[size]; } }
我們來看看 readLine 方法,它用于從 socket input stream 中讀取一行
public String readLine() { final StringBuilder sb = new StringBuilder(); while (true) { // 預(yù)讀 ensureFill(); byte b = buf[count++]; if (b == " ") { // 按照協(xié)議 必定連續(xù)出現(xiàn),所以讀到 后再預(yù)讀一次確保數(shù)據(jù)完整性 ensureFill(); // Must be one more byte byte c = buf[count++]; if (c == " ") { break; } sb.append((char) b); sb.append((char) c); } else { sb.append((char) b); } } final String reply = sb.toString(); if (reply.length() == 0) { throw new JedisConnectionException("It seems like server has closed the connection."); } return reply; }RedisOutputStream 協(xié)議(Protocol)
Socket 連接在客戶端和服務(wù)器之間建立了一個通信通道,協(xié)議(Protocol)規(guī)定了數(shù)據(jù)傳輸格式,對于 Redis 這種使用 socket 長連接的服務(wù),一般都會自定義 協(xié)議,所以接下來要對 協(xié)議 進行抽象和封裝.
通常有兩種做法:
使用面向?qū)ο蟮姆治雠c設(shè)計,將每個協(xié)議單元抽象成一個類
將所有與協(xié)議相關(guān)的字段和方法封裝到一個工具類里頭
Jedis 使用了后者,可能是因為 Redis 命令本身比較簡單,沒必要過度設(shè)計.
Protocol 類包含了 Jedis 和 Redis 服務(wù)通信協(xié)議相關(guān)的代碼,比如上文提到的 Protocol.sendCommand 方法
private static void sendCommand(final RedisOutputStream os, final byte[] command, final byte[]... args) { try { os.write(ASTERISK_BYTE); os.writeIntCrLf(args.length + 1); os.write(DOLLAR_BYTE); os.writeIntCrLf(command.length); os.write(command); os.writeCrLf(); for (final byte[] arg : args) { os.write(DOLLAR_BYTE); os.writeIntCrLf(arg.length); os.write(arg); os.writeCrLf(); } } catch (IOException e) { throw new JedisConnectionException(e); } }
從 sendCommand 方法可以看出 Redis 發(fā)送命令協(xié)議,完整的協(xié)議可以參考 Redis 官網(wǎng)文檔,或者使用 tcpdump 這樣的抓包工具來觀察 Redis 協(xié)議
Facade 設(shè)計模式使用 Connection, Command, Protocol 等類就可以直接和 Redis 服務(wù)通信,但是客戶端庫通常會再做一層封裝供調(diào)用者使用,類似設(shè)計模式中的 Facade 模式,這也就是為什么在很多客戶端中會有各種各樣的 XXXClient, XXXManager 等
在 Jedis 中這個 Facade(門面)就是 Jedis 和 Client
Jedis 類層次結(jié)構(gòu):
BinaryJedis ---> Client Jedis
BinaryJedis 使用 Client 類和 Redis 服務(wù)通信
Client 類層次結(jié)構(gòu):
Connection BinaryClient Client
以 Redis set 命令的實現(xiàn)為例:
public String set(final byte[] key, final byte[] value) { checkIsInMultiOrPipeline(); // 發(fā)送命令 client.set(key, value); // 讀取響應(yīng) return client.getStatusCodeReply(); }性能優(yōu)化 連接池
如果只有一條路,車比較多的時候就會造成阻塞,因此直觀的方案是多修幾條(道)路,所以客戶端和服務(wù)器之間的連接普遍都會用到 連接池,除了上述效率的考慮外,使用連接池還可以增加容錯能力,比如一個連接掛了,系統(tǒng)可以從連接池中選取其它的連接進行服務(wù)
Jedis 通過 JedisPool 來抽象和封裝 連接池,向用戶隱藏了實現(xiàn)細節(jié):實例化 JedisPool 的一個實例并調(diào)用 getResource 方法就可以獲取一個 Jedis 實例,使用完成后調(diào)用 Jedis.close 方法,就這么簡單
public JedisPool(String host, int port) { ... } @Override public Jedis getResource() { Jedis jedis = super.getResource(); jedis.setDataSource(this); return jedis; }總結(jié)
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/66845.html
摘要:設(shè)計模式無論是對于最底層的的編碼實現(xiàn)還是較高層的架構(gòu)設(shè)計都有著重要的指導(dǎo)作用。所謂光說不練假把式,今天我就把項目中常見的應(yīng)用場景涉及到的主要設(shè)計模式及其相關(guān)設(shè)計模式總結(jié)一下,用實例分析和對比的方式在一片文章中就把最常見的種設(shè)計模式梳理清楚。 設(shè)計模式無論是對于最底層的的編碼實現(xiàn)還是較高層的架構(gòu)設(shè)計都有著重要的指導(dǎo)作用。所謂光說不練假把式,今天我就把項目中常見的應(yīng)用場景涉及到的主要設(shè)計模...
閱讀 2330·2021-09-30 09:47
閱讀 2949·2019-08-30 11:05
閱讀 2526·2019-08-29 17:20
閱讀 1912·2019-08-29 13:01
閱讀 1721·2019-08-26 13:39
閱讀 1221·2019-08-26 13:26
閱讀 3205·2019-08-23 18:40
閱讀 1810·2019-08-23 17:09