redis基礎學習跟Jedis的手動實現
文章目錄
基礎導論
redis需求的產生
基本的應用服務一般如下圖:
流程: 客戶端傳送請求到伺服器端,伺服器端查詢資料庫然後做相應到業務處理,最終返回給客戶端。
問題:一旦涉及到網際網路的高併發問題,比如秒殺的庫存扣減,APP的訪問流量高峰等,每一次伺服器都要通過IO流去查詢資料庫,速度特別慢並且很容易把資料庫打崩,所以引入了快取中介軟體,我們可以將資料儲存在記憶體中,訪問資料時候直接在記憶體中讀取,記憶體讀取效能比IO讀取提高百倍。比較常用的快取中介軟體有Redis 和 Memcached ,本次主要寫Redis,伺服器端獲取資料首先在redis中獲得,如獲得則直接將結果返回,如沒獲得再從mysql中讀取資料返回,並且會將mysql中資料快取到Redis中。
Redis簡介
Redis是一個開源的使用ANSI C語言編寫、支援網路、可基於記憶體亦可持久化的日誌型、Key-Value資料庫,並提供多種語言的API
Redis特性:速度快(QPS效能可達10W/s),鍵值對的資料結構伺服器,豐富的功能,簡單穩定,持久化,主從複製,高可用和分散式轉移, Redis 是單執行緒的,客戶端API支援語言較多,
Redis底層資料型別
1. String
底層資料格式 C語言中字串用char[],redis對其封裝了一成SDS(這個也是redis儲存的最小單元)。然後再SDS基礎上又封裝了一層 -> RedisObject,裡面可以指定物種資料型別,當我們 set name blog 時,redis其實會建立兩個RedisObject物件, 鍵的RedisObject 和 值的RedisOjbect 其中它們的type=REDIS_STRING。原始碼在sds.h
2. List
底層資料格式為雙向連結串列,可用來實現簡單的任務佇列等,原始碼是在adlist.c
3. Hash
底層資料格式涉及到hashtable, 在redis的這個層面,它永遠只有一個鍵,一個值,這個鍵永遠都是字串物件,而value 就是若干等k-v 屬性。底層還會涉及到rehash。
4. Set
底層資料格式底層跟Hash其實類似,我們可以認為Set 儲存到是Hash中到ke,value全部為空,跟Java中HashMap和HashSet 原理類似。
5.ZSet
底層資料格式為跳躍表,範圍查詢的天敵就是有序集合,跳躍表是有序集合的底層實現之一。跳躍表是基於多指標有序連結串列實現的,可以看成多個
在查詢時,從上層指標開始查詢,找到對應的區間之後再到下一層去查詢。下圖演示了查詢 22 的過程。
與紅黑樹等平衡樹相比,跳躍表具有以下優點:
插入速度非常快速,因為不需要進行旋轉等操作來維護平衡性;
更容易實現;
支援無鎖操作。
Redis使用場景 :
- 快取資料庫:
- 排行榜:
- 計數器應用:
- 社交網路
- 淘寶購物車:
- 訊息佇列:
- 其他場景等:自我聯想ing
Redis 指令
關於安裝跟配置百度即可,關於指令熟悉跟使用推薦查詢官方API
Jedis的手動實現
Java操作Redis使用的Jedis,它的 通訊協議採用的是RESP,它是基於TCP的應用層協議 RESP(REdis Serialization Protocol);RESP底層採用的是TCP的連線方式,通過tcp進行資料傳輸,然後根據解析規則解析相應資訊,該資料傳輸規則簡單明瞭,我們可以根據RESP協議以及Jedis底層原始碼實現自己到客戶端:
獲取Jedis傳送資料
倒入Jedis依賴
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.7.2</version>
</dependency>
- jedis測試
public static final void main(String[] args) {
Jedis jedis = new Jedis("127.0.0.1",6379);
System.out.println(jedis.set("name","sowhat"));
}
- 看jedis.set底層
public String set(final String key,String value) {
checkIsInMulti();
client.set(key,value);// 深入
return client.getStatusCodeReply();
}
public void set(final String key,final String value) {
set(SafeEncoder.encode(key),SafeEncoder.encode(value));
//對資料進行了編碼,再進入
}
public void set(final byte[] key,final byte[] value) {
sendCommand(Command.SET,key,value);//再進入
}
protected Connection sendCommand(final ProtocolCommand cmd,final byte[]... args) {
try {
connect();
// 劃重點 獲得一個連結 在進入
Protocol.sendCommand(outputStream,cmd,args);
//劃重點如何 傳送一個指令
pipelinedCommands++;
return this;
} catch (JedisConnectionException ex) {
// Any other exceptions related to connection?
broken = true;
throw ex;
}
}
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);
}
}
}
結論: 可以看到 傳輸資料對時候底層用的是Socket傳輸。這樣到話我們可以模擬一個redis伺服器端看jedis是如何加工資料的。
模擬服務端
package com.james.cache.socket;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class Server {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(6378);
// 程式碼阻塞等待客戶端連結
Socket socket = serverSocket.accept();
// 把訊息讀到byte陣列中
InputStream reader = socket.getInputStream();
byte[] request = new byte[1024];
reader.read(request);
//資料轉化為String 輸出
String req = new String(request);
System.out.println(req);
serverSocket.close();
}
}
jedis客戶端
package com.james.cache.socket;
import redis.clients.jedis.Jedis;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
public class Client {
public static void main(String[] args) throws IOException {
Jedis jedis = new Jedis("127.0.0.1",6378);
jedis.set("name","sowhat");
jedis.close();
}
}
*3
$3
SET
$4
name
$6
sowhat
可以看到伺服器端接收到的資料格式如上,這就是RESP的通訊資料格式
MyJedis 的手動實現
package com.james.cache.socket;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
public class MyJedis {
Socket socket;
InputStream reader;
OutputStream writer;
public MyJedis() throws Exception {
socket = new Socket("127.0.0.1",6379);
reader = socket.getInputStream();
writer = socket.getOutputStream();
}
public String set(String k,String v) throws Exception {
StringBuffer command = new StringBuffer();
command.append("*3").append("\r\n");
command.append("$3").append("\r\n");
command.append("SET").append("\r\n");
command.append("$").append(k.getBytes().length).append("\r\n");
command.append(k).append("\r\n");
command.append("$").append(v.getBytes().length).append("\r\n");
command.append(v).append("\r\n");
writer.write(command.toString().getBytes());
byte[] reponse = new byte[1024];
reader.read(reponse);
return new String(reponse);
}
public String get(String k) throws Exception {
StringBuffer command = new StringBuffer();
command.append("*2").append("\r\n");
command.append("$3").append("\r\n");
command.append("GET").append("\r\n");
command.append("$").append(k.getBytes().length).append("\r\n");
command.append(k).append("\r\n");
writer.write(command.toString().getBytes());
byte[] reponse = new byte[1024];
reader.read(reponse);
return new String(reponse);
}
}
testCode
@Test
public void testMyJedis() throws Exception
{
MyJedis myJedis = new MyJedis();
System.out.println(myJedis.set("name","sowhat1412"));
System.out.println( myJedis.get("name"));
}
結果: 可以發現我們自己的Jedis以及可以成功跟Redis客戶端通訊了。