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.
40+ commands Β β’Β 3 data types Β β’Β redis-cli compatible Β β’Β thread-safe
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 |
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
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
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<string, string><br/><i>Strings</i>"]
LIST["<b>list_store</b><br/>unordered_map<string, vector<string>><br/><i>Lists</i>"]
HASH["<b>hash_store</b><br/>unordered_map<string,<br/>unordered_map<string,string>><br/><i>Hashes</i>"]
EXP["<b>expiry_map</b><br/>unordered_map<string, time_point><br/><i>TTLs</i>"]
end
classDef store fill:#f3d9ff,stroke:#c471ed,stroke-width:2px,color:#3d0a3d
class KV,LIST,HASH,EXP store
Lazy expiration: TTLs aren't reaped by a timer. Instead,
purgeExpired()runs on each key access β if a key's deadline inexpiry_maphas passed, it's evicted on the spot before the command sees it.
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
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
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./my_redis_server # listens on 6379 (default)
./my_redis_server 6380 # listens on 6380On 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.
redis-cli -p 6379127.0.0.1:6379> PING
PONG
127.0.0.1:6379> SET foo "bar"
OK
127.0.0.1:6379> GET foo
"bar"
| Command | Syntax | Returns |
|---|---|---|
PING |
PING |
PONG |
ECHO |
ECHO <msg> |
<msg> |
FLUSHALL |
FLUSHALL |
Clears all data |
| 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 |
| 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 |
| 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 |
- Concurrency β Each client connection is handled in its own
std::thread, allowing many clients to be served simultaneously. - Synchronization β A single
std::mutex db_mutexguards all in-memory stores, guaranteeing consistency across every data type. - Data Stores β Three typed
unordered_maps back strings, lists, and hashes, with a separateexpiry_mapfor 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
RedisCommandHandleraccepts both inline and array RESP formats.
Verify functionality interactively, or run the bundled end-to-end script:
./test_all.shSee Concepts,UseCases&Tests.md for detailed concepts (TCP sockets, RESP, data structures) and real-world usage scenarios for each command.