Golang實(shí)現(xiàn)Redis過(guò)期時(shí)間實(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))
- [ ] easyredis之發(fā)布訂閱功能
- [ ] easyredis之有序集合(跳表實(shí)現(xiàn))
- [ ] easyredis之 pipeline 客戶端實(shí)現(xiàn)
- [ ] easyredis之事務(wù)(原子性/回滾)
- [ ] easyredis之連接池
- [ ] easyredis之分布式集群存儲(chǔ)
【第四篇】EasyRedis之過(guò)期時(shí)間
在使用Redis的時(shí)候經(jīng)常會(huì)對(duì)緩存設(shè)定過(guò)期時(shí)間,例如set key value ex 3,設(shè)定過(guò)期時(shí)間3s,等到過(guò)期以后,我們?cè)賵?zhí)行get key正常情況下是得不到數(shù)據(jù)的。不同的key會(huì)設(shè)定不同的過(guò)期時(shí)間1s 5s 2s等等。按照八股文我們知道key過(guò)期的時(shí)候,有兩種刪除策略:
- 惰性刪除:不主動(dòng)刪除過(guò)期key,當(dāng)訪問(wèn)該key的時(shí)候,如果發(fā)現(xiàn)過(guò)期了再刪除 好處:對(duì)CPU友好,不用頻繁執(zhí)行刪除,但是對(duì)內(nèi)存不友好,都過(guò)期了還占用內(nèi)存
- 定時(shí)刪除:主動(dòng)刪除key,到了key的過(guò)期時(shí)間,立即執(zhí)行刪除 好處:對(duì)內(nèi)存友好,可以緩解內(nèi)存壓力,對(duì)CPU不友好,需要頻繁的執(zhí)行刪除
所以redis就把兩種策略都實(shí)現(xiàn)了,我們看下代碼如何使下?
惰性刪除
本質(zhì)就是訪問(wèn)的時(shí)候判斷下key是否過(guò)期,過(guò)期就刪除并返回空。 代碼路徑engine/database.go在獲取key的值時(shí)候,我們會(huì)執(zhí)行一次 db.IsExpire(key)判斷key是否過(guò)期
func (db *DB) GetEntity(key string) (*payload.DataEntity, bool) {
// key 不存在
val, exist := db.dataDict.Get(key)
if !exist {
returnnil, false
}
// key是否過(guò)期(主動(dòng)檢測(cè)一次)
if db.IsExpire(key) {
returnnil, false
}
// 返回內(nèi)存數(shù)據(jù)
dataEntity, ok := val.(*payload.DataEntity)
if !ok {
returnnil, false
}
return dataEntity, true
}就是從過(guò)期字典ttlDict中獲取key的過(guò)期時(shí)間
如果沒(méi)有獲取到,說(shuō)明沒(méi)有設(shè)定過(guò)期時(shí)間(do nothing)
如果有過(guò)期時(shí)間,并且時(shí)間已經(jīng)過(guò)期,主動(dòng)刪除之
// 判斷key是否已過(guò)期
func (db *DB) IsExpire(key string) bool {
val, result := db.ttlDict.Get(key)
if !result {
returnfalse
}
expireTime, _ := val.(time.Time)
isExpire := time.Now().After(expireTime)
if isExpire { // 如果過(guò)期,主動(dòng)刪除
db.Remove(key)
}
return isExpire
}定時(shí)刪除
本質(zhì)是對(duì)key設(shè)定一個(gè)過(guò)期時(shí)間,時(shí)間一到立即執(zhí)行刪除的任務(wù)。 正常的思路肯定是設(shè)定一個(gè)固定的定時(shí)器,例如3s檢測(cè)一次,這種思路可以,但是存在一個(gè)問(wèn)題,
如果key的過(guò)期時(shí)間為
1s,那你3s才檢測(cè)是否太不夠及時(shí)了?那就把檢測(cè)間隔設(shè)定為
1s吧,那如果key的過(guò)期時(shí)間都為3s,到執(zhí)行時(shí)間檢測(cè)一遍發(fā)現(xiàn)任務(wù)都沒(méi)過(guò)期,那不就白白浪費(fèi)CPU時(shí)間了嗎?
這就要推出我們的時(shí)間輪算法了,時(shí)間輪算法就是在模擬現(xiàn)實(shí)世界鐘表的原理

我想里面增加2個(gè)3s的任務(wù),那就將任務(wù)添加到距離當(dāng)前位置
pos + 3的位置同時(shí)再加1個(gè)5s的任務(wù),那就將任務(wù)添加到距離當(dāng)前位置
pos + 5的位置

當(dāng)鐘表的指針指向pos + 3的位置,就執(zhí)行任務(wù)鏈表的任務(wù)即可。 因?yàn)殓姳硎茄h(huán)往復(fù)的運(yùn)行,那如果我再添加11s的任務(wù),可以發(fā)現(xiàn)該任務(wù)也是放置到 pos+3的位置,那任務(wù)就要區(qū)分下,到底是3s的任務(wù)還是11s的任務(wù)

所以里面又有了一個(gè)circle的標(biāo)記,表示當(dāng)前任務(wù)是第幾圈的任務(wù)
代碼路徑tool/timewheel
代碼中通過(guò)切片模型環(huán),通過(guò)鏈表模擬任務(wù)鏈表
// 循環(huán)隊(duì)列 + 鏈表
type TimeWheel struct {
// 間隔
interval time.Duration
// 定時(shí)器
ticker *time.Ticker
// 游標(biāo)
curSlotPos int
// 循環(huán)隊(duì)列大小
slotNum int
// 底層存儲(chǔ)
slots []*list.List
m map[string]*taskPos
// 任務(wù)通道
addChannel chan *task
cacelChannel chanstring
// 停止
stopChannel chanstruct{}
}當(dāng)添加任務(wù)的時(shí)候,需要通過(guò)延遲時(shí)間計(jì)算當(dāng)前任務(wù)的圈數(shù)circle
func (tw *TimeWheel) posAndCircle(d time.Duration) (pos, circle int) {
// 延遲(秒)
delaySecond := int(d.Seconds())
// 間隔(秒)
intervalSecond := int(tw.interval.Seconds())
// delaySecond/intervalSecond 表示從curSlotPos位置偏移
pos = (tw.curSlotPos + delaySecond/intervalSecond) % tw.slotNum
circle = (delaySecond / intervalSecond) / tw.slotNum
return
}
func (tw *TimeWheel) addTask(t *task) {
// 定位任務(wù)應(yīng)該保存在循環(huán)隊(duì)列的位置 & 圈數(shù)
pos, circle := tw.posAndCircle(t.delay)
t.circle = circle
// 將任務(wù)保存到循環(huán)隊(duì)列pos位置
ele := tw.slots[pos].PushBack(t)
// 在map中記錄 key -> { pos, ele } 的映射
if t.key != "" {
// 已經(jīng)存在重復(fù)的key
if _, ok := tw.m[t.key]; ok {
tw.cancelTask(t.key)
}
tw.m[t.key] = &taskPos{pos: pos, ele: ele}
}
}代碼中注釋的很清晰,也就100多行建議看代碼結(jié)合上圖體會(huì)下(很簡(jiǎn)單)
額外補(bǔ)充
我們?cè)趫?zhí)行set key value ex 3的時(shí)候,先設(shè)定過(guò)期時(shí)間為3s,但是在1s的時(shí)候,我們又執(zhí)行了set key value,請(qǐng)問(wèn)key還會(huì)過(guò)期嗎??
答案:不會(huì)過(guò)期了。相當(dāng)于對(duì)key去掉了過(guò)期時(shí)間。所以在代碼處理中,我們需要考慮這種情況,重復(fù)設(shè)定的問(wèn)題
代碼細(xì)節(jié)位于engine/string.go set命令處理函數(shù)func cmdSet(db *DB, args [][]byte) protocal.Reply 的尾部位置

項(xiàng)目代碼地址: https://github.com/gofish2020/easyredis
以上就是Golang實(shí)現(xiàn)Redis過(guò)期時(shí)間實(shí)例探究的詳細(xì)內(nèi)容,更多關(guān)于Golang Redis過(guò)期時(shí)間的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
- Golang實(shí)現(xiàn)自己的Redis數(shù)據(jù)庫(kù)內(nèi)存實(shí)例探究
- 探索Golang實(shí)現(xiàn)Redis持久化AOF實(shí)例
- 探索Golang?Redis實(shí)現(xiàn)發(fā)布訂閱功能實(shí)例
- Golang實(shí)現(xiàn)自己的Redis(有序集合跳表)實(shí)例探究
- Golang實(shí)現(xiàn)自己的Redis(TCP篇)實(shí)例探究
- Golang實(shí)現(xiàn)自己的Redis(pipeline客戶端)實(shí)例探索
- Golang實(shí)現(xiàn)Redis事務(wù)深入探究
- Golang實(shí)現(xiàn)Redis網(wǎng)絡(luò)協(xié)議實(shí)例探究
相關(guān)文章
Go語(yǔ)言學(xué)習(xí)教程之goroutine和通道的示例詳解
這篇文章主要通過(guò)A?Tour?of?Go中的例子進(jìn)行學(xué)習(xí),以此了解Go語(yǔ)言中的goroutine和通道,文中的示例代碼講解詳細(xì),感興趣的可以了解一下2022-09-09
深入了解Golang網(wǎng)絡(luò)編程N(yùn)et包的使用
net包主要是增加?context?控制,封裝了一些不同的連接類(lèi)型以及DNS?查找等等,同時(shí)在有需要的地方引入?goroutine?提高處理效率。本文主要和大家分享下在Go中網(wǎng)絡(luò)編程的實(shí)現(xiàn),需要的可以參考一下2022-07-07
重學(xué)Go語(yǔ)言之錯(cuò)誤處理與異常機(jī)制詳解
Go語(yǔ)言的開(kāi)發(fā)者顯然覺(jué)得?try-catch被濫用了,因此?Go不支持使用?try-catch語(yǔ)句捕獲異常處理,那么,Go語(yǔ)言是如何定義和處理程序的異常呢,下面我們就來(lái)看看吧2023-08-08

