实现 dict
我们先来定义 redis 内部的数据结构 dict,dict 是一个 key-value 的数据结构,key 是一个字符串,value 是一个 interface{}
dict 会有很多方法,比如 Get、Put 等,我们先来定义 dict 的接口
type Consumer func(key string, val interface{}) bool
type Dict interface {
Get(key string) (val interface{}, exists bool)
Put(key string, val interface{}) (result int) // 存进去回复 1,没存进去回复 0
PutIfAbsent(key string, val interface{}) (result int) // 如果 key 不存在,才能设置,如果 key 存在回复 0,不存在回复 1
PutIfExists(key string, val interface{}) (result int) // 如果 key 存在,才能设置,如果 key 存在回复 1,不存在回复 0
Remove(key string) (result int)
ForEach(consumer Consumer) // 遍历所有键值对
Keys() []string // 获取所有的 key
RandomKeys(limit int) []string // 返回 n 个 key
RandomDistinctKeys(limit int) []string // 返回 n 个不重复的 key
Len() int
Clear() // 清空数据结构
}
对于 Dict 的实现,我们需要用并发安全的 map 去实现,也就是 sync.Map
type SyncDict struct {
m sync.Map
}
func MakeSyncDict() *SyncDict {
return &SyncDict{}
}
然后实现 Dict 接口
Get
从 sync.Map 中获取值,sync.Map 获取值的方法是 Load
func (dict *SyncDict) Get(key string) (val interface{}, exists bool) {
val, ok := dict.m.Load(key)
return val, ok
}
Len
通过 Range 遍历 Sync.Map 中的所有键值对,累加遍历的次数,得到 dict 的长度
func (dict *SyncDict) Len() int {
length := 0
dict.m.Range(func(key, value any) bool {
length++
return true
})
return length
}
Put
Put 方法是往 sync.Map 中存值,sync.Map 存值的方法是 Store,如果设置成功,返回 1,否则返回 0
func (dict *SyncDict) Put(key string, val interface{}) (result int) {
_, existed := dict.m.Load(key)
dict.m.Store(key, val)
if existed {
return 0
}
return 1
}
PutIfAbsent
PutIfAbsent 方法是如果 key 不存在,才能设置,如果 key 存在回复 0,不存在回复 1
func (dict *SyncDict) PutIfAbsent(key string, val interface{}) (result int) {
_, existed := dict.m.Load(key)
if existed {
return 0
}
dict.m.Store(key, val)
return 1
}
PutIfExist
PutIfExist 方法是如果 key 存在,才能设置,如果 key 存在回复 1,不存在回复 0
func (dict *SyncDict) PutIfExists(key string, val interface{}) (result int) {
_, existed := dict.m.Load(key)
if existed {
dict.m.Store(key, val)
return 1
}
return 0
}
Remove
Remove 方法是删除 key,如果 key 存在,返回 1,否则返回 0,sync.Map 删除值的方法是 Delete
func (dict *SyncDict) Remove(key string) (result int) {
_, existed := dict.m.Load(key)
dict.m.Delete(key)
if existed {
return 1
}
return 0
}
ForEach
ForEach 方法是遍历所有键值对,sync.Map 的遍历方法是 Range,遍历的时候调用 Consumer 函数
func (dict *SyncDict) ForEach(consumer Consumer) {
dict.m.Range(func(key, value any) bool {
consumer(key.(string), value)
return true
})
}
Keys
Keys 方法是获取所有的 key,刚开始初始化时,通过 dict.Len() 设置切片的长度,dict.Len 方法就是上面实现的 Len 方法
func (dict *SyncDict) Keys() []string {
// 用 dict.Len() 设置切片的长度
result := make([]string, dict.Len())
i := 0
dict.m.Range(func(key, value any) bool {
// 遍历时,将 key 转为 string 类型,存入切片
result[i] = key.(string)
i++
return true
})
return result
}
RandomKeys
RandomKeys 方法接收一个参数 limit,返回 limit 个 key
func (dict *SyncDict) RandomKeys(limit int) []string {
// 用 limit 设置切片的长度
result := make([]string, dict.Len())
// 遍历 limit
for i := 0; i < limit; i++ {
// 通过随机数获取 key
dict.m.Range(func(key, value any) bool {
result[i] = key.(string)
// return false 的作用是只遍历一次,这样就保证一次 for 循环时,随机获取其中一个 key
return false
})
}
return result
}
RandomDistinctKeys
RandomDistinctKeys 方法接收一个参数 limit,返回 limit 个不重复的 key
go 的 forEach 是无序的,所以需要在外面用 i 记录下遍历了多次次,如果 i 等于 limit,就返回 false,否则一直遍历下去
func (dict *SyncDict) RandomDistinctKeys(limit int) []string {
result := make([]string, dict.Len())
i := 0
dict.m.Range(func(key, value any) bool {
result[i] = key.(string)
i++
// 如果 i 等于 limit 说明遍历了 limit 次,就返回 false,结束遍历
if i == limit {
return false
}
return true
})
return result
}
Clear
Clear 方法是清空数据结构,我们可以直接将 dict 重新赋值为一个新的 dict
这里要注意的是要用 *dict,用 dict 不会修改原来的 dict
func (dict *SyncDict) Clear() {
// dict = MakeSyncDict(),这里相当于新建了一个 dict 指针,原来的 dict 不会有任何变化
*dict = *MakeSyncDict()
}
实现 command
dict 是 redis 最底层的数据结构,它的上一层是 db,db 就是一个分数据库(redis 一共有 16 个数据库)
type DB struct {
index int // 数据的编号
data dict.Dict // 数据类型
}
func makeDB() *DB {
return &DB{
data: dict.MakeSyncDict(),
}
}
声明两数据类型,第一个类型是一个方法 ExecFunc,它的作用是 redis 中所有的指令的类型
CmdLine 是一个二维的字节切片
type ExecFunc func(db *DB, args [][]byte) resp.Reply
type CmdLine = [][]byte
每一个 command 会有一个执行方法,也就是说每一个指令,比如 Ping、GET 都有一个 command 的结构体,这个结构体里面都有一个 exector 的方法
外面就实现 exector 这个方法,然后施加到 db 上
arity 是参数的数量
type command struct {
exector ExecFunc // 执行方法
arity int // 参数数量
}
然后在定义一个 cmdTable,它的类型就是一个 command 的 map,用来存放所有的 command
var cmdTable = make(map[string]*command)
然后在实现一个注册指令的方法
func RegisterCommand(name string, exector ExecFunc, arity int) {
name = strings.ToLower(name)
cmdTable[name] = &command{exector: exector, arity: arity}
}
实现 DB.Exec
Exec 方法是执行指令的方法,它接收一个 CmdLine 参数,CmdLine 就是上面定义的 [][]byte
func (db *DB) Exec(c resp.Connection, cmdLine CmdLine) resp.Reply {
// 拿到指令的名字,比如 GET、SET
cmdName := strings.ToLower(string(cmdLine[0]))
// 从 cmdTable 中拿到指令的结构体
cmd, ok := cmdTable[cmdName]
// 如果指令不存在,返回错误
if !ok {
return reply.MakeErrReply("ERR unknown command " + cmdName)
}
// 验证参数的数量,如果参数数量不对,返回错误
if !validateArity(cmd.arity, cmdLine) {
return reply.MakeArgNumErrReply(cmdName)
}
// 拿到指令的执行方法
fun := cmd.exector
// 执行指令,传入 db 和指令的参数
// SET key value,cmdLine[1:] 就是 key 和 value
return fun(db, cmdLine[1:])
}
validateArity
validateArity 方法是验证指令传入的参数数量是否正确
redis 的指令有两种情况
- 固定参数,比如
SET Key Value,所以参数数量必须等于arity - 不固定参数,比如
EXISTS k1 k2 ...,这种参数不固定的,arity会赋值-2,所以参数数量必须大于等于-arity
func validateArity(arity int, cmdArgs [][]byte) bool {
argNum := len(cmdArgs)
// 固定参数,比如 SET Key Value,所以参数数量必须等于 arity
if arity > 0 {
return argNum == arity
}
// 不固定参数,比如 EXISTS k1 k2 ...,这种参数不固定的,arity 会赋值 -2,所以参数数量必须大于等于 -arity
return argNum >= -arity
}
GetEntity
这是一个 db 的方法,是对 dict 的封装
GetEntity 方法是从 db 中通过 key 获取对应的 value
func (db *DB) GetEntity(key string) (*databaseface.DataEntity, bool) {
// 调用 dict 的 Get 方法,通过 key 获取 value
raw, ok := db.data.Get(key)
if !ok {
return nil, false
}
// 底层 dict 存的是 interface{},所以需要转为 *DataEntity
entity, _ := raw.(*databaseface.DataEntity)
return entity, true
}
PutEntity
PutEntity 方法是往 db 中存入 key 和 value,dict 的 Put 也是返回 1 或 0,所以这里也是返回 1 或 0
func (db *DB) PutEntity(key string, entity *databaseface.DataEntity) int {
return db.data.Put(key, entity)
}
PutIfExists
PutIfExists 方法是如果 key 存在,才能设置,如果 key 存在回复 1,不存在回复 0
func (db *DB) PutIfExists(key string, entity *databaseface.DataEntity) int {
return db.data.PutIfExists(key, entity)
}
PutIfAbsent
PutIfAbsent 方法是如果 key 不存在,才能设置,如果 key 存在回复 0,不存在回复 1
func (db *DB) PutIfAbsent(key string, entity *databaseface.DataEntity) int {
return db.data.PutIfAbsent(key, entity)
}
Remove
Remove 方法是删除 key,如果 key 存在,返回 1,否则返回 0
func (db *DB) Remove(key string) int {
return db.data.Remove(key)
}
Removes
Removes 是删除一组的key
通过遍历 keys,调用上面实现的 Remove 方法,删除每个 key
func (db *DB) Removes(keys ...string) (deleted int) {
deleted = 0
// 遍历 keys,删除每一个 key
for _, key := range keys {
// 先判断 key 是否存在,存在就删除,并且 deleted++
_, exists := db.data.Get(key)
if exists {
db.Remove(key)
deleted++
}
}
// 返回删除的数量
return deleted
}
Flush
Flush 方法是清空数据结构,调用 dict.Clear 方法可以实现清空数据库的操作
func (db *DB) Flush() {
db.data.Clear()
}
实现 Ping 指令
Ping 指令是 redis 中最简单的指令,只是返回一个 PONG,所以我们先来实现 Ping 指令
Ping 指令是比较简单的,直接调用 reply.MakePongReply 方法,返回一个 PONG 的回复
func Ping(db *DB, args [][]byte) resp.Reply {
return reply.MakePongReply()
}
在这个文件中定义一个 init 方法,文件加载时会自动执行这个方法
这个方法会注册 Ping 指令
func init() {
RegisterCommand("ping", Ping, 1) // 注册 Ping 指令,参数数量 arity 是 1
}
实现 Keys 相关指令
Keys 相关指令有:DEL、EXISTS、KEYS、FLUSHDB、RENAME、RENAMENX 这几个
DEL
DEL 指令可以删除一堆 key,所以参数数量是不固定的,arity 是 -2
// DEL K1 K2 K3
func DEL(db *DB, args [][]byte) resp.Reply {
keys := make([]string, len(args))
for i, v := range args {
keys[i] = string(v)
}
deleted := db.Removes(keys...)
return reply.MakeIntReply(int64(deleted))
}
然后将 DEL 指令注册到 cmdTable 中,arity 是 -2,代表参数数量是不固定的
RegisterCommand("DEL", DEL, -2)
EXISTS
EXISTS 指令是判断 key 是否存在,参数数量是不固定的,arity 是 -2
// EXISTS K1 K2 K3 ...
func EXISTS(db *DB, args [][]byte) resp.Reply {
result := int64(0)
for _, arg := range args {
key := string(arg)
_, exists := db.GetEntity(key)
if exists {
result++
}
}
return reply.MakeIntReply(result)
}
然后将 EXISTS 指令注册到 cmdTable 中,arity 是 -2,代表参数数量是不固定的
RegisterCommand("EXISTS", EXISTS, -2)
FLUSHDB
FLUSHDB 指令是清空数据库,后面不需要参数,如果有参数,直接忽略
func FLUSHDB(db *DB, args [][]byte) resp.Reply {
db.Flush()
return reply.MakeOkReply()
}
然后将 FLUSHDB 指令注册到 cmdTable 中,忽略后面的参数,所以 arity 是 -1
RegisterCommand("FLUSHDB", FLUSHDB, -1)
TYPE
TYPE 指令在 redis 中是获取一个 key 的数据类型,这里我们只有 string 类型,所以直接返回 string
// TYPE K1
func TYPE(db *DB, args [][]byte) resp.Reply {
key := string(args[0])
entity, exists := db.GetEntity(key)
// 如果 key 不存在,返回 none
if !exists {
return reply.MakeStatusReply("none") // TCP :none\r\n
}
switch entity.Data.(type) {
// 如果是 []byte 类型,返回 string
case []byte:
return reply.MakeStatusReply("string")
}
return reply.MakeUnknownErrReply()
}
然后将 TYPE 指令注册到 cmdTable 中,arity 是 2
RegisterCommand("TYPE", TYPE, 2) // TYPE K1
RENAME
RENAME 指令是重命名 key,RENAME K1 K2,将 K1 重命名为 K2,如何设置成功,返回 OK,否则返回错误
// RENAME K1 K2
func RENAME(db *DB, args [][]byte) resp.Reply {
// 从用户输入的命令中拿到 K1
src := string(args[0])
// 从用户输入的命令中拿到 K2
dest := string(args[1])
// 通过 K1 获取 entity
entity, exists := db.GetEntity(src)
// 如果 K1 不存在,返回错误
if !exists {
return reply.MakeErrReply("no such key")
}
// 将 K2 的值设置为 K1 的 entity
db.PutEntity(dest, entity)
// 删除 K1
db.Remove(src)
return reply.MakeOkReply()
}
然后将 RENAME 指令注册到 cmdTable 中,arity 是 3
RegisterCommand("RENAME", RENAME, 3) // RENAME K1 K2
RENAMENX
RENAMENX 指令是重命名 key,RENAMENX K1 K2,将 K1 重命名为 K2
RENAMENX 和 RENAME 的区别是,如果 K2 存在,RENAMENX 不会重命名(返回 0,设置成功了返回 1),RENAME 会覆盖
// RENAMENX K1 K2
func RENAMENX(db *DB, args [][]byte) resp.Reply {
// 从用户输入的命令中拿到 K1
src := string(args[0])
// 从用户输入的命令中拿到 K2
dest := string(args[1])
_, ok := db.GetEntity(dest)
// 如果 K2 存在,啥也不干
if ok {
return reply.MakeIntReply(0)
}
entity, exists := db.GetEntity(src)
// 如果 K1 不存在,返回错误
if !exists {
return reply.MakeErrReply("no such key")
}
// 将 K2 的值设置为 K1 的 entity
db.PutEntity(dest, entity)
// 删除 K1
db.Remove(src)
return reply.MakeIntReply(1)
}
然后将 RENAMENX 指令注册到 cmdTable 中,arity 是 3
RegisterCommand("RENAMENX", RENAMENX, 3) // RENAMENX K1 K2
KEYS
KEYS 指令是获取所有的 key,KEYS *,返回所有的 key
KEYS 指令是通过 wildcard 包来实现的,wildcard 包是一个通配符的包,可以通过 * 来匹配所有的 key
// KEYS *
func KEYS(db *DB, args [][]byte) resp.Reply {
// 通过 wildcard 包来实现通配符
pattern := wildcard.CompilePattern(string(args[0]))
// 存放所有通配符的 key
result := make([][]byte, 0)
db.data.ForEach(func(key string, val interface{}) bool {
// 如果 key 匹配通配符,就存入 result
if pattern.IsMatch(key) {
// 将 key 转为 []byte 类型,存入 result
result = append(result, []byte(key))
}
// 返回 true,继续遍历
return true
})
// 返回所有匹配的 key
return reply.MakeMultiBulkReply(result)
}
然后将 KEYS 指令注册到 cmdTable 中,arity 是 2
RegisterCommand("KEYS", KEYS, 2) // KEYS *
实现 String 相关指令
String 相关指令有:GET、SET、SETNX、GETSET、STRLEN 这几个
GET
GET 指令是获取 key 的值,GET K1,返回 K1 的值
// GET K1
func GET(db *DB, args [][]byte) resp.Reply {
// 从用户输入的命令中拿到 K1
key := string(args[0])
// 通过 K1 获取 entity
entity, exists := db.GetEntity(key)
// 如果 K1 不存在,返回 nil
if !exists {
return reply.MakeNullBulkReply()
}
bytes := entity.Data.([]byte)
// 将 data 包装成 MakeBulkReply
return reply.MakeBulkReply(bytes)
}
然后将 GET 指令注册到 cmdTable 中,arity 是 2
RegisterCommand("GET", GET, 2)
SET
SET 指令是设置 key 的值,SET K1 V1,将 K1 的值设置为 V1
// SET K1 v
func SET(db *DB, args [][]byte) resp.Reply {
// 从用户输入的命令中拿到 K1
key := string(args[0])
// 从用户输入的命令中拿到 v
value := args[1]
// 将 v 存入 entity
entity := &databaseface.DataEntity{Data: value}
// 将 K1 和 entity 存入 db
db.PutEntity(key, entity)
// 返回 OK
return reply.MakeOkReply()
}
然后将 SET 指令注册到 cmdTable 中,arity 是 3
RegisterCommand("SET", SET, 3)
SETNX
SETNX 指令和 SET 指令的区别是,SETNX 指令是如果 key 不存在,才能设置,如果 key 存在回复 0,不存在回复 1
// SETNX K1 v
func SETNX(db *DB, args [][]byte) resp.Reply {
// 从用户输入的命令中拿到 K1
key := string(args[0])
// 从用户输入的命令中拿到 v
value := args[1]
// 将 v 存入 entity
entity := &databaseface.DataEntity{Data: value}
// 调用 PutIfAbsent,将 K1 和 entity 存入 db,如果 key 不存在,返回 1,存在返回 0
result := db.PutIfAbsent(key, entity)
return reply.MakeIntReply(int64(result))
}
然后将 SETNX 指令注册到 cmdTable 中,arity 是 3
RegisterCommand("SETNX", SETNX, 3)
GETSET
GETSET 指令是设置 key 的值,并且返回 key 的旧值,GETSET K1 V1,将 K1 的值设置为 V1,返回 K1 的旧值
// GETSET K1 v1
func GETSET(db *DB, args [][]byte) resp.Reply {
// 从用户输入的命令中拿到 K1
key := string(args[0])
// 从用户输入的命令中拿到 v1
value := args[1]
// 通过 K1 获取 entity
entity, exists := db.GetEntity(key)
// 将 v1 转为 []byte 存入 db 中
db.PutEntity(key, &databaseface.DataEntity{Data: value})
// 如果 K1 不存在,返回 nil
if !exists {
return reply.MakeNullBulkReply()
}
// 返回 K1 的旧值
return reply.MakeBulkReply(entity.Data.([]byte))
}
然后将 GETSET 指令注册到 cmdTable 中,arity 是 3
RegisterCommand("GETSET", GETSET, 3)
STRLEN
STRLEN 指令是获取 key 的长度,STRLEN K1,返回 K1 的长度
// STRLEN K
func STRLEN(db *DB, args [][]byte) resp.Reply {
// 从用户输入的命令中拿到 K
key := string(args[0])
// 通过 K 获取 entity
entity, exists := db.GetEntity(key)
// 如果 K 不存在,返回 null
if !exists {
return reply.MakeNullBulkReply()
}
bytes := entity.Data.([]byte)
// 返回 K 的长度
return reply.MakeIntReply(int64(len(bytes)))
}
然后将 STRLEN 指令注册到 cmdTable 中,arity 是 2
RegisterCommand("STRLEN", STRLEN, 2)
实现核心 Database
redis 基本指令都实现了之后,还差一个最核心的内核
我们 redis 当成一个数据库,它内部有 16 个分数据库,每个数据库都是一个 DB,所以我们需要实现一个 Database,它是一个 DB 的集合
type StandaloneDatabase struct {
dbSet []*DB
}
这个结构体需要实现 Database 接口:
type Database interface {
Exec(client resp.Connection, args [][]byte) resp.Reply // 执行指令:执行的客户端 client,执行的指令 args,返回的是 Reply
Close() // 关闭数据库
AfterClientClose(c resp.Connection) // 数据库关闭之后的善后工作
}
这个接口最核心的是 Exec 方法,它接收一个 client 和 args,返回一个 Reply
这个方法主要的作用就是调用分数据库的 db.Exec 方法,然后返回结果
func (database *StandaloneDatabase) Exec(client resp.Connection, args [][]byte) resp.Reply {
// 用来 recover panic
defer func() {
if err := recover(); err != nil {
logger.Error(err)
}
}()
// 拿到第一个参数,也就是指令的名字
cmdName := strings.ToLower(string(args[0]))
// 如果指令是 select,切换数据库
if cmdName == "select" {
if len(args) != 2 {
return reply.MakeArgNumErrReply("select")
}
execSelect(client, database, args[1:])
}
// 从 client 中拿到 dbIndex
dbIndex := client.GetDBIndex()
// 通过 dbIndex 拿到 db
db := database.dbSet[dbIndex]
// 执行指令
return db.Exec(client, args)
}
其中 execSelect 方法是切换数据库的方法
// select 2
func execSelect(c resp.Connection, database *StandaloneDatabase, args [][]byte) resp.Reply {
// 从用户输入的命令中拿到 dbIndex
dbIndex, err := strconv.Atoi(string(args[0]))
// 如果 dbIndex 不是数字,返回错误
if err != nil {
return reply.MakeErrReply("ERR invalid DB index")
}
// 如果 dbIndex 超出了数据库的范围,返回错误
if dbIndex >= len(database.dbSet) {
return reply.MakeErrReply("ERR db index is out of range")
}
// 设置 dbIndex
c.SelectDB(dbIndex)
// 返回 OK
return reply.MakeOkReply()
}
然后在实现 StandAloneDatabase 的 New 方法
func NewStandaloneDatabase() *StandaloneDatabase {
database := &StandaloneDatabase{}
if config.Properties.Databases == 0 {
config.Properties.Databases = 16
}
// 初始化 16 个数据库
database.dbSet = make([]*DB, config.Properties.Databases)
// 初始化每一个数据库
for i := range database.dbSet {
db := makeDB()
db.index = i
database.dbSet[i] = db
}
return database
}
测试
运行 main.go,然后用 nc 连接
echo -ne '*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n' | nc 127.0.0.1 3000