天天看點

Redis基礎學習 跟 Jedis 的手動實作

基礎導論

redis需求的産生

基本的應用服務一般如下圖:

Redis基礎學習 跟 Jedis 的手動實作

流程: 用戶端發送請求到伺服器端,伺服器端查詢資料庫然後做相應到業務處理,最終傳回給用戶端。

問題:一旦涉及到網際網路的高并發問題,比如秒殺的庫存扣減,APP的通路流量高峰等,每一次伺服器都要通過IO流去查詢資料庫,速度特别慢并且很容易把資料庫打崩,是以引入了緩存中間件,我們可以将資料存儲在記憶體中,通路資料時候直接在記憶體中讀取,記憶體讀取性能比IO讀取提高百倍。比較常用的緩存中間件有Redis 和 Memcached ,本次主要寫Redis,伺服器端擷取資料首先在redis中獲得,如獲得則直接将結果傳回,如沒獲得再從mysql中讀取資料傳回,并且會将mysql中資料緩存到Redis中。

Redis基礎學習 跟 Jedis 的手動實作

Redis簡介

Redis是一個開源的使用ANSI C語言編寫、支援網絡、可基于記憶體亦可持久化的日志型、Key-Value資料庫,并提供多種語言的API

Redis特性:速度快(QPS性能可達10W/s),鍵值對的資料結構伺服器,豐富的功能,簡單穩定,持久化,主從複制,高可用和分布式轉移, Redis 是單線程的,用戶端API支援語言較多,

Redis支援執行Lua腳本,可自動實作原子性操作。

Redis底層資料類型

Redis基礎學習 跟 Jedis 的手動實作
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

底層資料格式為跳躍表,範圍查找的天敵就是有序集合,跳躍表是有序集合的底層實作之一。跳躍表是基于多指針有序連結清單實作的,可以看成多個有序連結清單。

Redis基礎學習 跟 Jedis 的手動實作

在查找時,從上層指針開始查找,找到對應的區間之後再到下一層去查找。下圖示範了查找 22 的過程。

Redis基礎學習 跟 Jedis 的手動實作

與紅黑樹等平衡樹相比,跳躍表具有以下優點:

插入速度非常快速,因為不需要進行旋轉等操作來維護平衡性;

更容易實作;

支援無鎖操作。

Redis使用場景 :

  1. 緩存資料庫:
  2. 排行榜:
  3. 計數器應用:
  4. 社交網絡
  5. 淘寶購物車:
  6. 消息隊列:
  7. 其他場景等:自我聯想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>
           
  1. jedis測試
public static final void main(String[] args) {

        Jedis jedis = new Jedis("127.0.0.1", 6379);
        System.out.println(jedis.set("name", "sowhat"));
    }
           
  1. 看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基礎學習 跟 Jedis 的手動實作

參考

redis性能測試9W+/伺服器14W+

RESP通訊協定

繼續閱讀