Arc<Mutex<HashMap>> and the dispatch match

Before the code: what a key-value store actually is

So far the server understands the Redis language but has nothing to remember. Time to give it a brain. The brain is a HashMap that lives in memory, plus a small companion table of timers for keys that should disappear on their own. That is the whole Redis storage model, in plain English. The Rust twist is that you cannot just share a HashMap between async tasks. You wrap it in Arc and a Mutex so the compiler lets every task hold its own handle and only one of them touches the map at a time.

The store, with and without timers

A plain HashMap remembers forever. A HashMap plus a timer table is what makes Redis a cache, a session store, and a rate limiter.

Quiz: Quiz

Loading practice…

Rust will not let you share &mut HashMap across tasks. The fix is Arc<Mutex<HashMap>>: Arc gives every task a clone of the handle; Mutex gives them exclusive access one at a time.

03-get-set/src/main.rs
rust
#[derive(Default)]
struct StoreInner {
    store: HashMap<String, String>,
}

type Store = Arc<Mutex<StoreInner>>;

async fn handle_get(store: &Store, key: &str) -> Option<String> {
    store.lock().await.store.get(key).cloned()
}

async fn handle_set(store: &Store, key: String, value: String) {
    store.lock().await.store.insert(key, value);
}

StoreInner holds the HashMap. Arc<Mutex<>> wraps it. Every handler clones the Arc cheaply (just bumps a refcount).

03-get-set/src/main.rs
rust
match name.as_str() {
    "PING" => encode_simple("PONG"),
    "SET" => {
        if rest.len() < 2 { encode_error("ERR wrong number of arguments") }
        else { store.lock().await.store.insert(rest[0].clone(), rest[1].clone()); encode_simple("OK") }
    }
    "GET" => encode_bulk(store.lock().await.store.get(&rest[0]).map(String::as_str)),
    "DEL" => { /* ... */ }
    _ => encode_error(&format!("ERR unknown command '{}'", args[0])),
}

Dispatch via match on the command name. No HashMap<&str, fn>, just match. Compiler optimises it into a jump table.

Matching exercise: Pick the right Rust concurrency primitive

Loading practice…

Why tokio::sync::Mutex and not std::sync::Mutex here? Because we lock, then call async store methods that may await. Holding a std Mutex across an await deadlocks the executor: the task suspends with the lock held, the executor cannot make progress on the waiter. tokio::sync::Mutex is await-safe.