引擎骨架弄好後,今天直接來實作最基本的 String 操作:SET、GET、DEL 和 EXISTS。
我在 code/db/db.go 裡加了這幾個 func。
SETRedis 的 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,
}
}
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讀資料並檢查是否過期;沒過期就直接回傳,真的過期時才改用寫鎖刪除。這個優化先記著,等基本功能穩了再補。
DELDEL 命令可以接收多個 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
}
EXISTSEXISTS 接收多個 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
}
我另外寫了一個私有輔助方法 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 了!
今天把最基本的 SET、GET、DEL 與 EXISTS 補起來,也先埋好過期時間檢查的入口。
明天會正式處理 TTL 和惰性刪除。記憶體管理這塊細節應該不少,明天見囉!