1. Redis通信協(xié)議
??????Jedis Client是Redis官網(wǎng)推薦的一個(gè)面向java客戶端,庫(kù)文件實(shí)現(xiàn)了對(duì)redis各類API進(jìn)行封裝調(diào)用。redis通信協(xié)議是Redis客戶端與Redis Server之間交流的語(yǔ)言,它規(guī)定了請(qǐng)求和返回值的格式。redis-cli與server端使用一種專門(mén)為redis設(shè)計(jì)的協(xié)議RESP(Redis Serialization Protocol)交互,Resp本身沒(méi)有指定TCP,但redis上下文只使用TCP連接。
??????RESP規(guī)定:
- 用 \r\n 做間隔
- 對(duì)于簡(jiǎn)單的字符串,以+開(kāi)頭
set hello world
+OK\r\n
- 對(duì)于錯(cuò)誤消息,以-開(kāi)頭,例如:
sethx // 該命令不存在
-ERR unknown command 'sethx'
- 對(duì)于整數(shù),以:開(kāi)頭,例如:
dbsize
:100\r\n
- 對(duì)于大字符串,以$開(kāi)頭,接著跟上字符串長(zhǎng)度的數(shù)字:最長(zhǎng)為512MB。例如:
get name
$6\r\nfoobar\r\n 代表一個(gè)長(zhǎng)6的字符串, foobar
$0\r\n\r\n 長(zhǎng)度為0 的空字符串
$-1\r\n Null
- 對(duì)于數(shù)組,以*開(kāi)頭,接上數(shù)組元素的個(gè)數(shù)。
*0\r\n 一個(gè)空的數(shù)組
mset name1 foo name2 bar
mget name1 name2
*2\r\n$3\r\nfoo\r\n$3\r\nbar\r\n 一個(gè)有兩個(gè)元素的數(shù)組 foo bar
- 我們知道redis-cli只能看到最終的執(zhí)行結(jié)果,因?yàn)閞edis-cli本身就是按照RESP進(jìn)行結(jié)果解析的,所以看不到中間結(jié)果。
- 通過(guò)RESP,執(zhí)行一個(gè)命令,客戶端與服務(wù)端的交互步驟如下:
輸入命令->將命令編碼成字節(jié)流->通過(guò)TCP發(fā)送到服務(wù)端->服務(wù)端解析字節(jié)流->服務(wù)端執(zhí)行命令->
->將結(jié)果編碼成字節(jié)流->通過(guò)TCP鏈接發(fā)送給客戶端->解析字節(jié)流->得到執(zhí)行結(jié)果
比如執(zhí)行set hello world,根據(jù)resp協(xié)議,需要客戶端解析為下面格式字節(jié)流發(fā)送給服務(wù)端
*3\r\n
$3\r\nset\r\n
$5\r\nhello\r\n
$5\r\nworld\r\n
2. jedis通信原理
??????試想,如果讓我們自己根據(jù)上面提到的協(xié)議用java去實(shí)現(xiàn)一個(gè)客戶端與redis服務(wù)端實(shí)現(xiàn)通信,該怎么做呢?
public class TSocketClient {
// 定義socket
private Socket socket;
public TSocketClient() {
try {
socket = new Socket("192.168.58.99", 6379);
} catch (IOException e) {
e.printStackTrace();
}
}
public String set(final String key, final String value) throws IOException {
StringBuilder sb = new StringBuilder();
sb.append("*3").append("\r\n");
sb.append("$3").append("\r\n");
sb.append("set").append("\r\n");
sb.append("$").append(key.getBytes().length).append("\r\n");
sb.append(key).append("\r\n");
sb.append("$").append(value.getBytes().length).append("\r\n");
sb.append(value).append("\r\n");
socket.getOutputStream().write(sb.toString().getBytes());
byte[] b = new byte[2048];
socket.getInputStream().read(b);
return new String(b);
}
public String get(final String key) throws IOException {
StringBuilder sb = new StringBuilder();
sb.append("*2").append("\r\n"); // *表示數(shù)組 后面數(shù)字表示數(shù)組長(zhǎng)度
sb.append("$3").append("\r\n");
sb.append("get").append("\r\n");
sb.append("$").append(key.getBytes().length).append("\r\n"); // 美元符號(hào)表示字符串,后面的數(shù)字表示長(zhǎng)度
sb.append(key).append("\r\n");
socket.getOutputStream().write(sb.toString().getBytes());
byte[] b = new byte[2048];
socket.getInputStream().read(b);
return new String(b);
}
public static void main(String[] args) throws IOException {
TSocketClient client = new TSocketClient();
client.set("hello", "ziyan");
}
}
???? 上面代碼通過(guò)實(shí)現(xiàn)resp協(xié)議實(shí)現(xiàn)了與redis服務(wù)端的通信,其實(shí)jedis客戶端本質(zhì)上也是通過(guò)建立socket按照resp協(xié)議與redis通信,下面來(lái)分析jedis具體的代碼:
???第一部分:jedis對(duì)象的創(chuàng)建:Jedis jedis = new Jedis(); 主要是創(chuàng)建連接Redis服務(wù)器的客戶端,在Jedis基類BinaryJedis中主要有Connection對(duì)象,創(chuàng)建jedis對(duì)象的時(shí)候尚未連接到redis服務(wù)器,在Connection類中,主要設(shè)置了鏈接Redis所使用socket的參數(shù)以及操作socket所使用的工具。
//創(chuàng)建Redis客戶端
Jedis jedis = new Jedis();
//調(diào)用set 命令,返回狀態(tài)標(biāo)記
String code=jedis.set("s", "s");
System.out.println("code="+code);
//調(diào)用get命令
String s =jedis.get("s");
System.out.println("s="+s);
//Jedis客戶端鏈接,使用原始socket進(jìn)行鏈接
public class Connection implements Closeable
{
private static final byte[][] EMPTY_ARGS = new byte[0][];
//默認(rèn)主機(jī)
private String host = Protocol.DEFAULT_HOST;
//默認(rèn)端口
private int port = Protocol.DEFAULT_PORT;
//原始socket
private Socket socket;
//輸入輸出流
private RedisOutputStream outputStream;
private RedisInputStream inputStream;
private int connectionTimeout = Protocol.DEFAULT_TIMEOUT;
private int soTimeout = Protocol.DEFAULT_TIMEOUT;
private boolean broken = false;
public Connection() {
}
}
???從Connection的成員變量中可以看出,jedis使用了jdk的io socket來(lái)處理網(wǎng)絡(luò)通信。
??第二部分:在調(diào)用 String code=jedis.set("s", "s"); 命令的時(shí)候,才是真正創(chuàng)建鏈接的過(guò)程。Client(BinaryClient).set(byte[], byte[]) 方法參數(shù)就是把由String 字符串轉(zhuǎn)換成字節(jié)數(shù)值,并調(diào)用Connection的sendCommand方法來(lái)發(fā)送Redis命令。
//每次發(fā)送命令前都判斷是否鏈接,如果鏈接端口并且鏈接不上,則拋出異常
protected Connection sendCommand(final ProtocolCommand cmd, final byte[]... args) {
try {
connect();//每次發(fā)送Redis命令都會(huì)調(diào)用Connect()方法來(lái)鏈接Redis遠(yuǎn)程服務(wù)器
Protocol.sendCommand(outputStream, cmd, args); //操作socket 的輸出流來(lái)發(fā)送命令
return this;
} catch (JedisConnectionException ex) {
/*
* When client send request which formed by invalid protocol, Redis send back error message
* before close connection. We try to read it to provide reason of failure.
*/
try {
String errorMessage = Protocol.readErrorLineIfPossible(inputStream);
if (errorMessage != null && errorMessage.length() > 0) {
ex = new JedisConnectionException(errorMessage, ex.getCause());
}
} catch (Exception e) {
/*
* Catch any IOException or JedisConnectionException occurred from InputStream#read and just
* ignore. This approach is safe because reading error message is optional and connection
* will eventually be closed.
*/
}
// Any other exceptions related to connection?
broken = true;
throw ex;
}
}
每次調(diào)用sendCommand發(fā)送命令時(shí)候,都會(huì)調(diào)用Connnect()方法嘗試鏈接遠(yuǎn)程端口。
//在發(fā)送命令之前連接redis服務(wù)器
public void connect() {
if (!isConnected()) {
try {
//創(chuàng)建新socket
socket = new Socket();
//設(shè)置socket參數(shù)
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
//設(shè)置鏈接超時(shí)時(shí)間
socket.connect(new InetSocketAddress(host, port), connectionTimeout);
//設(shè)置讀取超時(shí)時(shí)間
socket.setSoTimeout(soTimeout);
//獲取socket原始輸入輸出流
outputStream = new RedisOutputStream(socket.getOutputStream());
inputStream = new RedisInputStream(socket.getInputStream());
} catch (IOException ex) {
broken = true;
throw new JedisConnectionException(ex);
}
}
}
每次鏈接到遠(yuǎn)程Redis服務(wù)器后,第一個(gè)命令就是發(fā)送密鑰命令,這是一個(gè)BinaryClient的重寫(xiě)方法,方法里還是調(diào)用父類Connection的connect方法。
@Override
public void connect() {
if (!isConnected()) {
super.connect();
if (password != null) {
auth(password);
getStatusCodeReply();
}
if (db > 0) {
select(Long.valueOf(db).intValue());
getStatusCodeReply();
}
}
}
在每次發(fā)送一個(gè)命令后,都會(huì)去獲取返回碼。
public String set(final String key, String value) {
checkIsInMultiOrPipeline();
client.set(key, value);
return client.getStatusCodeReply();
}
在取狀態(tài)碼時(shí),每次都去刷新通道。讀取數(shù)據(jù)流最終通過(guò)SocketInputStream 類來(lái)讀取。
public String getStatusCodeReply() {
flush();
pipelinedCommands--;
final byte[] resp = (byte[]) readProtocolWithCheckingBroken();
if (null == resp) {
return null;
} else {
return SafeEncoder.encode(resp);
}
}
3. jedis的使用
???? jedis的使用方法很簡(jiǎn)單:
// 創(chuàng)建Redis客戶端
Jedis jedis = new Jedis("192.168.58.99", 6379);
// 調(diào)用set 命令,返回狀態(tài)標(biāo)記
String code=jedis.set("s", "xxx");
System.out.println("code="+code);
// 調(diào)用get命令
String s =jedis.get("s");
System.out.println("s="+s);
????上面代碼中只用到了包含ip和端口兩個(gè)參數(shù)的構(gòu)造函數(shù),更常用的是包含四個(gè)參數(shù)的構(gòu)造函數(shù):
public Jedis(final String host, final int port, final int connectionTimeout, final int soTimeout)
// connectionTimeout 表示客戶端連接超時(shí)
// soTimeout 表示客戶端讀寫(xiě)超時(shí)
3.1 為什么要用jedis連接池?
?????雖然基于內(nèi)存的Redis數(shù)據(jù)庫(kù)有著超高的性能,但是底層的網(wǎng)絡(luò)通信卻占用了一次數(shù)據(jù)請(qǐng)求的大量時(shí)間,因?yàn)槊看螖?shù)據(jù)交互都需要先建立連接,假設(shè)一次數(shù)據(jù)交互總共用時(shí)30ms,超高性能的Redis數(shù)據(jù)庫(kù)處理數(shù)據(jù)所花的時(shí)間可能不到1ms,也即是說(shuō)前期的連接占用了29ms,上面介紹的jedis直連方式,也就是每次new jedis都會(huì)新建tcp連接,使用后再斷開(kāi)連接,這對(duì)于頻繁訪問(wèn)redis的場(chǎng)景顯然不是高效的使用方式。連接池則可以實(shí)現(xiàn)在客戶端建立多個(gè)鏈接并且不釋放,當(dāng)需要使用連接的時(shí)候通過(guò)一定的算法獲取已經(jīng)建立的連接,使用完了以后則還給連接池,這就免去了數(shù)據(jù)庫(kù)連接所占用的時(shí)間。因此,通常會(huì)使用連接池的方式對(duì)Jedis連接進(jìn)行管理,所有jedis對(duì)象會(huì)預(yù)先放在池子中(JedisPool),每次要連接redis,只需要在池子中借,用完了再歸還給池子。
??????客戶端連接Redis使用的是TCP協(xié)議,直連的方式每次需要建立TCP連接,而連接池的方式是可以預(yù)先初始化好jedis連接,每次只需要從jedis連接池借用即可,借用和歸還操作是在本地進(jìn)行的,只有少量的并發(fā)同步開(kāi)銷(xiāo),遠(yuǎn)遠(yuǎn)小于新建tcp連接的開(kāi)銷(xiāo)。此外,連接池的方式可以有效保護(hù)和控制資源的使用,而直連的方式無(wú)法限制jedis對(duì)象的個(gè)數(shù),并且可能存在連接泄漏的情況。
??????Jedis提供了JedisPool這個(gè)類作為Jedis的連接池,同時(shí)使用Apache的通用對(duì)象池工具common-pool作為資源的管理工具。JedisPoolConfig繼承了GenericObjectPoolConfig,提供了很多參數(shù)配置:
- maxActive: 控制一個(gè)pool可分配多少個(gè)jedis實(shí)例,如果pool已經(jīng)分配了maxActive個(gè)jedis實(shí)例,此時(shí)pool的狀態(tài)為exhausted。
- maxIdle: 控制一個(gè)pool最多有多少個(gè)狀態(tài)為idle的jedis實(shí)例。(如果超出了這個(gè)閾值,會(huì)close掉超出的連接)
- whenExhaustedAction: 表示當(dāng)pool中的jedis實(shí)例被allocated完時(shí),pool要采取的操作:默認(rèn)有三種:
??????WHEN_EXHAUSTED_FAIL : 表示無(wú)jedis實(shí)例時(shí),直接拋出NoSuchElementException,
??????WHEN_EXHAUSTED_BLOCK 表示阻塞住,達(dá)到maxWait時(shí)拋出,JedisConnectionException
??????WHEN_EXHAUSTED_GROW 表示新建一個(gè)jedis實(shí)例,設(shè)置的maxActive無(wú)用。 - maxwait: 表示borrow一個(gè)jedis實(shí)例時(shí),最大的等待時(shí)間,如果超出等待時(shí)間,直接拋出jedisConnectionException。
- testOnBorrow:在borrow一個(gè)jedis實(shí)例時(shí),是否提前進(jìn)行validate操作;如果為true,則使用redisPING PONG命令測(cè)試redis連接是否有效,保證得到的jedis實(shí)例均是可用的;
- testOnReturn:在return給pool時(shí),是否提前進(jìn)行validate操作;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisCommands;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
public class TestJedis {
public static final Logger logger = LoggerFactory.getLogger(TestJedis.class);
// Jedispool
JedisCommands jedisCommands;
JedisPool jedisPool;
// common-pool 連接池配置,
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
String ip = "192.168.58.99";
int port = 6379;
int timeout = 2000;
public TestJedis() {
// 初始化jedis
// 設(shè)置配置
jedisPoolConfig.setMaxTotal(1024);
jedisPoolConfig.setMaxIdle(100);
jedisPoolConfig.setMaxWaitMillis(100);
jedisPoolConfig.setTestOnBorrow(false);
jedisPoolConfig.setTestOnReturn(true);
// 初始化JedisPool
jedisPool = new JedisPool(jedisPoolConfig, ip, port, timeout);
//
Jedis jedis = jedisPool.getResource();
jedisCommands = jedis;
}
public void setValue(String key, String value) {
this.jedisCommands.set(key, value);
}
public String getValue(String key) {
return this.jedisCommands.get(key);
}
public static void main(String[] args) {
TestJedis testJedis = new TestJedis();
testJedis.setValue("testJedisKey", "testJedisValue");
logger.info("get value from redis:{}",testJedis.getValue("testJedisKey"));
}
}
對(duì)于jedis對(duì)象池的原理可參考我的上一篇文章jedis對(duì)象池
3.2 jedis pipeline
?????我們知道redis提供了mget、mset方法,但沒(méi)有提供mdel方法,如果要實(shí)現(xiàn)這個(gè)功能,可以借助Pipeline來(lái)模擬批量刪除,Jedis支持Pipeline特性,可以通過(guò)jedis實(shí)現(xiàn)。
public void mdel(List<String> keys) {
Jedis jedis = new Jedis("192.168.58.99");
Pipeline pipeline = jedis.pipelined();
for (String key : keys) {
pipeline.del(key); // 此時(shí)命令并非真正執(zhí)行
}
// 真正執(zhí)行命令
pipeline.sync(); // 除了pipline.sync(),還可以使用pipeline.syncAndReturnAll()將pipeline的命令進(jìn)行返回。
}