iT邦幫忙

2026 iThome 鐵人賽

DAY 7
0
Software Development

手刻 Redis:用 Go 從零打造高效能高併發的記憶體資料庫系列 第 7

Day 07:實作記憶體引擎基礎命令:SET, GET, DEL 與 EXISTS

  • 分享至 

  • xImage
  •  

引擎骨架弄好後,今天直接來實作最基本的 String 操作:SETGETDELEXISTS


基礎命令的設計與實作

我在 code/db/db.go 裡加了這幾個 func。

1. 寫入資料:SET

Redis 的 SET 不只是寫進去而已,還可以順便帶過期時間。我的 Go API 先收一個 time.Duration,如果有設定 TTL,就換算成一個絕對過期時間存起來:

func (db *DB) Set(key string, val []byte, ttl time.Duration) {
	db.mu.Lock()
	defer db.mu.Unlock()

	var expireAt time.Time
	if ttl > 0 {
		expireAt = time.Now().Add(ttl) // 計算過期的絕對時間
	}

	db.data[key] = &entry{
		dataType: TypeString,
		val:      val,
		expireAt: expireAt,
	}
}

2. 讀取資料:GET(含鎖優化思考)

GET 表面上只是讀資料,但因為後面要支援 惰性刪除(Lazy Expiration),讀到過期資料時會順手把 key 刪掉,所以它其實可能會改 map。這裡先用寫鎖 Lock,避免 race:

func (db *DB) Get(key string) (interface{}, bool) {
	db.mu.Lock() // 因為惰性刪除可能會修改 map,故此處使用寫鎖 Lock
	defer db.mu.Unlock()

	e, exists := db.getEntryWithoutLock(key)
	if !exists {
		return nil, false
	}
	return e.val, true
}

寫 Get 的時候原本想用 RLock 拼效能,後來想到惰性刪除可能會改 map,乖乖換回 Lock,這細節還滿容易踩坑的。

[!TIP]
效能優化思考(雙重檢查鎖)
如果每次 Get 都直接上寫鎖,讀取併發會被限制住。比較好的做法是先用 RLock 讀資料並檢查是否過期;沒過期就直接回傳,真的過期時才改用寫鎖刪除。這個優化先記著,等基本功能穩了再補。

3. 刪除資料:DEL

DEL 命令可以接收多個 Key,並回傳成功刪除的 Key 數量。如果 Key 不存在或已過期,則不計入刪除數量:

func (db *DB) Del(keys ...string) int {
	db.mu.Lock()
	defer db.mu.Unlock()

	deleted := 0
	for _, key := range keys {
		if _, exists := db.getEntryWithoutLock(key); exists {
			delete(db.data, key) // 從 map 中移除
			deleted++
		}
	}
	return deleted
}

4. 判斷存在:EXISTS

EXISTS 接收多個 Key,並回傳其中「活著的」(即存在且未過期)Key 的個數:

func (db *DB) Exists(keys ...string) int {
	db.mu.Lock()
	defer db.mu.Unlock()

	existsCount := 0
	for _, key := range keys {
		if _, exists := db.getEntryWithoutLock(key); exists {
			existsCount++
		}
	}
	return existsCount
}

metadata檢查輔助方法

我另外寫了一個私有輔助方法 getEntryWithoutLock。這樣所有讀取路徑都能共用同一套過期判斷,不用每個命令都手寫一次:

func (db *DB) getEntryWithoutLock(key string) (*entry, bool) {
	e, exists := db.data[key]
	if !exists {
		return nil, false
	}

	// 檢查是否已過期
	if e.isExpired() {
		delete(db.data, key) // 惰性刪除
		return nil, false
	}

	return e, true
}

跑起來看看

這幾天打的底終於可以連貫起來了!先啟動伺服器:

$ go run ./code/main.go

printf + nc 送完整的 RESP 陣列指令過去:

# 設定 key: mykey, value: hello
$ printf "*3\r\n\$3\r\nSET\r\n\$5\r\nmykey\r\n\$5\r\nhello\r\n" | nc localhost 6379
# 預期回覆:+OK

# 取得 mykey
$ printf "*2\r\n\$3\r\nGET\r\n\$5\r\nmykey\r\n" | nc localhost 6379
# 預期回覆:$5\r\nhello

# 檢查 mykey 是否存在
$ printf "*2\r\n\$6\r\nEXISTS\r\n\$5\r\nmykey\r\n" | nc localhost 6379
# 預期回覆::1

# 刪除 mykey
$ printf "*2\r\n\$3\r\nDEL\r\n\$5\r\nmykey\r\n" | nc localhost 6379
# 預期回覆::1

看到字串的 SET 和 GET 正確運作,而且回傳的是標準 RESP 格式,代表我們已經寫出一個超迷你的 Redis 了!

總結

今天把最基本的 SETGETDELEXISTS 補起來,也先埋好過期時間檢查的入口。

明天會正式處理 TTL 和惰性刪除。記憶體管理這塊細節應該不少,明天見囉!


上一篇
Day 06:設計與實作thread-safe 的記憶體儲存引擎
下一篇
Day 08:過期時間(TTL)與惰性刪除機制深度解析
系列文
手刻 Redis:用 Go 從零打造高效能高併發的記憶體資料庫12
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言