写在文章开头
在之前的系列文章中,我们通过命令行模式完成了mini-redis
解析和处理指令的执行基调,这篇文章笔者将对mini-redis中存储字符串的set和get
指令的设计和实现进行分析讲解,希望对你了解mini-redis
有所帮助。
Hi,我是 sharkChili ,是个不断在硬核技术上作死的技术人,是 CSDN的博客专家 ,也是开源项目 Java Guide 的维护者之一,熟悉 Java 也会一点 Go ,偶尔也会在 C源码 边缘徘徊。写过很多有意思的技术博客,也还在研究并输出技术的路上,希望我的文章对你有帮助,非常欢迎你关注我的公众号: 写代码的SharkChili 。
因为近期收到很多读者的私信,所以也专门创建了一个交流群,感兴趣的读者可以通过上方的公众号获取笔者的联系方式完成好友添加,点击备注 “加群” 即可和笔者和笔者的朋友们进行深入交流。
详解mini-redis如何设计和实现字符串操作指令
设计思路
要想完全复刻redis
的字符串操作指令,我们就必须了解每个指令的作用和含义,先来说说set
指令,按照redis
官网的说明,set
指令大体有5种操作:
- 常规
set
操作:指令示例为set k v
将key
为k
,意为值为v的键值对存入客户端所设置的redis
数据库(默认为0)中。 - 参数
nx
:指令示例为set k v nx
,当k不存在redis
客户端所指向的数据库中时,将kv
键值对插入数据库。 - 参数
xx
:指令示例为set k v xx
即当k存在于redis
客户端所指向的数据库时,修改这个k的值。 - 参数
ex
:指令示例为set k v ex x
,通过参数ex
设置,存入数据库的key
会在x
秒后删除。 - 参数
px
:指令示例为set k v px x
,通过参数px
设置,存入数据库的key
会在x
毫秒后删除。
而get
指令就比较简单了,基于redis-cli
所指向的数据库查询要查询的key
是否存在,如果存在则返回字符串,反之返回nil
。
了解了所有指令的含义之后,我们就来介绍介绍mini-redis
的实现思路了,对于set
指令,mini-redis
通过字符串解析后得到set
指令,从将参数传入到函数setCommand
进行处理。而setCommand
处理逻辑比较简单,由于索引0、1、2分别对应set key value
,所以我们只需从索引3开始遍历查看当前传入的指令是否存在特殊参数(如nx、ex等)
并进行标记。
以下图为例,我们redis-cli
键入的指令为set k v ex 3
,跳过argv
数组中0、1、2几个常规set指令必传的字符串,得到ex和键值对存活的数值3,这意味着存储的kv
会在3秒后过期。
解析到键值对kv
之后,定位到当前客户端指针所指向的数据库db
(默认为0),先将其存储到数据库结构体记录键值对的字典dict
中,然后基于ex
后面的参数得到期时间并以k作为键,到期时间作为value
将其其存到expires
字典中:
后续我们在通过get
指令获取参数时,首先会看到expires
字典中查看这个key
是否过期,如果过期则将dict
字典中的键值对删除,返回给客户端nil
,反之返回键值对的结果:
落地set指令
经过上述的分析,我们就可以进行代码落地了,这里笔者给出mini-redis
的set
指令处理函数setCommand
,可以看到笔者从索引3开始遍历特殊参数,基于各个字符串例如nx
、xx
进行指令特殊处理标识,如果遇到ex
和px
则读取后一位参数,以ex为例就算将unit单位设置为秒然后读取后一位参数值,最后将这些参数全部传入setGenericCommand
进行内存持久化操作:
func setCommand(c *redisClient) {
var j uint64
var expire string
unit := UNIT_SECONDS
flags := REDIS_SET_NO_FLAGS
//traverse the arguments after setting the key-value pair in a set.
for j = 3; j < c.argc; j++ {
a := c.argv[j]
var next string
if j == c.argc-1 {
next = ""
} else {
next = c.argv[j+1]
}
// if the string "nx" is included, mark through bitwise operations that the current key can only be set when it does not exist.
if strings.ToLower(a) == "nx" {
flags |= REDIS_SET_NX
} else if strings.ToLower(a) == "xx" { //if "xx" is included, mark the flags to indicate that the key can only be set if it already exists.
flags |= REDIS_SET_XX
} else if strings.ToLower(a) == "ex" { //if it is "ex", set the unit to seconds and read the next parameter.
unit = UNIT_SECONDS
expire = next
j++
} else if strings.ToLower(a) == "px" { //if it is "px", set the unit to milliseconds and read the next parameter.
unit = UNIT_MILLISECONDS
expire = next
j++
} else { // Treat all other cases as exceptions.
addReply(c, shared.syntaxerr)
return
}
}
//pass the key, value, instruction identifier flags, and expiration time unit into `setGenericCommand` for memory persistence operation.
setGenericCommand(c, flags, c.argv[1], c.argv[2], expire, unit, "", "")
}
对应我们给出setGenericCommand
指令的核心流程,它会基于上一步的入参判断是否存在expire ,如果存在则将其转为整数,后续会基于当前时间计算到期时间并以传入key
作为键,到期时间作为值存入expires
字典中。随后在进行标记判断,通过上一步位运算的标记,只有符合以下某种情况则不进行键值对插入:
- 指令包含nx且调用
lookupKeyWrite
查看当前数据库包含这个键值对。 - 指令包含xx且调用
lookupKeyWrite
查看数据库不包含这个键值对。
完成这些校验之后,进行键值对、到期时间信息写入内存中:
func setGenericCommand(c *redisClient, flags int, key string, val string, expire string, unit int, ok_reply string, abort_reply string) {
//initialize a pointer to record the expiration time in milliseconds.
var milliseconds *int64
milliseconds = new(int64)
//if `expire` is not empty, parse it as an int64 and store it in `milliseconds`.
if expire != "" {
if getLongLongFromObjectOrReply(c, expire, milliseconds, "") != REDIS_OK {
return
}
if unit == UNIT_SECONDS {
*milliseconds = *milliseconds * 1000
}
}
/**
the following two cases will no longer undergo key-value persistence operations:
1. if the command contains "nx" and the data exists for this value.
2. if the command contains "xx" and the data for this value does not exist.
*/
if (flags&REDIS_SET_NX > 0 && *lookupKeyWrite(c.db, key) != nil) ||
(flags&REDIS_SET_XX > 0 && *lookupKeyWrite(c.db, key) == nil) {
addReply(c, shared.nullbulk)
return
}
//if `expire` is not empty, add the converted value to the current time to obtain the expiration time. Then,
//use the passed key as the key and the expiration time as the value to store in the `expires` dictionary.
if expire != "" {
c.db.expires[key] = time.Now().UnixMilli() + *milliseconds
}
//store the key-value pair in a dictionary.
c.db.dict[key] = val
addReply(c, shared.ok)
}
而判断是否存在的函数lookupKeyWrite
逻辑比较简单,调用expireIfNeeded
查看当前key是否过期如果过期则删除,然后在调用lookupKey
返回查询结果:
func lookupKeyWrite(db *redisDb, key string) *interface{} {
//check if the key has expired, and if so, delete it.
expireIfNeeded(db, key)
//query the dictionary for the value corresponding to the key.
return lookupKey(db, key)
}
对此我们也给出expireIfNeeded
的逻辑,可以看到我们会从expires
拿到这个key
的过期时间,如果这个过期时间大于当前时间,则说明key没有过期直接返回,反之则说明key
过期,调用deDelete
将dict中的key
删除:
func expireIfNeeded(db *redisDb, key string) int {
//get the expiration time of the key.
when, exists := db.expires[key]
if !exists {
return 0
}
if when < 0 {
return 0
}
now := time.Now().UnixMilli()
//if the current time is less than the expiration time, it means the current key has not expired, so return directly.
if now < when {
return 0
}
//delete expired keys.
deDelete(db, key)
return 1
}
落地get指令
get
指令就比较简单了,直接调用getGenericCommand
其内部调用lookupKeyReadOrReply
检查当前key
是否到期,如果到期则将这个键值对删除,然后再到dict
中查询键值对是否存在,如果存在则调用addReplyBulk
将结果写给客户端:
func getCommand(c *redisClient) {
getGenericCommand(c)
}
func getGenericCommand(c *redisClient) int {
//check if the key exists, and if it does not, return a null bulk response from the constant values.
o := lookupKeyReadOrReply(c, c.argv[1], &shared.nullbulk)
if *o == nil {
return REDIS_OK
}
//return the value to the client if it exists.
val := (*o).(string)
addReplyBulk(c, &val)
return REDIS_OK
}
而lookupKeyRead
内部逻辑也是先检查key
是否到期,然后调用lookupKey
查询返回值:
func lookupKeyRead(db *redisDb, key string) *interface{} {
//check if the key has expired and delete it.
expireIfNeeded(db, key)
val := lookupKey(db, key)
return val
}
测试落地结果
我们启动mini-redis
,通过redis-cli
键入常规字符串设置指令,调用get
可以得到存储的结果:
127.0.0.1:6379> set k v
OK
127.0.0.1:6379> get k
"v"
设置时效后,key
到期后查询就返回nil
:
127.0.0.1:6379> set key v ex 10
OK
127.0.0.1:6379> get key
"v"
127.0.0.1:6379> get key
(nil)
使用nx
插入存在的键值对,直接返回nil
:
127.0.0.1:6379> set k v nx
(nil)
127.0.0.1:6379>
小结
自此我们将mini-redis
中字符串操作指令的实现思路和落地代码核心部分都进行了详细分析,希望对你阅读笔者的源码有所帮助。这里笔者也给出源码地址:
https://github.com/shark-ctrl/mini-redis
我是 sharkchili ,CSDN Java 领域博客专家,mini-redis的作者,我想写一些有意思的东西,希望对你有帮助,如果你想实时收到我写的硬核的文章也欢迎你关注我的公众号: 写代码的SharkChili 。
因为近期收到很多读者的私信,所以也专门创建了一个交流群,感兴趣的读者可以通过上方的公众号获取笔者的联系方式完成好友添加,点击备注 “加群” 即可和笔者和笔者的朋友们进行深入交流。