A Redis-like in-memory key-value store built from scratch in C++17.
This project implements a TCP server that supports multiple concurrent clients, basic key-value commands, key expiry, LRU eviction, snapshot persistence, and benchmarking.
- TCP server using POSIX sockets
- Multiple concurrent clients handled by a worker thread pool
- Thread-safe shared store using mutex-protected critical sections
- In-memory key-value storage
- LRU eviction using a hash map + doubly linked list
- Key expiry with lazy deletion and background cleanup
- Snapshot persistence to disk
- Simple command protocol usable with
netcat - Benchmark client for throughput measurement
Stores a key-value pair.
SET key value
Example:
SET name alice
Response:
+OK
Gets the value for a key.
GET key
Example:
GET name
Response:
alice
If the key does not exist:
(nil)
Deletes a key.
DEL key
Response:
1
If the key does not exist:
0
Sets a time-to-live for a key.
EXPIRE key seconds
Example:
EXPIRE name 10
Response:
1
Returns the remaining time-to-live for a key.
TTL key
Responses:
-2
Key does not exist.
-1
Key exists but has no expiry.
5
Key expires in 5 seconds.
g++ -std=c++17 -O2 -pthread main.cpp -o kvstore./kvstoreThe server listens on port 6380.
In another terminal, connect using nc:
nc localhost 6380Example session:
SET name alice
+OK
GET name
alice
DEL name
1
GET name
(nil)
The store has a fixed capacity. When the capacity is full, the least recently used key is evicted.
For example, with capacity 3:
SET a 1
SET b 2
SET c 3
GET a
SET d 4
GET b
GET a
GET c
GET d
Expected result:
+OK
+OK
+OK
1
+OK
(nil)
1
3
4
Explanation:
a,b, andcare inserted.GET amakesarecently used.SET d 4exceeds capacity.bis evicted because it is now the least recently used key.
Keys can be given a TTL using EXPIRE.
Example:
SET token abc123
EXPIRE token 3
GET token
Before expiry:
abc123
After 3 seconds:
GET token
(nil)
The store uses two expiry strategies:
- Lazy expiry: expired keys are removed when accessed.
- Active expiry: a background thread periodically removes expired keys.
The store periodically writes a snapshot of the current data to disk.
On startup, the server loads the snapshot file and restores saved keys.
This is a simple snapshot-based persistence system, similar in idea to Redis RDB persistence. Redis also supports AOF, or append-only file persistence, where each write command is logged. This project currently implements only snapshot persistence.
Tradeoff:
- Snapshotting is simple and fast.
- Recent writes may be lost if the server crashes before the next snapshot.
Client connections
|
v
TCP listening socket
|
v
Accepted client sockets
|
v
Worker thread pool
|
v
Command handler
|
v
Mutex-protected store
|
v
Hash map + doubly linked list
|
+--> Expiry background thread
|
+--> Snapshot persistence thread
The store uses two data structures together:
unordered_map<string, Node*> lru_index
This gives O(1) lookup by key.
Doubly linked list
This tracks usage order for LRU eviction.
- Most recently used keys are moved near the head.
- Least recently used keys stay near the tail.
- When capacity is full, the tail node is evicted.
This gives O(1) GET, SET, and eviction behavior.
The server uses a fixed-size worker thread pool.
Accepted client sockets are pushed into a shared queue. Worker threads wait on a condition variable, pop client sockets from the queue, and handle client commands.
The key-value store itself is shared mutable state, so access is protected using a mutex.
Network I/O is kept outside the store lock where possible, so slow clients do not hold the mutex while writing responses.
A Python benchmark client was used to open multiple connections and send many SET and GET commands.
Benchmark configuration:
Total operations: 200000
Threads: 10
Build: g++ -std=c++17 -O2 -pthread main.cpp -o kvstore
Result:
Elapsed time: 1.76 seconds
Throughput: 113737.42 ops/sec
- How to build a TCP server using POSIX sockets
- How
socket,bind,listen,accept,recv, andwritework - Why TCP is a byte stream and not a message-based protocol
- How to handle multiple clients using threads
- How race conditions happen with shared mutable state
- How mutexes and RAII locking protect critical sections
- How condition variables are used in a worker thread pool
- How key expiry works using timestamps and background cleanup
- How LRU eviction works using a hash map and doubly linked list
- How snapshot persistence serializes in-memory data to disk
- How to benchmark throughput using multiple client connections
- The command protocol is simple and line-based, not Redis RESP.
- TCP stream framing is simplified.
- A single global mutex protects the store, so reads and writes are serialized.
- Snapshot persistence may lose recent writes if the server crashes before the next snapshot.
- The snapshot format is simple and assumes keys and values do not contain tabs or newlines.
- The server does not yet support authentication, replication, or clustering.
- Implement proper stream buffering for partial and multiple commands per read
- Support the Redis RESP protocol
- Add append-only log persistence
- Use
std::shared_mutexto allow concurrent reads - Add more benchmark modes
- Add configurable capacity, port, and snapshot interval
- Split the code into multiple files
- Add unit tests for LRU, expiry, and persistence