Skip to content

About

A Redis-like in-memory key-value store built in C++ implementing RESP protocol, TTL expiration, persistence, Pub/Sub messaging, and concurrent client handling.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Latest commit

Β 

History

4 Commits

Folders and files

Repository files navigation

πŸš€ my_redis_server

A lightweight, Redis-compatible in-memory data store β€” written from scratch in modern C++

A from-scratch Redis clone speaking the real RESP wire protocol over raw TCP sockets, with multi-client concurrency, lazy key expiration, and periodic disk persistence.


C++ Protocol Concurrency Port License


40+ commands Β β€’Β  3 data types Β β€’Β  redis-cli compatible Β β€’Β  thread-safe


πŸ“– Overview

my_redis_server is a Redis-compatible server that implements common Redis commands over a plain TCP socket using the RESP (Redis Serialization Protocol). Because it speaks the real protocol, you can connect to it with the standard redis-cli β€” no custom client required.

Name my_redis_server
Default Port 6379
Language C++17
Wire Protocol RESP (inline + array formats)
Persistence Text-based RDB snapshot (dump.my_rdb)
Concurrency Model One std::thread per client, single mutex-guarded store

πŸ—οΈ System Architecture

The server is split into three cohesive layers: a networking layer that owns the socket and client threads, a protocol layer that parses RESP and dispatches commands, and a storage layer that owns the in-memory data and persistence.

flowchart TB
    subgraph Clients["πŸ–₯️  Clients"]
        C1["redis-cli"]
        C2["my_redis_cli"]
        C3["Any RESP client"]
    end

    subgraph Server["βš™οΈ  RedisServer  (Networking Layer)"]
        LISTEN["TCP Listener<br/>(port 6379)"]
        T1["Client Thread 1"]
        T2["Client Thread 2"]
        TN["Client Thread N"]
        SIG["SIGINT Handler<br/>(graceful shutdown)"]
    end

    subgraph Handler["πŸ”Œ  RedisCommandHandler  (Protocol Layer)"]
        PARSE["RESP Parser"]
        DISPATCH["Command Dispatch"]
    end

    subgraph DB["πŸ—„οΈ  RedisDatabase  (Storage Layer β€” Singleton)"]
        MUTEX["πŸ”’ std::mutex db_mutex"]
        KV["kv_store<br/>strings"]
        LIST["list_store<br/>lists"]
        HASH["hash_store<br/>hashes"]
        EXP["expiry_map<br/>TTLs"]
    end

    DUMP[("πŸ’Ύ dump.my_rdb")]

    C1 & C2 & C3 -->|TCP| LISTEN
    LISTEN --> T1 & T2 & TN
    T1 & T2 & TN --> PARSE
    PARSE --> DISPATCH
    DISPATCH --> MUTEX
    MUTEX --> KV & LIST & HASH & EXP
    DB <-->|snapshot / load| DUMP
    SIG -.->|flush on exit| DUMP

    classDef client fill:#f8d7f4,stroke:#b53471,stroke-width:2px,color:#3d0a3d
    classDef net fill:#e7d3fb,stroke:#9b59b6,stroke-width:2px,color:#2d1a4d
    classDef proto fill:#dcc7fb,stroke:#7d3cff,stroke-width:2px,color:#2d1a4d
    classDef store fill:#f3d9ff,stroke:#c471ed,stroke-width:2px,color:#3d0a3d
    classDef disk fill:#ffe0f0,stroke:#f368e0,stroke-width:2px,color:#3d0a3d

    class C1,C2,C3 client
    class LISTEN,T1,T2,TN,SIG net
    class PARSE,DISPATCH proto
    class MUTEX,KV,LIST,HASH,EXP store
    class DUMP disk
Loading

πŸ”„ Request Lifecycle

Every command travels the same path: raw bytes off the socket β†’ RESP parsed into tokens β†’ dispatched to a handler β†’ the store is mutated under a lock β†’ a RESP-encoded reply is written back.

sequenceDiagram
    autonumber
    participant Client as πŸ–₯️ Client
    participant Server as βš™οΈ RedisServer
    participant Handler as πŸ”Œ CommandHandler
    participant DB as πŸ—„οΈ RedisDatabase

    Client->>Server: TCP connect
    Server->>Server: spawn std::thread for client
    loop per command
        Client->>Server: send RESP bytes (e.g. SET foo bar)
        Server->>Handler: processCommand(raw)
        Handler->>Handler: parse RESP β†’ [SET, foo, bar]
        Handler->>DB: πŸ”’ lock db_mutex
        Handler->>DB: purgeExpired()
        DB->>DB: mutate kv_store / list_store / hash_store
        Handler->>DB: πŸ”“ unlock db_mutex
        Handler-->>Server: RESP-encoded reply (+OK)
        Server-->>Client: write reply
    end
    Client->>Server: disconnect
    Server->>Server: join / clean up thread
Loading

πŸ—‚οΈ Storage Model

All data lives in three unordered_map stores plus a dedicated TTL map. A single std::mutex guards every store, so reads and writes across all data types are serialized and consistent.

flowchart LR
    subgraph DB["RedisDatabase (Singleton)"]
        direction TB
        KV["<b>kv_store</b><br/>unordered_map&lt;string, string&gt;<br/><i>Strings</i>"]
        LIST["<b>list_store</b><br/>unordered_map&lt;string, vector&lt;string&gt;&gt;<br/><i>Lists</i>"]
        HASH["<b>hash_store</b><br/>unordered_map&lt;string,<br/>unordered_map&lt;string,string&gt;&gt;<br/><i>Hashes</i>"]
        EXP["<b>expiry_map</b><br/>unordered_map&lt;string, time_point&gt;<br/><i>TTLs</i>"]
    end

    classDef store fill:#f3d9ff,stroke:#c471ed,stroke-width:2px,color:#3d0a3d
    class KV,LIST,HASH,EXP store
Loading

Lazy expiration: TTLs aren't reaped by a timer. Instead, purgeExpired() runs on each key access β€” if a key's deadline in expiry_map has passed, it's evicted on the spot before the command sees it.


πŸ’Ύ Persistence Flow

Persistence uses a simplified, text-based RDB snapshot. A background thread snapshots periodically, the SIGINT handler snapshots on graceful shutdown, and the server attempts to load the snapshot at startup.

flowchart TD
    START(["🟒 Server start"]) --> LOAD{"dump.my_rdb<br/>exists?"}
    LOAD -->|yes| RESTORE["Load into memory<br/><i>Database Loaded From dump.my_rdb</i>"]
    LOAD -->|no| EMPTY["Start empty<br/><i>starting with an empty database</i>"]
    RESTORE --> RUN["🟣 Serving clients"]
    EMPTY --> RUN

    RUN --> BG["⏱️ Background thread<br/>dump every 300s"]
    BG --> RUN
    RUN --> CTRLC{{"Ctrl+C / SIGINT"}}
    CTRLC --> FLUSH["Flush snapshot to disk"]
    FLUSH --> STOP(["πŸ”΄ Graceful shutdown"])

    classDef start fill:#d4f5dd,stroke:#27ae60,stroke-width:2px,color:#0a3d1a
    classDef proc fill:#e7d3fb,stroke:#9b59b6,stroke-width:2px,color:#2d1a4d
    classDef decision fill:#ffe0f0,stroke:#f368e0,stroke-width:2px,color:#3d0a3d
    classDef stop fill:#ffd4d4,stroke:#e74c3c,stroke-width:2px,color:#3d0a0a

    class START start
    class RESTORE,EMPTY,RUN,BG,FLUSH proc
    class LOAD,CTRLC decision
    class STOP stop
Loading

πŸ“ Repository Structure

my_redis_server/
β”œβ”€β”€ include/                         # Public headers
β”‚   β”œβ”€β”€ RedisCommandHandler.h
β”‚   β”œβ”€β”€ RedisDatabase.h
β”‚   └── RedisServer.h
β”œβ”€β”€ src/                             # Implementation
β”‚   β”œβ”€β”€ RedisCommandHandler.cpp
β”‚   β”œβ”€β”€ RedisDatabase.cpp
β”‚   β”œβ”€β”€ RedisServer.cpp
β”‚   └── main.cpp                     # Entry point
β”œβ”€β”€ Concepts,UseCases&Tests.md       # Design concepts & command use cases
β”œβ”€β”€ Makefile                         # Build rules
β”œβ”€β”€ test_all.sh                      # End-to-end test script
└── README.md                        # You are here

⚑ Quick Start

Build

Requires a C++17 (or later) compiler.

# from project root
make

# or compile manually
g++ -std=c++17 -pthread -Iinclude src/*.cpp -o my_redis_server

# clean build artifacts
make clean

Run

./my_redis_server          # listens on 6379 (default)
./my_redis_server 6380     # listens on 6380

On startup you'll see one of:

Database Loaded From dump.my_rdb
# β€” or β€”
No dump found or load failed; starting with an empty database.

Press Ctrl+C to gracefully shut down and persist immediately.

Connect

redis-cli -p 6379
127.0.0.1:6379> PING
PONG
127.0.0.1:6379> SET foo "bar"
OK
127.0.0.1:6379> GET foo
"bar"

πŸ“š Command Reference

Common

Command Syntax Returns
PING PING PONG
ECHO ECHO <msg> <msg>
FLUSHALL FLUSHALL Clears all data

Key / Value

Command Syntax Description
SET SET <key> <value> Store a string
GET GET <key> Retrieve string (or nil)
KEYS KEYS * List all keys
TYPE TYPE <key> string / list / hash / none
DEL / UNLINK DEL <key> Delete a key
EXPIRE EXPIRE <key> <seconds> Set a TTL
RENAME RENAME <old> <new> Rename a key

Lists

Command Syntax Description
LGET LGET <key> All elements
LLEN LLEN <key> Length
LPUSH / RPUSH LPUSH <key> <v1> [v2 ...] Push one or more elements
LPOP / RPOP LPOP <key> Pop one element
LREM LREM <key> <count> <value> Remove occurrences
LINDEX LINDEX <key> <index> Get element by index
LSET LSET <key> <index> <value> Set element by index

Hashes

Command Syntax Description
HSET HSET <key> <field> <value> Set a field
HGET HGET <key> <field> Get a field
HEXISTS HEXISTS <key> <field> Field exists?
HDEL HDEL <key> <field> Delete a field
HLEN HLEN <key> Field count
HKEYS HKEYS <key> All fields
HVALS HVALS <key> All values
HGETALL HGETALL <key> All field/value pairs
HMSET HMSET <key> <f1> <v1> [f2 v2 ...] Set multiple fields

🧠 Design & Architecture

  • Concurrency β€” Each client connection is handled in its own std::thread, allowing many clients to be served simultaneously.
  • Synchronization β€” A single std::mutex db_mutex guards all in-memory stores, guaranteeing consistency across every data type.
  • Data Stores β€” Three typed unordered_maps back strings, lists, and hashes, with a separate expiry_map for TTLs.
  • Expiration β€” Lazy eviction: purgeExpired() runs on each access, so expired keys never surface to a command.
  • Persistence β€” A simplified text-based RDB is dumped to / loaded from dump.my_rdb.
  • Singleton Pattern β€” RedisDatabase::getInstance() enforces a single shared store across all client threads.
  • RESP Parsing β€” A custom parser in RedisCommandHandler accepts both inline and array RESP formats.

πŸ§ͺ Testing

Verify functionality interactively, or run the bundled end-to-end script:

./test_all.sh

See Concepts,UseCases&Tests.md for detailed concepts (TCP sockets, RESP, data structures) and real-world usage scenarios for each command.


Built with πŸ’œ by Tejal Patel

About

A Redis-like in-memory key-value store built in C++ implementing RESP protocol, TTL expiration, persistence, Pub/Sub messaging, and concurrent client handling.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages