Concurrency and expiry
Two engineering properties separate Structures from the cache it is wire-compatible with, and both are about what happens when more than one caller touches the same data.
Snapshot isolation, without fork()
Redis takes point-in-time snapshots by forking the process, which copies the page table and relies on copy-on-write. It works, and it has well-known costs: a latency spike at fork time, and memory amplification when the parent writes heavily during the snapshot.
Structures uses multi-version concurrency control instead:
- Non-blocking reads through versioned snapshots, with no
fork()required. - Wait-free reads against snapshot copies, so a reader never waits on a writer.
- Point-in-time snapshots for key enumeration and scanning, so a
SCANsees a consistent view rather than a moving target. - Transaction rollback through an operation log.
The practical consequence: a long scan does not block writers, a heavy write does not stall readers, and taking a consistent view of the keyspace does not require duplicating the process.
Fine-grained locking
Locking is not per-key-or-nothing. The lock manager reaches three levels:
| Level | What it locks |
|---|---|
| Key | A whole structure |
| Field | One field within a hash or document |
| Frame row or column | A single row or column of a dataframe structure |
That granularity is the difference between two callers updating different fields of the same hash concurrently, and one of them waiting.
Four properties around it:
- Deadlock detection, so a cycle is found rather than waited out.
- Lock timeout, so a stuck holder does not block a queue indefinitely.
- FIFO queueing, so waiters are served in order rather than starved.
- Fencing tokens, so a holder that has lost its lock (through a timeout or a failover) cannot complete a write as if it still held it.
Fencing tokens are the one people skip. Without them, a process that stalls, loses its lease, and then wakes up will happily finish its write on top of whoever took the lock next. A token makes that write detectably stale and rejected.
Transactions
MULTI/EXEC transactions run with multi-version concurrency control, snapshot
isolation and WATCH, so an optimistic transaction aborts when its watched keys
changed underneath it rather than silently applying to a different state than it
was planned against.
Expiry reaches further than a key
TTL is driven by a hierarchical timing wheel, and the levels it reaches are the notable part:
| Level | Example |
|---|---|
| Key | The whole structure expires |
| Field | One field of a hash expires, leaving the rest |
| Member | One member of a set or sorted set |
| Path | A path inside a JSON document |
| Row and column | Within a frame structure |
A single field can expire without dropping the record around it. That is genuinely unusual, and it is what lets a structure hold a mix of durable and ephemeral state rather than forcing a split across two keys with two lifetimes.
Expiry is instrumented, so expiry behaviour is observable rather than something you infer from data going missing.
Eviction, when memory runs short
Ten eviction policies govern what goes when a namespace is under pressure, and spill is the alternative to eviction: a cold key can move to warm and cold tiers instead of being dropped.
The distinction matters. Eviction loses data. Spill relocates it and restores it transparently on access. Which one applies is a policy decision rather than an engine limitation.
Related
- Structures, the type surface.
- Distribution and protection, spill, replicas and the fence.
- The wire, how a client reaches all of this.
- Data lifecycle, TTL and tombstones across the platform.