Build Your Own Redis
Target Audience: Mid-Level Engineers (2-5 years experience)Language: Go (with Java & JavaScript alternatives)
Duration: 3-4 weeks
Difficulty: ⭐⭐⭐⭐☆
Why Build Redis?
Redis is the world’s most popular in-memory data store, used by companies from tiny startups to Netflix, Twitter, and GitHub. By building your own, you’ll master:- Network programming — TCP servers, connection handling, and the realities of partial reads and connection lifecycle
- Protocol design — RESP (Redis Serialization Protocol) parsing, and why protocol simplicity is a feature, not a limitation
- Data structures at scale — Hash tables, skip lists, doubly-linked lists, and when to choose each one
- Persistence strategies — RDB snapshots vs. AOF logging, and the durability-performance trade-off that every database must make
- Concurrent programming — Goroutines, channels, mutexes, and the art of keeping shared state consistent without destroying throughput
This is an advanced project. You should be comfortable with TCP/IP basics (what a socket is, how connections are established) and have experience with at least one systems programming language. If
net.Listen("tcp", ":6379") looks unfamiliar, review the Go networking tutorial first.Redis Architecture Overview
┌─────────────────────────────────────────────────────────────────────────────┐
│ REDIS ARCHITECTURE │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ CLIENT SERVER STORAGE │
│ ────── ────── ─────── │
│ │
│ ┌──────────┐ RESP ┌─────────────────┐ ┌───────────┐ │
│ │ redis │◄───────────►│ Command Router │ │ RDB │ │
│ │ -cli │ Protocol │ │◄──────►│ Snapshot │ │
│ └──────────┘ │ ┌───────────┐ │ └───────────┘ │
│ │ │ GET │ │ │
│ ┌──────────┐ │ │ SET │ │ ┌───────────┐ │
│ │ App │◄───────────►│ │ DEL │ │◄──────►│ AOF │ │
│ │ Client │ │ │ ... │ │ │ Append │ │
│ └──────────┘ │ └───────────┘ │ └───────────┘ │
│ │ │ │ │
│ │ ┌────▼────┐ │ │
│ │ │ Data │ │ │
│ │ │ Store │ │ │
│ │ │ (HashMap)│ │ │
│ │ └─────────┘ │ │
│ └─────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
What You’ll Build
Core Features
| Feature | Description | Skills Learned |
|---|---|---|
| RESP Parser | Parse/serialize Redis protocol | Protocol design, binary parsing |
| TCP Server | Handle multiple clients | Network programming, concurrency |
| String Commands | GET, SET, APPEND, INCR | Basic key-value operations |
| Expiration | EXPIRE, TTL, PTTL | Timer management, lazy deletion |
| List Commands | LPUSH, RPUSH, LPOP, LRANGE | Linked list implementation |
| Set Commands | SADD, SMEMBERS, SINTER | Set operations |
| Hash Commands | HSET, HGET, HGETALL | Nested hash tables |
| Sorted Sets | ZADD, ZRANGE, ZRANK | Skip list implementation |
| Pub/Sub | SUBSCRIBE, PUBLISH | Event-driven patterns |
| Persistence | RDB, AOF | Durability, recovery |
Implementation: Go
Project Structure
myredis/
├── cmd/
│ └── server/
│ └── main.go
├── internal/
│ ├── protocol/
│ │ ├── parser.go
│ │ ├── writer.go
│ │ └── types.go
│ ├── server/
│ │ ├── server.go
│ │ ├── client.go
│ │ └── handler.go
│ ├── store/
│ │ ├── store.go
│ │ ├── string.go
│ │ ├── list.go
│ │ ├── set.go
│ │ ├── hash.go
│ │ ├── zset.go
│ │ └── expiry.go
│ ├── persistence/
│ │ ├── rdb.go
│ │ └── aof.go
│ └── pubsub/
│ └── pubsub.go
├── go.mod
└── README.md
Core Implementation
package protocol
// RESP (Redis Serialization Protocol) types
// https://redis.io/docs/reference/protocol-spec/
type RESPType byte
const (
SimpleString RESPType = '+' // +OK\r\n
Error RESPType = '-' // -ERR message\r\n
Integer RESPType = ':' // :1000\r\n
BulkString RESPType = '$' // $5\r\nhello\r\n
Array RESPType = '*' // *2\r\n$3\r\nfoo\r\n$3\r\nbar\r\n
)
// Value represents a RESP value
type Value struct {
Type RESPType
Str string
Num int64
Bulk []byte
Array []Value
Null bool
}
// Common responses
var (
OKResponse = Value{Type: SimpleString, Str: "OK"}
NullResponse = Value{Type: BulkString, Null: true}
PongResponse = Value{Type: SimpleString, Str: "PONG"}
)
// NewError creates an error response
func NewError(msg string) Value {
return Value{Type: Error, Str: msg}
}
// NewInteger creates an integer response
func NewInteger(n int64) Value {
return Value{Type: Integer, Num: n}
}
// NewBulkString creates a bulk string response
func NewBulkString(s string) Value {
return Value{Type: BulkString, Bulk: []byte(s)}
}
// NewArray creates an array response
func NewArray(vals []Value) Value {
return Value{Type: Array, Array: vals}
}
package protocol
import (
"bufio"
"errors"
"io"
"strconv"
)
var (
ErrInvalidSyntax = errors.New("invalid RESP syntax")
ErrUnexpectedEOF = errors.New("unexpected end of input")
)
// Parser reads RESP values from a buffered reader
type Parser struct {
reader *bufio.Reader
}
// NewParser creates a new RESP parser
func NewParser(r io.Reader) *Parser {
return &Parser{
reader: bufio.NewReader(r),
}
}
// Parse reads and parses a single RESP value
func (p *Parser) Parse() (Value, error) {
typeByte, err := p.reader.ReadByte()
if err != nil {
return Value{}, err
}
switch RESPType(typeByte) {
case SimpleString:
return p.parseSimpleString()
case Error:
return p.parseError()
case Integer:
return p.parseInteger()
case BulkString:
return p.parseBulkString()
case Array:
return p.parseArray()
default:
// Handle inline commands (for redis-cli without RESP)
p.reader.UnreadByte()
return p.parseInlineCommand()
}
}
func (p *Parser) readLine() (string, error) {
line, err := p.reader.ReadString('\n')
if err != nil {
return "", err
}
// Remove \r\n
if len(line) >= 2 && line[len(line)-2] == '\r' {
return line[:len(line)-2], nil
}
return line[:len(line)-1], nil
}
func (p *Parser) parseSimpleString() (Value, error) {
line, err := p.readLine()
if err != nil {
return Value{}, err
}
return Value{Type: SimpleString, Str: line}, nil
}
func (p *Parser) parseError() (Value, error) {
line, err := p.readLine()
if err != nil {
return Value{}, err
}
return Value{Type: Error, Str: line}, nil
}
func (p *Parser) parseInteger() (Value, error) {
line, err := p.readLine()
if err != nil {
return Value{}, err
}
n, err := strconv.ParseInt(line, 10, 64)
if err != nil {
return Value{}, ErrInvalidSyntax
}
return Value{Type: Integer, Num: n}, nil
}
func (p *Parser) parseBulkString() (Value, error) {
line, err := p.readLine()
if err != nil {
return Value{}, err
}
length, err := strconv.Atoi(line)
if err != nil {
return Value{}, ErrInvalidSyntax
}
// Null bulk string
if length == -1 {
return Value{Type: BulkString, Null: true}, nil
}
// Read exactly 'length' bytes plus \r\n
buf := make([]byte, length+2)
_, err = io.ReadFull(p.reader, buf)
if err != nil {
return Value{}, ErrUnexpectedEOF
}
return Value{Type: BulkString, Bulk: buf[:length]}, nil
}
func (p *Parser) parseArray() (Value, error) {
line, err := p.readLine()
if err != nil {
return Value{}, err
}
count, err := strconv.Atoi(line)
if err != nil {
return Value{}, ErrInvalidSyntax
}
// Null array
if count == -1 {
return Value{Type: Array, Null: true}, nil
}
array := make([]Value, count)
for i := 0; i < count; i++ {
val, err := p.Parse()
if err != nil {
return Value{}, err
}
array[i] = val
}
return Value{Type: Array, Array: array}, nil
}
func (p *Parser) parseInlineCommand() (Value, error) {
line, err := p.readLine()
if err != nil {
return Value{}, err
}
// Split by spaces and convert to array
parts := splitInlineCommand(line)
array := make([]Value, len(parts))
for i, part := range parts {
array[i] = Value{Type: BulkString, Bulk: []byte(part)}
}
return Value{Type: Array, Array: array}, nil
}
func splitInlineCommand(line string) []string {
var parts []string
var current []byte
inQuote := false
quoteChar := byte(0)
for i := 0; i < len(line); i++ {
c := line[i]
if inQuote {
if c == quoteChar {
inQuote = false
} else {
current = append(current, c)
}
} else {
if c == '"' || c == '\'' {
inQuote = true
quoteChar = c
} else if c == ' ' {
if len(current) > 0 {
parts = append(parts, string(current))
current = nil
}
} else {
current = append(current, c)
}
}
}
if len(current) > 0 {
parts = append(parts, string(current))
}
return parts
}
package protocol
import (
"bufio"
"io"
"strconv"
)
// Writer writes RESP values to a buffered writer
type Writer struct {
writer *bufio.Writer
}
// NewWriter creates a new RESP writer
func NewWriter(w io.Writer) *Writer {
return &Writer{
writer: bufio.NewWriter(w),
}
}
// Write serializes a Value to RESP format and writes it
func (w *Writer) Write(v Value) error {
switch v.Type {
case SimpleString:
return w.writeSimpleString(v.Str)
case Error:
return w.writeError(v.Str)
case Integer:
return w.writeInteger(v.Num)
case BulkString:
return w.writeBulkString(v)
case Array:
return w.writeArray(v)
default:
return nil
}
}
// Flush flushes the underlying buffer
func (w *Writer) Flush() error {
return w.writer.Flush()
}
func (w *Writer) writeSimpleString(s string) error {
_, err := w.writer.WriteString("+" + s + "\r\n")
return err
}
func (w *Writer) writeError(s string) error {
_, err := w.writer.WriteString("-" + s + "\r\n")
return err
}
func (w *Writer) writeInteger(n int64) error {
_, err := w.writer.WriteString(":" + strconv.FormatInt(n, 10) + "\r\n")
return err
}
func (w *Writer) writeBulkString(v Value) error {
if v.Null {
_, err := w.writer.WriteString("$-1\r\n")
return err
}
length := len(v.Bulk)
w.writer.WriteString("$" + strconv.Itoa(length) + "\r\n")
w.writer.Write(v.Bulk)
_, err := w.writer.WriteString("\r\n")
return err
}
func (w *Writer) writeArray(v Value) error {
if v.Null {
_, err := w.writer.WriteString("*-1\r\n")
return err
}
length := len(v.Array)
w.writer.WriteString("*" + strconv.Itoa(length) + "\r\n")
for _, elem := range v.Array {
if err := w.Write(elem); err != nil {
return err
}
}
return nil
}
package store
import (
"sync"
"time"
)
// Store is the main data store
type Store struct {
data map[string]*Entry
expires map[string]time.Time
mu sync.RWMutex
}
// Entry represents a stored value with its type
type Entry struct {
Type DataType
Value interface{}
}
// DataType represents the type of stored data
type DataType int
const (
TypeString DataType = iota
TypeList
TypeSet
TypeHash
TypeZSet
)
// NewStore creates a new data store
func NewStore() *Store {
s := &Store{
data: make(map[string]*Entry),
expires: make(map[string]time.Time),
}
// Start background expiry goroutine
go s.expireLoop()
return s
}
// Get retrieves a value by key
func (s *Store) Get(key string) (*Entry, bool) {
s.mu.RLock()
defer s.mu.RUnlock()
// Check expiration
if exp, ok := s.expires[key]; ok && time.Now().After(exp) {
return nil, false
}
entry, ok := s.data[key]
return entry, ok
}
// Set stores a value
func (s *Store) Set(key string, entry *Entry) {
s.mu.Lock()
defer s.mu.Unlock()
s.data[key] = entry
delete(s.expires, key) // Clear any existing expiration
}
// SetWithExpiry stores a value with an expiration time
func (s *Store) SetWithExpiry(key string, entry *Entry, expiry time.Duration) {
s.mu.Lock()
defer s.mu.Unlock()
s.data[key] = entry
s.expires[key] = time.Now().Add(expiry)
}
// Delete removes a key
func (s *Store) Delete(keys ...string) int64 {
s.mu.Lock()
defer s.mu.Unlock()
var count int64
for _, key := range keys {
if _, ok := s.data[key]; ok {
delete(s.data, key)
delete(s.expires, key)
count++
}
}
return count
}
// Exists checks if keys exist
func (s *Store) Exists(keys ...string) int64 {
s.mu.RLock()
defer s.mu.RUnlock()
var count int64
for _, key := range keys {
if exp, ok := s.expires[key]; ok && time.Now().After(exp) {
continue
}
if _, ok := s.data[key]; ok {
count++
}
}
return count
}
// Expire sets an expiration on a key
func (s *Store) Expire(key string, duration time.Duration) bool {
s.mu.Lock()
defer s.mu.Unlock()
if _, ok := s.data[key]; !ok {
return false
}
s.expires[key] = time.Now().Add(duration)
return true
}
// TTL returns the remaining time to live
func (s *Store) TTL(key string) int64 {
s.mu.RLock()
defer s.mu.RUnlock()
if _, ok := s.data[key]; !ok {
return -2 // Key doesn't exist
}
exp, ok := s.expires[key]
if !ok {
return -1 // No expiration
}
remaining := time.Until(exp)
if remaining < 0 {
return -2 // Expired
}
return int64(remaining.Seconds())
}
// Keys returns all keys matching a pattern
func (s *Store) Keys(pattern string) []string {
s.mu.RLock()
defer s.mu.RUnlock()
var keys []string
now := time.Now()
for key := range s.data {
// Check expiration
if exp, ok := s.expires[key]; ok && now.After(exp) {
continue
}
// Simple pattern matching (* matches everything)
if pattern == "*" || matchPattern(pattern, key) {
keys = append(keys, key)
}
}
return keys
}
// Background expiry cleanup
func (s *Store) expireLoop() {
ticker := time.NewTicker(100 * time.Millisecond)
defer ticker.Stop()
for range ticker.C {
s.mu.Lock()
now := time.Now()
// Sample and delete expired keys
count := 0
for key, exp := range s.expires {
if now.After(exp) {
delete(s.data, key)
delete(s.expires, key)
}
count++
if count > 20 { // Limit per iteration
break
}
}
s.mu.Unlock()
}
}
// Simple glob pattern matching
func matchPattern(pattern, s string) bool {
if pattern == "*" {
return true
}
// Simplified - you'd want a proper glob implementation
// This just handles prefix* and *suffix patterns
if len(pattern) > 0 && pattern[0] == '*' {
return len(s) >= len(pattern)-1 && s[len(s)-(len(pattern)-1):] == pattern[1:]
}
if len(pattern) > 0 && pattern[len(pattern)-1] == '*' {
return len(s) >= len(pattern)-1 && s[:len(pattern)-1] == pattern[:len(pattern)-1]
}
return pattern == s
}
package store
import (
"strconv"
)
// String operations
// GetString retrieves a string value
func (s *Store) GetString(key string) (string, bool) {
entry, ok := s.Get(key)
if !ok {
return "", false
}
if entry.Type != TypeString {
return "", false
}
return entry.Value.(string), true
}
// SetString stores a string value
func (s *Store) SetString(key, value string) {
s.Set(key, &Entry{
Type: TypeString,
Value: value,
})
}
// Append appends to a string value
func (s *Store) Append(key, value string) int64 {
s.mu.Lock()
defer s.mu.Unlock()
entry, ok := s.data[key]
if !ok {
s.data[key] = &Entry{Type: TypeString, Value: value}
return int64(len(value))
}
if entry.Type != TypeString {
return -1
}
newValue := entry.Value.(string) + value
entry.Value = newValue
return int64(len(newValue))
}
// Incr increments a string value as integer
func (s *Store) Incr(key string) (int64, error) {
return s.IncrBy(key, 1)
}
// IncrBy increments by a specified amount
func (s *Store) IncrBy(key string, delta int64) (int64, error) {
s.mu.Lock()
defer s.mu.Unlock()
entry, ok := s.data[key]
if !ok {
s.data[key] = &Entry{Type: TypeString, Value: strconv.FormatInt(delta, 10)}
return delta, nil
}
if entry.Type != TypeString {
return 0, ErrWrongType
}
current, err := strconv.ParseInt(entry.Value.(string), 10, 64)
if err != nil {
return 0, ErrNotInteger
}
newValue := current + delta
entry.Value = strconv.FormatInt(newValue, 10)
return newValue, nil
}
// Decr decrements a string value
func (s *Store) Decr(key string) (int64, error) {
return s.IncrBy(key, -1)
}
// MGet gets multiple keys
func (s *Store) MGet(keys ...string) []interface{} {
s.mu.RLock()
defer s.mu.RUnlock()
results := make([]interface{}, len(keys))
for i, key := range keys {
entry, ok := s.data[key]
if !ok || entry.Type != TypeString {
results[i] = nil
continue
}
results[i] = entry.Value.(string)
}
return results
}
// MSet sets multiple key-value pairs
func (s *Store) MSet(pairs map[string]string) {
s.mu.Lock()
defer s.mu.Unlock()
for key, value := range pairs {
s.data[key] = &Entry{Type: TypeString, Value: value}
}
}
// Errors
type StoreError string
func (e StoreError) Error() string { return string(e) }
const (
ErrWrongType = StoreError("WRONGTYPE Operation against a key holding the wrong kind of value")
ErrNotInteger = StoreError("ERR value is not an integer or out of range")
)
package store
import "container/list"
// List operations using Go's container/list
// LPush inserts values at the head of the list
func (s *Store) LPush(key string, values ...string) int64 {
s.mu.Lock()
defer s.mu.Unlock()
entry, ok := s.data[key]
if !ok {
l := list.New()
entry = &Entry{Type: TypeList, Value: l}
s.data[key] = entry
}
if entry.Type != TypeList {
return -1
}
l := entry.Value.(*list.List)
for _, v := range values {
l.PushFront(v)
}
return int64(l.Len())
}
// RPush inserts values at the tail of the list
func (s *Store) RPush(key string, values ...string) int64 {
s.mu.Lock()
defer s.mu.Unlock()
entry, ok := s.data[key]
if !ok {
l := list.New()
entry = &Entry{Type: TypeList, Value: l}
s.data[key] = entry
}
if entry.Type != TypeList {
return -1
}
l := entry.Value.(*list.List)
for _, v := range values {
l.PushBack(v)
}
return int64(l.Len())
}
// LPop removes and returns the first element
func (s *Store) LPop(key string) (string, bool) {
s.mu.Lock()
defer s.mu.Unlock()
entry, ok := s.data[key]
if !ok || entry.Type != TypeList {
return "", false
}
l := entry.Value.(*list.List)
if l.Len() == 0 {
return "", false
}
elem := l.Front()
l.Remove(elem)
// Clean up empty list
if l.Len() == 0 {
delete(s.data, key)
}
return elem.Value.(string), true
}
// RPop removes and returns the last element
func (s *Store) RPop(key string) (string, bool) {
s.mu.Lock()
defer s.mu.Unlock()
entry, ok := s.data[key]
if !ok || entry.Type != TypeList {
return "", false
}
l := entry.Value.(*list.List)
if l.Len() == 0 {
return "", false
}
elem := l.Back()
l.Remove(elem)
if l.Len() == 0 {
delete(s.data, key)
}
return elem.Value.(string), true
}
// LRange returns a range of elements
func (s *Store) LRange(key string, start, stop int64) []string {
s.mu.RLock()
defer s.mu.RUnlock()
entry, ok := s.data[key]
if !ok || entry.Type != TypeList {
return nil
}
l := entry.Value.(*list.List)
length := int64(l.Len())
// Handle negative indices
if start < 0 {
start = length + start
}
if stop < 0 {
stop = length + stop
}
// Bounds checking
if start < 0 {
start = 0
}
if stop >= length {
stop = length - 1
}
if start > stop || start >= length {
return nil
}
var result []string
var i int64 = 0
for elem := l.Front(); elem != nil && i <= stop; elem = elem.Next() {
if i >= start {
result = append(result, elem.Value.(string))
}
i++
}
return result
}
// LLen returns the length of the list
func (s *Store) LLen(key string) int64 {
s.mu.RLock()
defer s.mu.RUnlock()
entry, ok := s.data[key]
if !ok || entry.Type != TypeList {
return 0
}
return int64(entry.Value.(*list.List).Len())
}
// LIndex returns the element at index
func (s *Store) LIndex(key string, index int64) (string, bool) {
s.mu.RLock()
defer s.mu.RUnlock()
entry, ok := s.data[key]
if !ok || entry.Type != TypeList {
return "", false
}
l := entry.Value.(*list.List)
length := int64(l.Len())
// Handle negative index
if index < 0 {
index = length + index
}
if index < 0 || index >= length {
return "", false
}
var i int64 = 0
for elem := l.Front(); elem != nil; elem = elem.Next() {
if i == index {
return elem.Value.(string), true
}
i++
}
return "", false
}
package store
// Hash operations
// HSet sets field in hash to value
func (s *Store) HSet(key string, fields map[string]string) int64 {
s.mu.Lock()
defer s.mu.Unlock()
entry, ok := s.data[key]
if !ok {
h := make(map[string]string)
entry = &Entry{Type: TypeHash, Value: h}
s.data[key] = entry
}
if entry.Type != TypeHash {
return -1
}
h := entry.Value.(map[string]string)
var newFields int64 = 0
for field, value := range fields {
if _, exists := h[field]; !exists {
newFields++
}
h[field] = value
}
return newFields
}
// HGet gets a field from hash
func (s *Store) HGet(key, field string) (string, bool) {
s.mu.RLock()
defer s.mu.RUnlock()
entry, ok := s.data[key]
if !ok || entry.Type != TypeHash {
return "", false
}
h := entry.Value.(map[string]string)
value, ok := h[field]
return value, ok
}
// HGetAll gets all fields and values
func (s *Store) HGetAll(key string) map[string]string {
s.mu.RLock()
defer s.mu.RUnlock()
entry, ok := s.data[key]
if !ok || entry.Type != TypeHash {
return nil
}
h := entry.Value.(map[string]string)
result := make(map[string]string, len(h))
for k, v := range h {
result[k] = v
}
return result
}
// HDel deletes fields from hash
func (s *Store) HDel(key string, fields ...string) int64 {
s.mu.Lock()
defer s.mu.Unlock()
entry, ok := s.data[key]
if !ok || entry.Type != TypeHash {
return 0
}
h := entry.Value.(map[string]string)
var deleted int64 = 0
for _, field := range fields {
if _, exists := h[field]; exists {
delete(h, field)
deleted++
}
}
// Clean up empty hash
if len(h) == 0 {
delete(s.data, key)
}
return deleted
}
// HExists checks if field exists
func (s *Store) HExists(key, field string) bool {
s.mu.RLock()
defer s.mu.RUnlock()
entry, ok := s.data[key]
if !ok || entry.Type != TypeHash {
return false
}
h := entry.Value.(map[string]string)
_, exists := h[field]
return exists
}
// HLen returns number of fields in hash
func (s *Store) HLen(key string) int64 {
s.mu.RLock()
defer s.mu.RUnlock()
entry, ok := s.data[key]
if !ok || entry.Type != TypeHash {
return 0
}
h := entry.Value.(map[string]string)
return int64(len(h))
}
// HKeys returns all field names
func (s *Store) HKeys(key string) []string {
s.mu.RLock()
defer s.mu.RUnlock()
entry, ok := s.data[key]
if !ok || entry.Type != TypeHash {
return nil
}
h := entry.Value.(map[string]string)
keys := make([]string, 0, len(h))
for k := range h {
keys = append(keys, k)
}
return keys
}
// HVals returns all values
func (s *Store) HVals(key string) []string {
s.mu.RLock()
defer s.mu.RUnlock()
entry, ok := s.data[key]
if !ok || entry.Type != TypeHash {
return nil
}
h := entry.Value.(map[string]string)
vals := make([]string, 0, len(h))
for _, v := range h {
vals = append(vals, v)
}
return vals
}
package store
// Set operations
// SAdd adds members to a set
func (s *Store) SAdd(key string, members ...string) int64 {
s.mu.Lock()
defer s.mu.Unlock()
entry, ok := s.data[key]
if !ok {
set := make(map[string]struct{})
entry = &Entry{Type: TypeSet, Value: set}
s.data[key] = entry
}
if entry.Type != TypeSet {
return -1
}
set := entry.Value.(map[string]struct{})
var added int64 = 0
for _, member := range members {
if _, exists := set[member]; !exists {
set[member] = struct{}{}
added++
}
}
return added
}
// SMembers returns all members of a set
func (s *Store) SMembers(key string) []string {
s.mu.RLock()
defer s.mu.RUnlock()
entry, ok := s.data[key]
if !ok || entry.Type != TypeSet {
return nil
}
set := entry.Value.(map[string]struct{})
members := make([]string, 0, len(set))
for member := range set {
members = append(members, member)
}
return members
}
// SIsMember checks if member exists in set
func (s *Store) SIsMember(key, member string) bool {
s.mu.RLock()
defer s.mu.RUnlock()
entry, ok := s.data[key]
if !ok || entry.Type != TypeSet {
return false
}
set := entry.Value.(map[string]struct{})
_, exists := set[member]
return exists
}
// SCard returns the cardinality (size) of set
func (s *Store) SCard(key string) int64 {
s.mu.RLock()
defer s.mu.RUnlock()
entry, ok := s.data[key]
if !ok || entry.Type != TypeSet {
return 0
}
return int64(len(entry.Value.(map[string]struct{})))
}
// SRem removes members from set
func (s *Store) SRem(key string, members ...string) int64 {
s.mu.Lock()
defer s.mu.Unlock()
entry, ok := s.data[key]
if !ok || entry.Type != TypeSet {
return 0
}
set := entry.Value.(map[string]struct{})
var removed int64 = 0
for _, member := range members {
if _, exists := set[member]; exists {
delete(set, member)
removed++
}
}
if len(set) == 0 {
delete(s.data, key)
}
return removed
}
// SInter returns intersection of sets
func (s *Store) SInter(keys ...string) []string {
s.mu.RLock()
defer s.mu.RUnlock()
if len(keys) == 0 {
return nil
}
// Get first set
entry, ok := s.data[keys[0]]
if !ok || entry.Type != TypeSet {
return nil
}
result := make(map[string]struct{})
for member := range entry.Value.(map[string]struct{}) {
result[member] = struct{}{}
}
// Intersect with remaining sets
for _, key := range keys[1:] {
entry, ok := s.data[key]
if !ok || entry.Type != TypeSet {
return nil
}
set := entry.Value.(map[string]struct{})
for member := range result {
if _, exists := set[member]; !exists {
delete(result, member)
}
}
}
members := make([]string, 0, len(result))
for member := range result {
members = append(members, member)
}
return members
}
// SUnion returns union of sets
func (s *Store) SUnion(keys ...string) []string {
s.mu.RLock()
defer s.mu.RUnlock()
result := make(map[string]struct{})
for _, key := range keys {
entry, ok := s.data[key]
if !ok || entry.Type != TypeSet {
continue
}
set := entry.Value.(map[string]struct{})
for member := range set {
result[member] = struct{}{}
}
}
members := make([]string, 0, len(result))
for member := range result {
members = append(members, member)
}
return members
}
// SDiff returns difference of sets (first - rest)
func (s *Store) SDiff(keys ...string) []string {
s.mu.RLock()
defer s.mu.RUnlock()
if len(keys) == 0 {
return nil
}
entry, ok := s.data[keys[0]]
if !ok || entry.Type != TypeSet {
return nil
}
result := make(map[string]struct{})
for member := range entry.Value.(map[string]struct{}) {
result[member] = struct{}{}
}
for _, key := range keys[1:] {
entry, ok := s.data[key]
if !ok || entry.Type != TypeSet {
continue
}
set := entry.Value.(map[string]struct{})
for member := range set {
delete(result, member)
}
}
members := make([]string, 0, len(result))
for member := range result {
members = append(members, member)
}
return members
}
package server
import (
"fmt"
"log"
"net"
"sync"
"myredis/internal/store"
)
// Server represents the Redis server
type Server struct {
listener net.Listener
store *store.Store
clients map[*Client]struct{}
mu sync.Mutex
quit chan struct{}
}
// New creates a new server
func New(addr string) (*Server, error) {
listener, err := net.Listen("tcp", addr)
if err != nil {
return nil, fmt.Errorf("failed to listen: %w", err)
}
return &Server{
listener: listener,
store: store.NewStore(),
clients: make(map[*Client]struct{}),
quit: make(chan struct{}),
}, nil
}
// Start begins accepting connections
func (s *Server) Start() error {
log.Printf("Server listening on %s", s.listener.Addr())
for {
conn, err := s.listener.Accept()
if err != nil {
select {
case <-s.quit:
return nil
default:
log.Printf("Accept error: %v", err)
continue
}
}
client := NewClient(conn, s)
s.addClient(client)
go s.handleClient(client)
}
}
// Stop gracefully shuts down the server
func (s *Server) Stop() {
close(s.quit)
s.listener.Close()
s.mu.Lock()
for client := range s.clients {
client.Close()
}
s.mu.Unlock()
}
func (s *Server) addClient(c *Client) {
s.mu.Lock()
s.clients[c] = struct{}{}
s.mu.Unlock()
}
func (s *Server) removeClient(c *Client) {
s.mu.Lock()
delete(s.clients, c)
s.mu.Unlock()
}
func (s *Server) handleClient(c *Client) {
defer func() {
c.Close()
s.removeClient(c)
}()
c.Handle()
}
// Store returns the data store
func (s *Server) Store() *store.Store {
return s.store
}
package server
import (
"io"
"log"
"net"
"strings"
"myredis/internal/protocol"
)
// Client represents a connected client
type Client struct {
conn net.Conn
server *Server
parser *protocol.Parser
writer *protocol.Writer
}
// NewClient creates a new client
func NewClient(conn net.Conn, server *Server) *Client {
return &Client{
conn: conn,
server: server,
parser: protocol.NewParser(conn),
writer: protocol.NewWriter(conn),
}
}
// Handle processes client commands
func (c *Client) Handle() {
for {
value, err := c.parser.Parse()
if err != nil {
if err != io.EOF {
log.Printf("Parse error: %v", err)
}
return
}
response := c.executeCommand(value)
if err := c.writer.Write(response); err != nil {
log.Printf("Write error: %v", err)
return
}
c.writer.Flush()
}
}
// Close closes the client connection
func (c *Client) Close() {
c.conn.Close()
}
func (c *Client) executeCommand(value protocol.Value) protocol.Value {
if value.Type != protocol.Array || len(value.Array) == 0 {
return protocol.NewError("ERR unknown command")
}
// Extract command and arguments
args := make([]string, len(value.Array))
for i, v := range value.Array {
args[i] = string(v.Bulk)
}
cmd := strings.ToUpper(args[0])
args = args[1:]
return c.handleCommand(cmd, args)
}
func (c *Client) handleCommand(cmd string, args []string) protocol.Value {
store := c.server.Store()
switch cmd {
// Connection commands
case "PING":
if len(args) == 0 {
return protocol.PongResponse
}
return protocol.NewBulkString(args[0])
case "ECHO":
if len(args) < 1 {
return protocol.NewError("ERR wrong number of arguments for 'echo' command")
}
return protocol.NewBulkString(args[0])
// String commands
case "GET":
if len(args) < 1 {
return protocol.NewError("ERR wrong number of arguments for 'get' command")
}
value, ok := store.GetString(args[0])
if !ok {
return protocol.NullResponse
}
return protocol.NewBulkString(value)
case "SET":
if len(args) < 2 {
return protocol.NewError("ERR wrong number of arguments for 'set' command")
}
store.SetString(args[0], args[1])
return protocol.OKResponse
case "DEL":
if len(args) < 1 {
return protocol.NewError("ERR wrong number of arguments for 'del' command")
}
count := store.Delete(args...)
return protocol.NewInteger(count)
case "EXISTS":
if len(args) < 1 {
return protocol.NewError("ERR wrong number of arguments for 'exists' command")
}
count := store.Exists(args...)
return protocol.NewInteger(count)
case "INCR":
if len(args) < 1 {
return protocol.NewError("ERR wrong number of arguments for 'incr' command")
}
n, err := store.Incr(args[0])
if err != nil {
return protocol.NewError(err.Error())
}
return protocol.NewInteger(n)
case "DECR":
if len(args) < 1 {
return protocol.NewError("ERR wrong number of arguments for 'decr' command")
}
n, err := store.Decr(args[0])
if err != nil {
return protocol.NewError(err.Error())
}
return protocol.NewInteger(n)
// List commands
case "LPUSH":
if len(args) < 2 {
return protocol.NewError("ERR wrong number of arguments for 'lpush' command")
}
n := store.LPush(args[0], args[1:]...)
if n < 0 {
return protocol.NewError(string(store.ErrWrongType))
}
return protocol.NewInteger(n)
case "RPUSH":
if len(args) < 2 {
return protocol.NewError("ERR wrong number of arguments for 'rpush' command")
}
n := store.RPush(args[0], args[1:]...)
if n < 0 {
return protocol.NewError(string(store.ErrWrongType))
}
return protocol.NewInteger(n)
case "LPOP":
if len(args) < 1 {
return protocol.NewError("ERR wrong number of arguments for 'lpop' command")
}
v, ok := store.LPop(args[0])
if !ok {
return protocol.NullResponse
}
return protocol.NewBulkString(v)
case "RPOP":
if len(args) < 1 {
return protocol.NewError("ERR wrong number of arguments for 'rpop' command")
}
v, ok := store.RPop(args[0])
if !ok {
return protocol.NullResponse
}
return protocol.NewBulkString(v)
case "LRANGE":
if len(args) < 3 {
return protocol.NewError("ERR wrong number of arguments for 'lrange' command")
}
start := parseInt(args[1])
stop := parseInt(args[2])
values := store.LRange(args[0], start, stop)
return toArrayResponse(values)
case "LLEN":
if len(args) < 1 {
return protocol.NewError("ERR wrong number of arguments for 'llen' command")
}
return protocol.NewInteger(store.LLen(args[0]))
// Hash commands
case "HSET":
if len(args) < 3 || len(args)%2 == 0 {
return protocol.NewError("ERR wrong number of arguments for 'hset' command")
}
fields := make(map[string]string)
for i := 1; i < len(args); i += 2 {
fields[args[i]] = args[i+1]
}
n := store.HSet(args[0], fields)
if n < 0 {
return protocol.NewError(string(store.ErrWrongType))
}
return protocol.NewInteger(n)
case "HGET":
if len(args) < 2 {
return protocol.NewError("ERR wrong number of arguments for 'hget' command")
}
v, ok := store.HGet(args[0], args[1])
if !ok {
return protocol.NullResponse
}
return protocol.NewBulkString(v)
case "HGETALL":
if len(args) < 1 {
return protocol.NewError("ERR wrong number of arguments for 'hgetall' command")
}
h := store.HGetAll(args[0])
result := make([]protocol.Value, 0, len(h)*2)
for k, v := range h {
result = append(result, protocol.NewBulkString(k))
result = append(result, protocol.NewBulkString(v))
}
return protocol.NewArray(result)
case "HDEL":
if len(args) < 2 {
return protocol.NewError("ERR wrong number of arguments for 'hdel' command")
}
n := store.HDel(args[0], args[1:]...)
return protocol.NewInteger(n)
// Set commands
case "SADD":
if len(args) < 2 {
return protocol.NewError("ERR wrong number of arguments for 'sadd' command")
}
n := store.SAdd(args[0], args[1:]...)
if n < 0 {
return protocol.NewError(string(store.ErrWrongType))
}
return protocol.NewInteger(n)
case "SMEMBERS":
if len(args) < 1 {
return protocol.NewError("ERR wrong number of arguments for 'smembers' command")
}
return toArrayResponse(store.SMembers(args[0]))
case "SISMEMBER":
if len(args) < 2 {
return protocol.NewError("ERR wrong number of arguments for 'sismember' command")
}
if store.SIsMember(args[0], args[1]) {
return protocol.NewInteger(1)
}
return protocol.NewInteger(0)
case "SCARD":
if len(args) < 1 {
return protocol.NewError("ERR wrong number of arguments for 'scard' command")
}
return protocol.NewInteger(store.SCard(args[0]))
case "SINTER":
return toArrayResponse(store.SInter(args...))
case "SUNION":
return toArrayResponse(store.SUnion(args...))
// Key commands
case "KEYS":
if len(args) < 1 {
return protocol.NewError("ERR wrong number of arguments for 'keys' command")
}
return toArrayResponse(store.Keys(args[0]))
case "TTL":
if len(args) < 1 {
return protocol.NewError("ERR wrong number of arguments for 'ttl' command")
}
return protocol.NewInteger(store.TTL(args[0]))
default:
return protocol.NewError("ERR unknown command '" + cmd + "'")
}
}
func parseInt(s string) int64 {
var n int64
fmt.Sscanf(s, "%d", &n)
return n
}
func toArrayResponse(values []string) protocol.Value {
if values == nil {
return protocol.NewArray(nil)
}
result := make([]protocol.Value, len(values))
for i, v := range values {
result[i] = protocol.NewBulkString(v)
}
return protocol.NewArray(result)
}
package main
import (
"flag"
"log"
"os"
"os/signal"
"syscall"
"myredis/internal/server"
)
func main() {
port := flag.String("port", "6379", "Port to listen on")
flag.Parse()
addr := ":" + *port
srv, err := server.New(addr)
if err != nil {
log.Fatalf("Failed to create server: %v", err)
}
// Handle graceful shutdown
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-sigCh
log.Println("Shutting down...")
srv.Stop()
}()
log.Printf("Starting MyRedis server on %s", addr)
if err := srv.Start(); err != nil {
log.Fatalf("Server error: %v", err)
}
}
Testing Your Redis
# Build and run
go build -o myredis ./cmd/server
./myredis -port 6379
# In another terminal, use redis-cli
redis-cli
127.0.0.1:6379> PING
PONG
127.0.0.1:6379> SET hello world
OK
127.0.0.1:6379> GET hello
"world"
127.0.0.1:6379> LPUSH mylist a b c
(integer) 3
127.0.0.1:6379> LRANGE mylist 0 -1
1) "c"
2) "b"
3) "a"
Advanced Topics
1. Persistence (RDB Snapshots)
// internal/persistence/rdb.go
package persistence
import (
"encoding/gob"
"os"
"myredis/internal/store"
)
func SaveRDB(store *store.Store, filename string) error {
file, err := os.Create(filename)
if err != nil {
return err
}
defer file.Close()
encoder := gob.NewEncoder(file)
// Serialize store data...
return nil
}
2. Pub/Sub
// internal/pubsub/pubsub.go
package pubsub
import (
"sync"
)
type PubSub struct {
channels map[string]map[chan string]struct{}
mu sync.RWMutex
}
func (ps *PubSub) Subscribe(channel string) chan string {
ps.mu.Lock()
defer ps.mu.Unlock()
if ps.channels[channel] == nil {
ps.channels[channel] = make(map[chan string]struct{})
}
ch := make(chan string, 100)
ps.channels[channel][ch] = struct{}{}
return ch
}
func (ps *PubSub) Publish(channel, message string) int {
ps.mu.RLock()
defer ps.mu.RUnlock()
subscribers := ps.channels[channel]
for ch := range subscribers {
select {
case ch <- message:
default:
// Channel full, skip
}
}
return len(subscribers)
}
Exercises
Level 1: Core Implementation
- Add APPEND command
- Implement SETEX (set with expiry)
- Add LINDEX command
Level 2: Advanced Features
- Implement Sorted Sets with skip list
- Add RDB persistence
- Implement MULTI/EXEC transactions
Level 3: Production Features
- Add AOF persistence
- Implement cluster mode
- Add Lua scripting support
What You’ve Learned
TCP server implementation in Go
Binary protocol parsing (RESP)
Concurrent data structure design
Memory management and expiration
Production-grade Go patterns
Next Steps
Java Implementation
See how concurrency differs in Java
JavaScript Implementation
Node.js event loop approach
Build Docker
Ready for the ultimate challenge?