欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Golang實現自己的Redis數據庫內存實例探究

 更新時間:2024年01月24日 10:16:58   作者:紹納 nullbody筆記  
這篇文章主要為大家介紹了Golang實現自己的Redis數據庫內存實例探究,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪

引言

用11篇文章實現一個可用的Redis服務,姑且叫EasyRedis吧,希望通過文章將Redis掰開撕碎了呈現給大家,而不是僅僅停留在八股文的層面,并且有非常爽的感覺,歡迎持續(xù)關注學習。

[x] easyredis之TCP服務

[x] easyredis之網絡請求序列化協議(RESP)

[x] easyredis之內存數據庫

[ ] easyredis之過期時間 (時間輪實現)

[ ] easyredis之持久化 (AOF實現)

[ ] easyredis之發(fā)布訂閱功能

[ ] easyredis之有序集合(跳表實現)

[ ] easyredis之 pipeline 客戶端實現

[ ] easyredis之事務(原子性/回滾)

[ ] easyredis之連接池

[ ] easyredis之分布式集群存儲

EasyRedis之內存數據庫篇

上篇文章已經可以解析出Redis serialization protocol,本篇基于解析出來的命令,進行代碼處理過程: 這里以5個常用命令作為本篇文章的切入口: 命令官方文檔 https://redis.io/commands

# ping服務器
PING [message]
# 授權密碼設置
AUTH <password>
# 選擇數據庫
SELECT index
# 設置key
SET key value [NX | XX]  [EX seconds | PX milliseconds]
# 獲取key
GET key

ping服務器

代碼路徑: engine/engine.go這個功能算是小試牛刀的小功能,讓大家對基本的套路有個簡單的認識

// redisCommand 待執(zhí)行的命令  protocal.Reply 執(zhí)行結果
func (e *Engine) Exec(c *connection.KeepConnection, redisCommand [][]byte) (result protocal.Reply) {
	 //... 省略...
	commandName := strings.ToLower(string(redisCommand[0]))
	if commandName == "ping" { // https://redis.io/commands/ping/
		return Ping(redisCommand[1:])
	}
	 //... 省略...
}

Exec函數就是進行命令處理的總入口函數,通過從協議中解析出來的redisCommand,我們可以提取出命令名commandName變量,然后在Ping(redisCommand[1:])函數中進行邏輯處理。

func Ping(redisArgs [][]byte) protocal.Reply {

	iflen(redisArgs) == 0 { // 不帶參數
		return protocal.NewPONGReply()
	} elseiflen(redisArgs) == 1 { // 帶參數1個
		return protocal.NewBulkReply(redisArgs[0])
	}
    // 否則,回復命令格式錯誤
	return protocal.NewArgNumErrReply("ping")
}

Ping函數的本質就是基于PING [message]這個redis命令的基本格式,進行不同的數據響應。

這里建議大家看下Ping命令的文檔 https://redis.io/commands/ping/ 

以上圖為例

  • 如果是直接的PING命令,后面不帶參數,我們要回復PONG

  • 如果帶參"Hello world",我們原樣回復Hello world

  • 如果帶了兩個參數helloworld,直接回復錯誤。

有了這個處理套路,那么其他的命令也可依葫蘆畫瓢了。

授權密碼設置

啟動redis服務的時候,如果有設定需要密碼,那么客戶端連接上來以后,需要先執(zhí)行一次 Auth password的授權命令

// redisCommand 待執(zhí)行的命令  protocal.Reply 執(zhí)行結果
func (e *Engine) Exec(c *connection.KeepConnection, redisCommand [][]byte) (result protocal.Reply) {

	//... 省略...
	commandName := strings.ToLower(string(redisCommand[0]))
	if commandName == "auth" {
		return Auth(c, redisCommand[1:])
	}
	// 校驗密碼
	if !checkPasswd(c) {
		return protocal.NewGenericErrReply("Authentication required")
	}

	//... 省略...
}

服務器接收到命令后,依據commandName的變量值為auth,則執(zhí)行 Auth(c, redisCommand[1:])函數

func Auth(c *connection.KeepConnection, redisArgs [][]byte) protocal.Reply {
	iflen(redisArgs) != 1 {
		return protocal.NewArgNumErrReply("auth")
	}

	if conf.GlobalConfig.RequirePass == "" {
		return protocal.NewGenericErrReply("No authorization is required")
	}

	password := string(redisArgs[0])
	if conf.GlobalConfig.RequirePass != password {
		return protocal.NewGenericErrReply("Auth failed, password is wrong")
	}

	c.SetPassword(password)
	return protocal.NewOkReply()
}

這里的解析過程我們是按照 AUTH <password>這個命令格式進行解析,解析出來密碼以后,我們需要將密碼保存在c *connection.KeepConnection對象的成員變量中。這里就類似session的原理,存儲以后,當前連接接下來的命令就不需要繼續(xù)帶上密碼了。在每次處理其他命令之前,校驗下當前連接的密碼是否有效:

func checkPasswd(c *connection.KeepConnection) bool {
	// 如果沒有配置密碼
	if conf.GlobalConfig.RequirePass == "" {
		returntrue
	}
	// 密碼是否一致
	return c.GetPassword() == conf.GlobalConfig.RequirePass
}

選擇數據庫

這個命令雖然用的比較少,但是這個涉及到服務端結構的設計。redis的服務端是支持多個數據庫,每個數據庫就是一個CRUD的基本存儲單元,不同的數據庫(存儲單元)之間的數據是不共享的。默認情況下,我們使用的都是select 0數據庫。

代碼結構如下圖:(這個圖很重要,配合代碼好好理解下)

我們需要在Engine結構體中創(chuàng)建多個 *DB對象

func NewEngine() *Engine {
	engine := &Engine{}
	// 多個dbSet
	engine.dbSet = make([]*atomic.Value, conf.GlobalConfig.Databases)
	for i := 0; i < conf.GlobalConfig.Databases; i++ {
		// 創(chuàng)建 *db
		db := newDB()
		db.SetIndex(i)
		// 保存到 atomic.Value中
		dbset := &atomic.Value{}
		dbset.Store(db)
		// 賦值到 dbSet中
		engine.dbSet[i] = dbset
	}
	return engine
}

在用戶端發(fā)送來 select index 命令,服務端需要記錄下來當前連接選中的數據庫索引

// redisCommand 待執(zhí)行的命令  protocal.Reply 執(zhí)行結果
func (e *Engine) Exec(c *connection.KeepConnection, redisCommand [][]byte) (result protocal.Reply) {
	//....忽略....
	// 基礎命令
	switch commandName {
	case"select": // 表示當前連接,要選中哪個db https://redis.io/commands/select/
		return execSelect(c, redisCommand[1:])
	}
	//....忽略....
}
// 這里會對 選中的索引 越界的判斷,如果一切都正常,就保存到 c *connection.KeepConnection 連接的成員變量 index
func execSelect(c *connection.KeepConnection, redisArgs [][]byte) protocal.Reply {
	iflen(redisArgs) != 1 {
		return protocal.NewArgNumErrReply("select")
	}
	dbIndex, err := strconv.ParseInt(string(redisArgs[0]), 10, 64)
	if err != nil {
		return protocal.NewGenericErrReply("invaild db index")
	}
	if dbIndex < 0 || dbIndex >= int64(conf.GlobalConfig.Databases) {
		return protocal.NewGenericErrReply("db index out of range")
	}
	c.SetDBIndex(int(dbIndex))
	return protocal.NewOkReply()
}

設置key

在用戶要求執(zhí)行 set key value命令的時候,我們需要先選中執(zhí)行功能*DB對象,就是上面的select index命令要求選中的對象,默認是0

// redisCommand 待執(zhí)行的命令  protocal.Reply 執(zhí)行結果
func (e *Engine) Exec(c *connection.KeepConnection, redisCommand [][]byte) (result protocal.Reply) {

	//....忽略....

	// redis 命令處理
	dbIndex := c.GetDBIndex()
	logger.Debugf("db index:%d", dbIndex)
	db, errReply := e.selectDB(dbIndex)
	if errReply != nil {
		return errReply
	}
	return db.Exec(c, redisCommand)
}

可以看到,最終代碼執(zhí)行execNormalCommand函數,該函數會從命令注冊中心獲取命令的執(zhí)行函數

func (db *DB) Exec(c *connection.KeepConnection, redisCommand [][]byte) protocal.Reply {
	return db.execNormalCommand(c, redisCommand)
}
func (db *DB) execNormalCommand(c *connection.KeepConnection, redisCommand [][]byte) protocal.Reply {
	cmdName := strings.ToLower(string(redisCommand[0]))
	// 從命令注冊中心,獲取命令的執(zhí)行函數
	command, ok := commandCenter[cmdName]
	if !ok {
		return protocal.NewGenericErrReply("unknown command '" + cmdName + "'")
	}
	fun := command.execFunc
	return fun(db, redisCommand[1:])
}

最終 set命令的實際執(zhí)行函數代碼路徑為engine/string.go中的func cmdSet(db *DB, args [][]byte) protocal.Reply函數。代碼的的本質其實還是解析字符串,按照官方文檔https://redis.io/commands/set/ 要求的格式獲取對應的參數,執(zhí)行數據的存儲db.PutEntity(key, &entity)。

func (db *DB) PutEntity(key string, entity *payload.DataEntity) int {
	return db.dataDict.Put(key, entity)
}

dataDict*ConcurrentDict類型的并發(fā)安全的字典

// 并發(fā)安全的字典
type ConcurrentDict struct {
	shds  []*shard      // 底層shard切片
	mask  uint32// 掩碼
	count *atomic.Int32 // 元素個數
}

ConcurrentDict通過分片的模式,將數據分散在不同的*shard對象中,shard的本質就是map+讀寫鎖mu

type shard struct {
	m  map[string]interface{}
	mu sync.RWMutex
}

所以內存數據庫的本質就是操作map

  • key就是set命令的key

  • value我們額外包裝了一個DataEntity對象,將實際的值保存在 RedisObject

type DataEntity struct {
	RedisObject interface{} // 字符串 跳表 鏈表 quicklist 集合 etc...
}

代碼中已經注釋的很清晰,建議直接看代碼

獲取key

代碼只是額外多調用了幾層函數,本質就是調用db.dataDict.Get(key) 函數,其實又是*ConcurrentDict,代碼可能感覺有點繞,把上面的代碼結構圖好好理解一下。

func cmdGet(db *DB, args [][]byte) protocal.Reply {
	iflen(args) != 1 {
		return protocal.NewSyntaxErrReply()
	}
	key := string(args[0])
	bytes, reply := db.getStringObject(key)
	if reply != nil {
		return reply
	}
	return protocal.NewBulkReply(bytes)
}
// 獲取底層存儲對象【字節(jié)流】
func (db *DB) getStringObject(key string) ([]byte, protocal.Reply) {
	payload, exist := db.GetEntity(key)
	if !exist {
		returnnil, protocal.NewNullBulkReply()
	}
	// 判斷底層對象是否為【字節(jié)流】
	bytes, ok := payload.RedisObject.([]byte)
	if !ok {
		returnnil, protocal.NewWrongTypeErrReply()
	}
	return bytes, nil
}
// 獲取內存中的數據
func (db *DB) GetEntity(key string) (*payload.DataEntity, bool) {
	// key 不存在
	val, exist := db.dataDict.Get(key)
	if !exist {
		returnnil, false
	}
	dataEntity, ok := val.(*payload.DataEntity)
	if !ok {
		returnnil, false
	}
	return dataEntity, true
}

效果演示

項目代碼地址: https://github.com/gofish2020/easyredis 

以上就是Golang實現自己的Redis數據庫內存實例探究的詳細內容,更多關于Golang Redis數據庫內存的資料請關注腳本之家其它相關文章!

相關文章

  • 在Go中創(chuàng)建隨機的安全密碼

    在Go中創(chuàng)建隨機的安全密碼

    今天小編就為大家分享一篇關于在Go中創(chuàng)建隨機的安全密碼,小編覺得內容挺不錯的,現在分享給大家,具有很好的參考價值,需要的朋友一起跟隨小編來看看吧
    2018-10-10
  • Golang通脈之數據類型詳情

    Golang通脈之數據類型詳情

    這篇文章主要介紹了Golang通脈之數據類型,在編程語言中標識符就是定義的具有某種意義的詞,比如變量名、常量名、函數名等等,Go語言中標識符允許由字母數字和_(下劃線)組成,并且只能以字母和_開頭,更詳細內容請看下面文章吧
    2021-10-10
  • GO項目配置與使用的方法步驟

    GO項目配置與使用的方法步驟

    本文主要介紹了GO項目配置與使用的方法步驟,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧<BR>
    2022-06-06
  • Golang極簡入門教程(二):方法和接口

    Golang極簡入門教程(二):方法和接口

    這篇文章主要介紹了Golang極簡入門教程(二):方法和接口,本文同時講解了錯誤、匿名域等內容,需要的朋友可以參考下
    2014-10-10
  • 一文解析 Golang sync.Once 用法及原理

    一文解析 Golang sync.Once 用法及原理

    這篇文章主要介紹了一文解析 Golang sync.Once 用法及原理,文章圍繞主題展開詳細的內容介紹,具有一定的參考價值,需要的小伙伴可以參考一下
    2022-08-08
  • go語言編程實現遞歸函數示例詳解

    go語言編程實現遞歸函數示例詳解

    這篇文章主要為大家介紹了go語言編程實現遞歸函數示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2022-09-09
  • 深入解析Sync.Pool如何提升Go程序性能

    深入解析Sync.Pool如何提升Go程序性能

    在并發(fā)編程中,資源的分配和回收是一個很重要的問題。Go?語言的?Sync.Pool?是一個可以幫助我們優(yōu)化這個問題的工具。本篇文章將會介紹?Sync.Pool?的用法、原理以及如何在項目中正確使用它,希望對大家有所幫助
    2023-05-05
  • Golang中omitempty關鍵字的具體實現

    Golang中omitempty關鍵字的具體實現

    本文主要介紹了Golang中omitempty關鍵字的具體實現,文中通過示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2022-01-01
  • go語言的sql包原理與用法分析

    go語言的sql包原理與用法分析

    這篇文章主要介紹了go語言的sql包原理與用法,較為詳細的分析了Go語言里sql包的結構、相關函數與使用方法,需要的朋友可以參考下
    2016-07-07
  • 詳解go語言中并發(fā)安全和鎖問題

    詳解go語言中并發(fā)安全和鎖問題

    這篇文章主要介紹了go語言中并發(fā)安全和鎖問題,包含互斥鎖解鎖過程,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2021-10-10

最新評論