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

Golang實(shí)現(xiàn)自己的Redis(有序集合跳表)實(shí)例探究

 更新時(shí)間:2024年01月24日 09:27:18   作者:紹納?nullbody筆記  
這篇文章主要為大家介紹了Golang實(shí)現(xiàn)自己的Redis(有序集合跳表)實(shí)例探究,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪

引言

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

[x] easyredis之TCP服務(wù)

[x] easyredis之網(wǎng)絡(luò)請(qǐng)求序列化協(xié)議(RESP)

[x] easyredis之內(nèi)存數(shù)據(jù)庫(kù)

[x] easyredis之過(guò)期時(shí)間 (時(shí)間輪實(shí)現(xiàn))

[x] easyredis之持久化 (AOF實(shí)現(xiàn))

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

[x] easyredis之有序集合(跳表實(shí)現(xiàn))

[ ] easyredis之 pipeline 客戶(hù)端實(shí)現(xiàn)

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

[ ] easyredis之連接池

[ ] easyredis之分布式集群存儲(chǔ)

EasyRedis之有序集合(跳表實(shí)現(xiàn))

有序集合需求分析:

  • 集合:同一個(gè)member只能出現(xiàn)一次(去重)
  • 有序:可以正序or倒序遍歷

Golang中去重我們可以使用map去重,有序則使用鏈表。所以有序集合的數(shù)據(jù)結(jié)構(gòu)定義如下:

// score可以相同,member不能重復(fù)
type SortedSet struct {
	dict map[string]*Pair // 利用map基于member去重
	skl  *skiplist        // 利用skl進(jìn)行排序
}
// 要存儲(chǔ)的值
type Pair struct {
	Member string
	Score  float64
}

那應(yīng)該怎么利用鏈表實(shí)現(xiàn)有序的?很簡(jiǎn)單,插入/修改/刪除都保證鏈表的有序性,那整個(gè)鏈表最后肯定就是一直有序的

例如:要插入節(jié)點(diǎn)9,為了保證鏈表的有序行,需要找到前驅(qū)節(jié)點(diǎn)。

前驅(qū)節(jié)點(diǎn):即比9小的節(jié)點(diǎn)中的【最大的一個(gè)】。1/2/3/4/5都比9小,但是這些小的當(dāng)中最大的一個(gè)是5。查找的方式簡(jiǎn)單粗暴,就是遍歷鏈表,從頭開(kāi)始遍歷。

偽代碼:

type  node struct{
	val int64
}
var curNode *node = &node{val:9}
if pre.next != nil && pre.next.val < curNode.val
{
	pre = pre.next
}
// 插入
pre.next = curNode

可以看出,每次我要插入一個(gè)新節(jié)點(diǎn),都是要遍歷鏈表,定位出前驅(qū)節(jié)點(diǎn),隨著鏈表長(zhǎng)度越來(lái)越長(zhǎng),是不是整個(gè)鏈表的插入效率就越來(lái)越差。那有沒(méi)有辦法快速定位到鏈表中的前驅(qū)節(jié)點(diǎn)。

這里就引申出了跳表的實(shí)現(xiàn)。 跳表的本質(zhì)就是鏈表,只是鏈表節(jié)點(diǎn)個(gè)頭長(zhǎng)高了。普通的鏈表只有一層,跳表是包含多層的鏈表,

當(dāng)我們要查找前驅(qū)節(jié)點(diǎn)5的時(shí)候, 偽代碼如下:

type Level struct {
    forward *node// 每層的指向
    span int64// 每層的跨度
}
type node struct {
    val int64
    level []*Level //每個(gè)節(jié)點(diǎn)的層
}
var maxlevel = 5
// 被插入的節(jié)點(diǎn)9 
var curNode *node = &node{val:9}
// dummy 節(jié)點(diǎn)
var dummy *node = &node{val:0}
// pre一開(kāi)始指向dummy節(jié)點(diǎn)
var pre *node = dummy
// 從最高層開(kāi)始,這里的i表示層高索引
for i := maxlevel-1;i >= 0;i-- {
    for pre.level[i].forward != nil && pre.level[i].forward.val < curNode.val {
        pre = pre.level[i].forward
    }
}
  • 從最高層開(kāi)始進(jìn)行遍歷,這里的最高層是 pre.level[4]

  • 當(dāng) pre.level[4].forward != nil && pre.level[4].forward.val < 9,更新pre的指向節(jié)點(diǎn)4。繼續(xù)重復(fù)for循環(huán)過(guò)程,因?yàn)閜re指向了節(jié)點(diǎn)4,所以 pre.level[4].forward == nil,(結(jié)合下圖理解)跳出內(nèi)部的for循環(huán),外部的for循環(huán)會(huì)將層高i-1。即:降層

  • 等到所有的層級(jí)全部遍歷完成以后,此時(shí)pre的指向就是最終的節(jié)點(diǎn)5

通過(guò)這種給節(jié)點(diǎn)增加層高的概念,可以看到找到節(jié)點(diǎn)5,我們只需要跳躍兩次從dummy -> 節(jié)點(diǎn)4 -> 節(jié)點(diǎn)5,比遍歷單層的鏈表效率高了很多。

有序集合代碼講解

有了上面的基礎(chǔ)知識(shí),就以zadd key score member向有序集合中插入為例講解下代碼。 入口函數(shù)位于engine/sortedset.go

// zadd key score member [score member...]
func cmdZAdd(db *DB, args [][]byte) protocol.Reply {
	//... 省略...
    // 獲取有序集合對(duì)象
	sortedSet, _, reply := db.getOrInitSortedSetObject(key)
	if reply != nil {
		return reply
	}
	// 將pairs保存到有序集合中
	i := int64(0)
	for _, pair := range pairs {
		if sortedSet.Add(pair.Member, pair.Score) {
			i++ // 新增的個(gè)數(shù)
		}
	}
	//... 省略...
}

其實(shí)就是調(diào)用sortedSet.Add(pair.Member, pair.Score)函數(shù). 在有序集合中,通過(guò)map保證member的唯一性(去重);如果有重復(fù)的,跳表中會(huì)先刪除;通過(guò) s.skl.insert(member, score)將數(shù)據(jù)插入到跳表中,并保證有序行;

// bool 為true表示新增, false表示修改
func (s *SortedSet) Add(member string, score float64) bool {
	pair, ok := s.dict[member]
	s.dict[member] = &Pair{
		Member: member,
		Score:  score,
	}
	// 說(shuō)明是重復(fù)添加
	if ok {
		// 分值不同
		if score != pair.Score {
			// 將原來(lái)的從跳表中刪除
			s.skl.remove(pair.Member, pair.Score)
			// 插入新值
			s.skl.insert(member, score)
		}
		// 分值相同,do nothing...
		returnfalse
	}
	// 新增
	s.skl.insert(member, score)
	returntrue
}

插入的邏輯細(xì)節(jié)很多,但是思想不復(fù)雜;因?yàn)樘硎嵌鄬拥?,所以插入一個(gè)新節(jié)點(diǎn)的時(shí)候,【每層的前驅(qū)節(jié)點(diǎn)】需要記錄下來(lái)beforeNode。如下圖:

如果我向插入一個(gè)節(jié)點(diǎn)3

  • level[4]第4層的前驅(qū)節(jié)點(diǎn)是 dummy
  • level[3]第3層的前驅(qū)節(jié)點(diǎn)是 1
  • level[2]第2層的前驅(qū)節(jié)點(diǎn)是 2
  • level[1]第1層的前驅(qū)節(jié)點(diǎn)是 2
  • level[0]第0層的前驅(qū)節(jié)點(diǎn)是 2

可以看到每一層的前驅(qū)節(jié)點(diǎn)是不一定相同的,所以每一層都記錄下來(lái),當(dāng)節(jié)點(diǎn)3要插入的時(shí)候,需要每一層進(jìn)行相連接。

完整代碼如下:

// 增
func (s *skiplist) insert(member string, score float64) *node {
	beforeNode := make([]*node, defaultMaxLevel)      // 每一層的前驅(qū)節(jié)點(diǎn)
	beforeNodeOrder := make([]int64, defaultMaxLevel) // 每一層的前驅(qū)節(jié)點(diǎn)排序編號(hào)
	node := s.header
	// i從最高層遍歷,通過(guò)遍歷,將member和score在每一層的前驅(qū)節(jié)點(diǎn)全部保存在 beforeNode中,以及節(jié)點(diǎn)的排序保存在beforeNodeOrder
	for i := s.maxLevel - 1; i >= 0; i-- {
		// 節(jié)點(diǎn)node的排序
		if i == s.maxLevel-1 {
			beforeNodeOrder[i] = 0
		} else {
			beforeNodeOrder[i] = beforeNodeOrder[i+1]
		}
		// 節(jié)點(diǎn)node在當(dāng)前i層的forward不為空
		for node.levels[i].forward != nil &&
			// node在層levels[i]的forward節(jié)點(diǎn),分值 < score 或者 分值相同但是成員 < member,說(shuō)明forward指向的節(jié)點(diǎn),作為下一個(gè)前驅(qū)節(jié)點(diǎn)
			(node.levels[i].forward.Score < score || (node.levels[i].forward.Score == score && node.levels[i].forward.Member < member)) {
			beforeNodeOrder[i] += int64(node.levels[i].span) // 更新節(jié)點(diǎn)node的編號(hào)
			node = node.levels[i].forward                    // 更新當(dāng)前節(jié)點(diǎn)node
		}
		beforeNode[i] = node
	}
	// 新節(jié)點(diǎn)層高
	newLevel := randomLevel()
	// 如果新層高比當(dāng)前已經(jīng)存在的層高s.maxLevel都要高,說(shuō)明還缺少了 newLevel - s.maxLevel范圍的前驅(qū)節(jié)點(diǎn)
	if newLevel > s.maxLevel {
		for i := s.maxLevel; i < newLevel; i++ {
			// beforeNode[i] 表示在i層的前驅(qū)節(jié)點(diǎn)
			beforeNode[i] = s.header
			beforeNodeOrder[i] = 0
			beforeNode[i].levels[i].forward = nil
			beforeNode[i].levels[i].span = s.length
		}
	}
	node = newNode(newLevel, member, score)
	// 將節(jié)點(diǎn)插入到多層鏈表中,僅僅對(duì)[0,newLevel)范圍進(jìn)行節(jié)點(diǎn)拼接
	for i := int16(0); i < newLevel; i++ {
		//也就是在每一層插入節(jié)點(diǎn)
		node.levels[i].forward = beforeNode[i].levels[i].forward
		beforeNode[i].levels[i].forward = node
		// 更新本層節(jié)點(diǎn)跨度
		node.levels[i].span = beforeNode[i].levels[i].span - (beforeNodeOrder[0] - beforeNodeOrder[i])
		beforeNode[i].levels[i].span = beforeNodeOrder[0] - beforeNodeOrder[i] + 1
	}
	// 如果新節(jié)點(diǎn)的高度很低,比最高低很多
	for i := newLevel; i < s.maxLevel; i++ {
		beforeNode[i].levels[i].span++ // 超過(guò)的節(jié)點(diǎn)的跨度默認(rèn)+1
	}
	// 修改第0層的 backward指向
	if beforeNode[0] == s.header {
		node.backward = nil
	} else {
		node.backward = beforeNode[0]
	}
	if node.levels[0].forward != nil {
		node.levels[0].forward.backward = node
	} else { // 說(shuō)明node是最后一個(gè)節(jié)點(diǎn)
		s.tailer = node
	}
	// 因?yàn)樾略?,?shù)量+1
	s.length++
	return node
}

代碼中還有一個(gè)細(xì)節(jié):節(jié)點(diǎn)3的高度因?yàn)槭请S機(jī)的,可能很高,也可能很矮

當(dāng)很高的時(shí)候,超過(guò)當(dāng)前最高的部分的前驅(qū)節(jié)點(diǎn)就是s.header 代碼細(xì)節(jié):

// 如果新層高比當(dāng)前已經(jīng)存在的層高s.maxLevel都要高,說(shuō)明還缺少了 newLevel - s.maxLevel范圍的前驅(qū)節(jié)點(diǎn)
	if newLevel > s.maxLevel {
		for i := s.maxLevel; i < newLevel; i++ {
			// beforeNode[i] 表示在i層的前驅(qū)節(jié)點(diǎn)
			beforeNode[i] = s.header
			beforeNodeOrder[i] = 0
			beforeNode[i].levels[i].forward = nil
			beforeNode[i].levels[i].span = s.length
		}
	}

當(dāng)很矮的時(shí)候,比最高部分矮的部分前驅(qū)節(jié)點(diǎn),需要span+1 代碼細(xì)節(jié):

// 如果新節(jié)點(diǎn)的高度很低,比最高低很多
	for i := newLevel; i < s.maxLevel; i++ {
		beforeNode[i].levels[i].span++ // 超過(guò)的節(jié)點(diǎn)的跨度默認(rèn)+1
	}

效果演示

項(xiàng)目代碼地址: https://github.com/gofish2020/easyredis 

以上就是Golang實(shí)現(xiàn)自己的Redis(有序集合跳表)實(shí)例探究的詳細(xì)內(nèi)容,更多關(guān)于Golang Redis有序集合的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • 關(guān)于golang利用channel和goroutine完成統(tǒng)計(jì)素?cái)?shù)的思路

    關(guān)于golang利用channel和goroutine完成統(tǒng)計(jì)素?cái)?shù)的思路

    這篇文章主要介紹了golang利用channel和goroutine完成統(tǒng)計(jì)素?cái)?shù)的思路詳解,通過(guò)思路圖分析及實(shí)例代碼相結(jié)合給大家介紹的非常詳細(xì),需要的朋友可以參考下
    2021-08-08
  • Go語(yǔ)言--切片(Slice)詳解

    Go語(yǔ)言--切片(Slice)詳解

    這篇文章主要介紹了Go語(yǔ)言--切片(Slice),Go 語(yǔ)言切片是對(duì)數(shù)組的抽象,下面文章小編將為大家詳細(xì)介紹該內(nèi)容,需要的朋友可以參考下,希望對(duì)你有所幫助
    2021-10-10
  • go語(yǔ)言環(huán)境搭建簡(jiǎn)述

    go語(yǔ)言環(huán)境搭建簡(jiǎn)述

    本文簡(jiǎn)單記錄了下go語(yǔ)言環(huán)境的搭建流程,給小伙伴們一個(gè)參考,希望大家能夠喜歡。
    2015-01-01
  • Golang?sync.Map底層實(shí)現(xiàn)場(chǎng)景示例詳解

    Golang?sync.Map底層實(shí)現(xiàn)場(chǎng)景示例詳解

    這篇文章主要為大家介紹了Golang?sync.Map底層實(shí)現(xiàn)及使用場(chǎng)景示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-09-09
  • Golang中HTTP服務(wù)的分析與設(shè)計(jì)詳解

    Golang中HTTP服務(wù)的分析與設(shè)計(jì)詳解

    這篇文章主要介紹了Golang中HTTP服務(wù)的分析與設(shè)計(jì),HTTP服務(wù)是實(shí)現(xiàn)Web應(yīng)用程序的重要組成部分,為了實(shí)現(xiàn)高效可擴(kuò)展的Web應(yīng)用程序,需要對(duì)HTTP服務(wù)進(jìn)行分析與設(shè)計(jì),需要的朋友可以參考下
    2023-05-05
  • go語(yǔ)言編程實(shí)現(xiàn)遞歸函數(shù)示例詳解

    go語(yǔ)言編程實(shí)現(xiàn)遞歸函數(shù)示例詳解

    這篇文章主要為大家介紹了go語(yǔ)言編程實(shí)現(xiàn)遞歸函數(shù)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-09-09
  • 利用rpm打包上線(xiàn)部署golang代碼的方法教程

    利用rpm打包上線(xiàn)部署golang代碼的方法教程

    RPM是RPM Package Manager(RPM軟件包管理器)的縮寫(xiě),這篇文章主要給大家介紹了關(guān)于利用rpm打包上線(xiàn)部署golang代碼的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧。
    2018-01-01
  • GoFrame框架gset使用對(duì)比PHP?Java?Redis優(yōu)勢(shì)

    GoFrame框架gset使用對(duì)比PHP?Java?Redis優(yōu)勢(shì)

    這篇文章主要為大家介紹了GoFrame框架gset對(duì)比PHP?Java?Redis的使用優(yōu)勢(shì)詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-06-06
  • Go語(yǔ)言死鎖與goroutine泄露問(wèn)題的解決

    Go語(yǔ)言死鎖與goroutine泄露問(wèn)題的解決

    最近在工作中使用golang編程,今天的文章給大家分享一下Go語(yǔ)言死鎖與goroutine泄露問(wèn)題,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2021-07-07
  • Golang Gorm 更新字段save、update、updates

    Golang Gorm 更新字段save、update、updates

    在gorm中,批量更新操作可以通過(guò)使用Update方法來(lái)實(shí)現(xiàn),本文主要介紹了Golang Gorm 更新字段save、update、updates,具有一定的參考價(jià)值,感興趣的可以了解一下
    2023-12-12

最新評(píng)論