詳解 mini-redis 復(fù)刻 Redis 的 I NCR 指令
因為近期比較忙碌,所以對于mini-redis的復(fù)刻基本處于一些指令向的完善,而本文將針對字符串操作中介紹筆者近期所復(fù)刻的鍵值自增指令的落地思路,以幫助讀者更好的理解和學(xué)習(xí)mini-redis。
對象類型前置校驗
因為指令是基于字符串操作的,所以在執(zhí)行INCR或者DECR之前我們都必須針對入?yún)⒌逆I值對進(jìn)行校驗,所以對于以下情況,我們都必須采用fail-fast的方式提前將失敗暴露,將鍵值對已存在,對應(yīng)的值非字符串類型(例如:字典類型),直接響應(yīng)錯誤:
基于上述的基本概念,我們給出落地的代碼,即位于command.go的incrDecrCommand方法,可以看到我們會優(yōu)先到redis內(nèi)存中查看是否存在對應(yīng)的key,如果存在則進(jìn)行必要的類型判斷,如果非字符串類型即REDIS_STRING則直接響應(yīng)錯誤出去,并直接返回:
func incrDecrCommand(c *redisClient, incr int64) {
var value int64
var oldValue int64
var newObj *robj
//查看鍵值對是否存在
o := lookupKeyWrite(c.db, c.argv[1])
//如果鍵值對存在且類型非字符串類型,直接響應(yīng)錯誤并返回
if o != nil && checkType(c, o, REDIS_STRING) {
return
}
//......
}
對此我們也給出checkType的內(nèi)部邏輯,可以看到當(dāng)比對類型不一致時會直接輸出錯誤并返回true,讀者可以參考注釋了解:
func checkType(c *redisClient, o *robj, rType int) bool {
//如果類型不一致,則輸出-WRONGTYPE Operation against a key holding the wrong kind of value
if o.robjType != rType {
addReply(c, shared.wrongtypeerr)
return true
}
return false
}
其實筆者這里也想吐槽一句redis對于函數(shù)設(shè)計的語義的不恰當(dāng)性,理論性合理的函數(shù)進(jìn)行校驗時正確的做法應(yīng)該是:
- 邏輯校驗失敗,輸出錯誤返回false。
- 邏輯校驗正確,返回true。
也只能說因為某些歷史原因,或者設(shè)計者有著自己的主觀編碼習(xí)慣吧,本著一比一的復(fù)刻理念,筆者也沿襲了這樣的編碼思路。
基于數(shù)值池高效完成字符串轉(zhuǎn)換
針對字符串類型(可以轉(zhuǎn)數(shù)值的情況下,它也會轉(zhuǎn)數(shù)值類型),我們都是通過robj類型創(chuàng)建和維護(hù),因為我們本次所復(fù)刻的incr和decr所操作的類型是字符串中可轉(zhuǎn)為數(shù)值的對象,所以本著數(shù)值類型有跡可循的規(guī)律以及空間換時間的思想,我們提出池化思想,即將0-9999數(shù)值緩存一份數(shù)值池,后續(xù)的增減操作后處于該范圍的數(shù)值都可以直接使用數(shù)值池里對應(yīng)的robj對象,以節(jié)約robj對象創(chuàng)建的開銷和非必要的內(nèi)存資源占用:
所以筆者在main.go中聲明sharedObjectsStruct 這個結(jié)構(gòu)體中聲明了一個integers維護(hù)常量池的robj對象:
type sharedObjectsStruct struct {
//......
integers [REDIS_SHARED_INTEGERS]*robj //通用0~9999常量數(shù)值池
//......
}
然后在createSharedObjects方法中完成初始化,后續(xù)就可以直接使用了:
func createSharedObjects() {
//......
var i int64
//初始化常量池對象
for i = 0; i < REDIS_SHARED_INTEGERS; i++ {
//基于接口封裝數(shù)值
num := interface{}(i)
//生成string對象
shared.integers[i] = createObject(REDIS_STRING, &num)
//聲明編碼類型為int
shared.integers[i].encoding = REDIS_ENCODING_INT
}
//......
}
于是我們就得出了后續(xù)的編碼邏輯:
- 將value強轉(zhuǎn)為數(shù)值判斷是否超出范圍,如果超了則拋出異常。反之進(jìn)入步驟2。
- 查看取值范圍是否大于10000,如果是則自己生成robj對象,反之采用池化數(shù)值池的robj。
- 基于1、2生成的數(shù)值對象將鍵值對更新或者覆蓋到內(nèi)存數(shù)據(jù)庫中。
/**
針對字符串類型的值進(jìn)行如下判斷的和轉(zhuǎn)換:
1. 如果為空,說明本次的key不存在,直接初始化一個空字符串,后續(xù)會直接初始化一個0值使用
2. 如果是字符串類型,則轉(zhuǎn)為字符串類型
3. 如果是數(shù)值類型,則先轉(zhuǎn)為字符串類型進(jìn)行后續(xù)的通用數(shù)值轉(zhuǎn)換操作保證一致性
*/
var s string
if o == nil {
s = ""
} else if isString(*o.ptr) {
s = (*o.ptr).(string)
} else {
s = strconv.FormatInt((*o.ptr).(int64), 10)
}
//進(jìn)行類型強轉(zhuǎn)為數(shù)值,如果失敗,直接輸出錯誤并返回
if getLongLongFromObjectOrReply(c, s, &value, nil) != REDIS_OK {
return
}
oldValue = value
//如果累加超范圍則報錯
if (incr < 0 && oldValue < 0 && incr < (math.MinInt64-oldValue)) ||
(incr > 0 && oldValue > 0 && incr > (math.MaxInt64-oldValue)) {
errReply := "increment or decrement would overflow"
addReplyError(c, &errReply)
return
}
//基于incr累加的值生成value
value += incr
//如果超常量池范圍則封裝一個對象使用
if o != nil &&
(value < 0 || value >= REDIS_SHARED_INTEGERS) &&
(value > math.MinInt64 || value < math.MaxInt64) {
newObj = o
i := interface{}(value)
o.ptr = &i
} else if o != nil {//如果對象存在,且累加結(jié)果沒超范圍則調(diào)用createStringObjectFromLongLong獲取常量對象
newObj = createStringObjectFromLongLong(value)
//將寫入結(jié)果覆蓋
dbOverwrite(c.db, c.argv[1], newObj)
} else {//從常量池獲取數(shù)值,然后添加鍵值對到數(shù)據(jù)庫中
newObj = createStringObjectFromLongLong(value)
dbAdd(c.db, c.argv[1], newObj)
}
通用結(jié)果響應(yīng)
完成上述操作后就是將結(jié)果按照RESP協(xié)議規(guī)范將結(jié)果響應(yīng)給客戶端,按照協(xié)議要求數(shù)值類型必須用:號開頭,所以假設(shè)我們累加結(jié)果為10,那么響應(yīng)給客戶端的結(jié)果就是10\r\n。
對應(yīng)我們的給出最后的代碼段:
//將累加后的結(jié)果返回給客戶端,按照RESP格式即 :數(shù)值\r\n,例如返回10 那么格式就是:10\r\n
reply := *shared.colon + strconv.FormatInt(value, 10) + *shared.crlf
addReply(c, &reply)
完整的代碼實現(xiàn)
我們來小結(jié)一下上述的實現(xiàn)思路:
- 鍵值對查詢與校驗。
- 數(shù)值類型轉(zhuǎn)換與越界判斷。
- 字符串類型強轉(zhuǎn)并基于取值范圍查看是否通過數(shù)值池獲取。
- 更新或覆蓋鍵值對。
- 將操作結(jié)果返回客戶端。
完整代碼如下:
func incrDecrCommand(c *redisClient, incr int64) {
var value int64
var oldValue int64
var newObj *robj
//查看鍵值對是否存在
o := lookupKeyWrite(c.db, c.argv[1])
//如果鍵值對存在且類型非字符串類型,直接響應(yīng)錯誤并返回
if o != nil && checkType(c, o, REDIS_STRING) {
return
}
/**
針對字符串類型的值進(jìn)行如下判斷的和轉(zhuǎn)換:
1. 如果為空,說明本次的key不存在,直接初始化一個空字符串,后續(xù)會直接初始化一個0值使用
2. 如果是字符串類型,則轉(zhuǎn)為字符串類型
3. 如果是數(shù)值類型,則先轉(zhuǎn)為字符串類型進(jìn)行后續(xù)的通用數(shù)值轉(zhuǎn)換操作保證一致性
*/
var s string
if o == nil {
s = ""
} else if isString(*o.ptr) {
s = (*o.ptr).(string)
} else {
s = strconv.FormatInt((*o.ptr).(int64), 10)
}
//進(jìn)行類型強轉(zhuǎn)為數(shù)值,如果失敗,直接輸出錯誤并返回
if getLongLongFromObjectOrReply(c, s, &value, nil) != REDIS_OK {
return
}
oldValue = value
if (incr < 0 && oldValue < 0 && incr < (math.MinInt64-oldValue)) ||
(incr > 0 && oldValue > 0 && incr > (math.MaxInt64-oldValue)) {
errReply := "increment or decrement would overflow"
addReplyError(c, &errReply)
return
}
//基于incr累加的值生成value
value += incr
//如果超常量池范圍則封裝一個對象使用
if o != nil &&
(value < 0 || value >= REDIS_SHARED_INTEGERS) &&
(value > math.MinInt64 || value < math.MaxInt64) {
newObj = o
i := interface{}(value)
o.ptr = &i
} else if o != nil { //如果對象存在,且累加結(jié)果沒超范圍則調(diào)用createStringObjectFromLongLong獲取常量對象
newObj = createStringObjectFromLongLong(value)
//將寫入結(jié)果覆蓋
dbOverwrite(c.db, c.argv[1], newObj)
} else { //從常量池獲取數(shù)值,然后添加鍵值對到數(shù)據(jù)庫中
newObj = createStringObjectFromLongLong(value)
dbAdd(c.db, c.argv[1], newObj)
}
//將累加后的結(jié)果返回給客戶端,按照RESP格式即 :數(shù)值\r\n,例如返回10 那么格式就是:10\r\n
reply := *shared.colon + strconv.FormatInt(value, 10) + *shared.crlf
addReply(c, &reply)
}
遞增遞減的復(fù)用
基于上述函數(shù)對應(yīng)的遞增指令I(lǐng)NCR就使用incrCommand,入?yún)?代表加1,而decrCommand則傳-1扣減即可:
func incrCommand(c *redisClient) {
//累加1
incrDecrCommand(c, 1)
}
func decrCommand(c *redisClient) {
//遞減1
incrDecrCommand(c, -1)
}
最終效果演示
最后,我們將服務(wù)啟動進(jìn)行測試,可以看到指令正常執(zhí)行:
127.0.0.1:6379> incr k1
(integer) 1
(4.50s)
127.0.0.1:6379> incr k1
(integer) 2
127.0.0.1:6379> incr k1
(integer) 3
127.0.0.1:6379> incr k1
(integer) 4
127.0.0.1:6379> incr k1
(integer) 5
127.0.0.1:6379> incr k1
(integer) 6
127.0.0.1:6379> decr k1
(integer) 5
127.0.0.1:6379> decr k1
(integer) 4
127.0.0.1:6379> decr k1
(integer) 3
127.0.0.1:6379> decr k1
(integer) 2
127.0.0.1:6379> decr k1
(integer) 1
127.0.0.1:6379> decr k1
(integer) 0
127.0.0.1:6379> decr k1
(integer) -1
127.0.0.1:6379>