Building a Simple In-Memory Redis Clone with Go

This article walks through the third part of an eleven‑article series that implements a functional Redis‑compatible in‑memory database in Go, detailing how the PING, AUTH, SELECT, SET and GET commands are parsed, routed and executed with concrete code examples.

Nullbody Notes
Nullbody Notes
Nullbody Notes
Building a Simple In-Memory Redis Clone with Go

Third Part – EasyRedis In‑Memory Database

The series aims to build a usable Redis service called EasyRedis by dissecting Redis internals. This third article concentrates on the in‑memory data store, using five common commands as entry points.

PING Command

The Exec function extracts the command name from the parsed RESP array and dispatches to Ping when the name is "ping". Ping returns protocal.NewPONGReply() for no arguments, echoes the single argument with protocal.NewBulkReply, and returns an argument‑number error for other cases.

func (e *Engine) Exec(c *connection.KeepConnection, redisCommand [][]byte) (result protocal.Reply) {
    commandName := strings.ToLower(string(redisCommand[0]))
    if commandName == "ping" {
        return Ping(redisCommand[1:])
    }
    // ... other commands
}

func Ping(redisArgs [][]byte) protocal.Reply {
    if len(redisArgs) == 0 {
        return protocal.NewPONGReply()
    } else if len(redisArgs) == 1 {
        return protocal.NewBulkReply(redisArgs[0])
    }
    return protocal.NewArgNumErrReply("ping")
}

AUTH Command

When a password is required, the client must send AUTH <password>. The Exec function routes to Auth, which validates the argument count, checks the global configuration RequirePass, compares the supplied password, stores it in the connection object, and returns an OK reply. Subsequent commands call checkPasswd to verify the stored password.

func (e *Engine) Exec(c *connection.KeepConnection, redisCommand [][]byte) (result protocal.Reply) {
    commandName := strings.ToLower(string(redisCommand[0]))
    if commandName == "auth" {
        return Auth(c, redisCommand[1:])
    }
    if !checkPasswd(c) {
        return protocal.NewGenericErrReply("Authentication required")
    }
    // ... other commands
}

func Auth(c *connection.KeepConnection, redisArgs [][]byte) protocal.Reply {
    if len(redisArgs) != 1 {
        return protocal.NewArgNumErrReply("auth")
    }
    if conf.GlobalConfig.RequirePass == "" {
        return protocal.NewGenericErrReply("No authorization is required")
    }
    password := string(redisArgs[0])
    if conf.GlobalConfig.RequirePass != password {
        return protocal.NewGenericErrReply("Auth failed, password is wrong")
    }
    c.SetPassword(password)
    return protocal.NewOkReply()
}

func checkPasswd(c *connection.KeepConnection) bool {
    if conf.GlobalConfig.RequirePass == "" {
        return true
    }
    return c.GetPassword() == conf.GlobalConfig.RequirePass
}

SELECT Command

The server supports multiple logical databases. Engine holds a slice of atomic *DB pointers, created in NewEngine. execSelect parses the index, validates the range against conf.GlobalConfig.Databases, and stores the selected index in the connection.

func NewEngine() *Engine {
    engine := &Engine{}
    engine.dbSet = make([]*atomic.Value, conf.GlobalConfig.Databases)
    for i := 0; i < conf.GlobalConfig.Databases; i++ {
        db := newDB()
        db.SetIndex(i)
        dbset := &atomic.Value{}
        dbset.Store(db)
        engine.dbSet[i] = dbset
    }
    return engine
}

func execSelect(c *connection.KeepConnection, redisArgs [][]byte) protocal.Reply {
    if len(redisArgs) != 1 {
        return protocal.NewArgNumErrReply("select")
    }
    dbIndex, err := strconv.ParseInt(string(redisArgs[0]), 10, 64)
    if err != nil {
        return protocal.NewGenericErrReply("invaild db index")
    }
    if dbIndex < 0 || dbIndex >= int64(conf.GlobalConfig.Databases) {
        return protocal.NewGenericErrReply("db index out of range")
    }
    c.SetDBIndex(int(dbIndex))
    return protocal.NewOkReply()
}

SET Command

After selecting a database, the SET command is processed by cmdSet located in engine/string.go. The command ultimately calls db.PutEntity, which stores a DataEntity (wrapping the raw value) into a concurrent dictionary.

func (e *Engine) Exec(c *connection.KeepConnection, redisCommand [][]byte) (result protocal.Reply) {
    dbIndex := c.GetDBIndex()
    db, errReply := e.selectDB(dbIndex)
    if errReply != nil {
        return errReply
    }
    return db.Exec(c, redisCommand)
}

func (db *DB) Exec(c *connection.KeepConnection, redisCommand [][]byte) protocal.Reply {
    return db.execNormalCommand(c, redisCommand)
}

func (db *DB) execNormalCommand(c *connection.KeepConnection, redisCommand [][]byte) protocal.Reply {
    cmdName := strings.ToLower(string(redisCommand[0]))
    command, ok := commandCenter[cmdName]
    if !ok {
        return protocal.NewGenericErrReply("unknown command '" + cmdName + "'")
    }
    return command.execFunc(db, redisCommand[1:])
}

// In engine/string.go
func cmdSet(db *DB, args [][]byte) protocal.Reply {
    // parse arguments according to https://redis.io/commands/set/
    // ...
    db.PutEntity(key, &payload.DataEntity{RedisObject: value})
    return protocal.NewOkReply()
}

func (db *DB) PutEntity(key string, entity *payload.DataEntity) int {
    return db.dataDict.Put(key, entity)
}

GET Command

The GET implementation validates a single argument, retrieves the stored DataEntity via db.getStringObject, checks that the underlying object is a byte slice, and returns it as a bulk reply.

func cmdGet(db *DB, args [][]byte) protocal.Reply {
    if len(args) != 1 {
        return protocal.NewSyntaxErrReply()
    }
    key := string(args[0])
    bytes, reply := db.getStringObject(key)
    if reply != nil {
        return reply
    }
    return protocal.NewBulkReply(bytes)
}

func (db *DB) getStringObject(key string) ([]byte, protocal.Reply) {
    payload, exist := db.GetEntity(key)
    if !exist {
        return nil, protocal.NewNullBulkReply()
    }
    bytes, ok := payload.RedisObject.([]byte)
    if !ok {
        return nil, protocal.NewWrongTypeErrReply()
    }
    return bytes, nil
}

func (db *DB) GetEntity(key string) (*payload.DataEntity, bool) {
    val, exist := db.dataDict.Get(key)
    if !exist {
        return nil, false
    }
    dataEntity, ok := val.(*payload.DataEntity)
    if !ok {
        return nil, false
    }
    return dataEntity, true
}

Concurrent Dictionary Implementation

The in‑memory store uses a sharded concurrent dictionary. Each shard contains a map[string]interface{} protected by a sync.RWMutex. The top‑level ConcurrentDict holds a slice of shards and a mask for fast indexing.

type ConcurrentDict struct {
    shds  []*shard
    mask  uint32
    count *atomic.Int32
}

type shard struct {
    m  map[string]interface{}
    mu sync.RWMutex
}

Thus the memory database essentially operates on Go maps with proper locking to achieve thread‑safe access.

Demo

Demo diagram
Demo diagram

The article concludes by encouraging readers to study the commented source code for deeper understanding.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

BackendconcurrencyRedisGodata-structuresin-memory database
Nullbody Notes
Written by

Nullbody Notes

Go backend development, learning open-source project source code together, focusing on simplicity and practicality.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.