Golang實(shí)現(xiàn)自己的Redis(有序集合跳表)實(shí)例探究
引言
用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 客戶端實(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)文章
Golang?int函數(shù)使用實(shí)例全面教程
這篇文章主要為大家介紹了Golang?int函數(shù)使用實(shí)例全面教程詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-10-10
golang創(chuàng)建文件目錄os.Mkdir,os.MkdirAll的區(qū)別說(shuō)明
本文主要講述os.Mkdir、os.MkdirAll區(qū)別以及在創(chuàng)建文件目錄過(guò)程中的一些其他技巧,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2021-03-03
Go事務(wù)中止時(shí)是否真的結(jié)束事務(wù)解析
這篇文章主要為大家介紹了Go事務(wù)中止時(shí)是否真的結(jié)束事務(wù)實(shí)例解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-04-04
Golang語(yǔ)言如何讀取http.Request中body的內(nèi)容
這篇文章主要介紹了Golang語(yǔ)言如何讀取http.Request中body的內(nèi)容問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-03-03
試了下Golang實(shí)現(xiàn)try catch的方法
雖然在使用Golang的時(shí)候發(fā)現(xiàn)沒(méi)有try catch這種錯(cuò)誤處理機(jī)制但是想一想golang作為一門(mén)優(yōu)雅的語(yǔ)言,似乎也是情理之中。那么夠怎么捕獲異常呢,本文就來(lái)介紹一下2021-07-07
Go語(yǔ)言中g(shù)o?mod?vendor使用方法
go mod vendor的功能是將新增的依賴包自動(dòng)寫(xiě)入當(dāng)前項(xiàng)目的 vendor目錄,下面這篇文章主要給大家介紹了關(guān)于Go語(yǔ)言中g(shù)o?mod?vendor使用的相關(guān)資料,需要的朋友可以參考下2022-10-10

