基礎導論
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支援執行Lua腳本,可自動實作原子性操作。
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
// <[email protected]_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用戶端通訊了。
參考
redis性能測試9W+/伺服器14W+
RESP通訊協定