Skip to main content

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 SCAN sees 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:

LevelWhat it locks
KeyA whole structure
FieldOne field within a hash or document
Frame row or columnA 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:

LevelExample
KeyThe whole structure expires
FieldOne field of a hash expires, leaving the rest
MemberOne member of a set or sorted set
PathA path inside a JSON document
Row and columnWithin 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.