用Go實現Redis之三get/set命令實現

寫在前面

本篇Godis版本號:v0.0.2git

前一篇文章實現了客戶端/服務端的交互。這一篇,主要介紹get/set命令的實現。
命令自己比較簡單,支撐命令的整個系統基礎比較麻煩。本文會介紹get/set操做涉及的組件和模塊,並適當簡化,最後實現功能。github

Redis用C語言寫成,C語言自身不支持複雜數據結構,因此Redis中的string、list、set等結構,均是Redis自身實現;而Go版本的Godis,會盡可能使用原生數據結構。數據庫


原理簡介

set命令和get命令是Redis中使用頻率最高的命令,以set爲例,命令「set key value」,將鍵值對存儲到Redis服務端,能夠簡化爲「操做一個遠程關聯數組」。
固然,相比關聯數組,Redis多了以下特性:segmentfault

  1. 多DB,支持數據庫切換;
  2. 高可用之數據持久化;
  3. 高可用之主從複製;
  4. 安全、事務、發佈訂閱等。

本文重點實現數據在內存中的存儲及查詢,交互協議和持久化會在後續短文實現。數組

執行流程

流程拆解

從服務端初始化、客戶端輸入「set alpha 123」命令,到接收到返回結果,經歷以下步驟:安全

  1. 實例化server及相關資源,準備鏈接;
  2. 客戶端與服務端創建鏈接,服務端初始化一個client結構體,用來保存當前鏈接;
  3. 將客戶端請求的「set alpha 123」字符串,分拆爲「set」、「alpha」、「123」三部分;
  4. 查找是否支持set命令,並確定參數合法,調set用命令的實現函數SetCommand,更新db數據;
  5. 執行結果響應給客戶端;

執行流程大體以下:
圖片描述數據結構


接下來分開說明主要步驟。函數

1.數據交互

前篇實現的客戶端/服務端交互使用的協議是textproto,沒有使用Redis自身的統一協議。這一篇,客戶端對服務端執行的get、set命令,均以原生文本方式發送給服務端執行。在讀者閱讀實現代碼時,也能夠看到最新release版本,與v0.0.1有一處diff是在godis-cli.go文件:測試

//清除掉回車換行符
//text = strings.Replace(text, "\n", "", -1)

該行被暫時註釋掉,也就是在v0.0.2版本,使用「n」做爲文本協議分隔符,肯定命令的結尾。ui

如客戶端發送"set alpha 123",服務端接收到的就是以下字節數據:

clipboard.png

分別對應ASCII碼爲:

clipboard.png

2.服務端準備

第一篇(https://segmentfault.com/a/11...)提到過服務端須要一個server結構體存儲相關信息,在服務端準備好處理請求前,對該結構進行實例化並進行一系列初始化操做:初始化基本配置、分配多db資源、加載磁盤持久化數據、信號監聽處理等。
初始化server的代碼主要是一些賦值操做和相應結構體初始化:

// 初始化服務端實例
func initServer() {
    godis.Pid = os.Getpid()
    godis.DbNum = 16
    initDb()
    godis.Start = time.Now().UnixNano() / 1000000
    //var getf server.CmdFun

    getCommand := &core.GodisCommand{Name: "get", Proc: core.GetCommand}
    setCommand := &core.GodisCommand{Name: "set", Proc: core.SetCommand}

    godis.Commands = map[string]*core.GodisCommand{
        "get": getCommand,
        "set": setCommand,
    }
}

// 初始化db
func initDb() {
    godis.Db = make([]*core.GodisDb, godis.DbNum)
    for i := 0; i < godis.DbNum; i++ {
        godis.Db[i] = new(core.GodisDb)
        godis.Db[i].Dict = make(map[string]*core.GodisObject, 100)
    }
}

這裏簡單解釋下core.GodisCommand結構。該結構很簡單,記錄了命令的名字、函數指針和參數校驗相關的信息。在執行命令前,校驗命令是否存在的過程,須要查找支持的」命令表「。該命令表就是commands,commands由一組core.GodisCommand構成。commands中查不到的命令,則爲不支持的命令;而命令參數須要知足哪些條件,由core.GodisCommand結構的其餘字段記錄。

3.服務端接收

當服務端準備就緒,開始接受請求。
請求到來,server會實例化一個client結構體,保存當前鏈接。該client結構體也在前篇有介紹,主要用來存儲當前鏈接的db等信息。

// CreateClient 鏈接創建 建立client記錄當前鏈接
func (s *Server) CreateClient(conn net.Conn) (c *Client) {
    c = new(Client)
    c.Db = s.Db[0]
    c.Argv = make([]*GodisObject, 5)
    c.QueryBuf = ""
    return c
}

4.執行命令

將請求的命令分解,校驗無誤後,調用響應函數執行。注意,只在當前client結構指向的db中執行插入、查詢、更新等操做。若是須要操做其餘db,執行"select"命令便將當前client指向的db指針指向select後的位置。
執行完成後,將結果寫入到client結構的Buf字段。

下面的handle函數包括了client的建立、數據接收、執行和返回。

// 處理請求
func handle(conn net.Conn) {
    c := godis.CreateClient(conn)
    for {
        err := c.ReadQueryFromClient(conn)

        if err != nil {
            log.Println("readQueryFromClient err", err)
            return
        }
        c.ProcessInputBuffer()
        godis.ProcessCommand(c)
        responseConn(conn, c)
    }
}
// ProcessCommand 執行命令
func (s *Server) ProcessCommand(c *Client) {
    v := c.Argv[0].Ptr
    name, ok := v.(string)
    if !ok {
        log.Println("error cmd")
        os.Exit(1)
    }
    cmd := lookupCommand(name, s)
    if cmd != nil {
        c.Cmd = cmd
        call(c, s)
    } else {
        addReply(c, CreateObject(ObjectTypeString, fmt.Sprintf("(error) ERR unknown command '%s'", name)))
    }
}

ProcessCommand函數先從命令表中查找命令,若是存在,調用該命令的實現,並將結果寫入client.Buf字段。

5.響應請求

將client.Buf內容,返回給請求方,完成。
最後將執行結果返回給請求方。

// 響應返回給客戶端
func responseConn(conn net.Conn, c *core.Client) {
    conn.Write([]byte(c.Buf))
}

測試

分別編譯服務端和命令行客戶端:
go build godis-server.go
go build godis-server.go

啓動 ./godis-server

1.非法命令:


clipboard.png

2.set/get命令:

服務端啓動:
clipboard.png

cli請求:

clipboard.png

本篇問題

  1. 文本協議的格式分隔符沒有處理好,在服務端又是使用的conn.Read(buff),讀入的數據與buff額外的緩衝區混在一塊兒。而print字符串又屏蔽了有效字符串後全零的問題(print調試最好輸出原始數據)。

下集預告

1. 實現Redis統一協議

相關文章
相關標籤/搜索