jonnify·redis /redis-patterns/caching/write-behind/coalescing

R1.03.3 · Write-behind — dive 3

Coalescing writes.

Twenty updates to one key between flushes should cost one database write, not twenty. The async buffer batched by flush; the dirty-set batches by key.

Grounding: echo_cache/journal.ex (record_many/2 group commit, compact/1) · coherence.ex (the 29-byte message) — verbatim. Engine: Valkey.

List versus dirty-set, for the same burst. Select a structure to read how many database writes the flush costs.
the burst key A key A key B key A key B List · every change key A key A key B key A key B DB writes: 5 Set · each key once key A key B (duplicates dropped) DB writes: 2
readout · Select a structure. A List keeps every change, so the flush writes once per entry; a dirty-set keeps each key once, so the flush writes once per distinct key.
§1

Keep each key once

write-behind.md.txt · Advantages

The async buffer batches by flush. Coalescing batches by key. The trick is the buffer’s structure. A List records every change in order, so a key updated five times appears five times, and the flush writes the database five times for that one key. A Set cannot hold a duplicate: SADD a key that is already present and the Set is unchanged. So a dirty-set of changed keys holds each key exactly once, no matter how often it changed.

The value itself is not stored on the buffer — only the key is marked dirty. The flush reads the current value of each dirty key with a normal GET or HGETALL, so it always carries the latest state, never an intermediate one. Twenty updates to one key leave one entry in the set and write the database once with the final value.

# write path — mark the key dirty, store the value (the value, not the key, holds state)
SET  ecc:{limits}:LIM0NgWEfAEJfs  {…}    # store the latest value
SADD dirty  LIM0NgWEfAEJfs               # mark dirty — a Set keeps it once however often it changes
SADD dirty  LIM0NgWEfAEJfs               # the same key again -> Set unchanged, still one entry

# flush path — one write per distinct key, with the current value
SMEMBERS dirty                           # -> {"LIM0NgWEfAEJfs", "LIM0KHTOWnGLuC"} — each once
GET      ecc:{limits}:LIM0NgWEfAEJfs     # read the latest value, then write the database once
dirty-set
A Valkey Set of changed keys. SADD is idempotent, so each key is held once however many times it changed.
coalescing
Collapsing many updates to one key into a single database write, carrying the latest value.
latest-wins
The flush reads the current value, not a queued one — so intermediate states never reach the source.
write amplification
The ratio of database writes to updates. A List keeps it at one; the dirty-set drops it toward the count of distinct keys.

Coalescing changes the durability story a little: the window holds only the latest value of each key, so a crash loses the most recent change, not a sequence of them. The trade itself is the one R1.03.2 measured.

§2

Coalesce a stream of updates

write-behind.md.txt · write coalescing

Feed updates into the dirty-set one at a time and watch it hold each key once. The keys come from a fixed repeating stream where one key is hot. Each update does an SADD; flush writes the database one round per distinct key and clears the set. The readout reports updates seen, the distinct keys held, and the coalescing ratio — how many updates one database write absorbed.

Step the stream. Update and flush over a fixed hot-key stream; the readout reports the coalescing ratio.

A hot key updated again and again still costs one database write per flush. The dirty-set turns update volume into a count of distinct keys.

§3

Coalescing on EchoCache — group commit at the writer’s edge

journal.ex:57 · :106 · coherence.ex:35

EchoCache coalesces in two places. At the cache, the dirty-set keeps each changed key once — the Valkey structure above. At the journal, the equivalent is record_many/2: “group commit at the writer’s edge — record a batch of intents inside one transaction, one WAL append amortized across the batch.” Newer-wins makes a re-applied version harmless, so two intents on the same name collapse to one effect: applying the same version twice is a comparison that answers stale the second time. compact/1 then retires the outbox by coverage — an intent is gone when its name carries an applied version at least as new.

The coherence message is the smallest possible cargo for this: id <> ":" <> version — 14 bytes of branded name, a colon, 14 bytes of branded version, 29 bytes total.

# group commit at the writer's edge — one WAL append across the batch
{:ok, seqs} = Journal.record_many(:limits_journal, triples)
# the 29-byte coherence message — id <> ":" <> version (14 + 1 + 14)
the application · the fair-lanes job queue, in depth

The 29-byte message rides EchoMQ

The coalesced obligation travels as the 29-byte coherence message over EchoMQ’s fair lanes, at-least-once. The lane mechanics — the braced emq:{q}: keyspace, the claim and complete verbs — are taught rung by rung in the EchoMQ course.

/echomq — the protocol, in depth →

coalescing

Mark a key dirty instead of queuing each change; a Set keeps it once. The flush reads the latest value and writes the source one time per distinct key.

on EchoCache

record_many/2 group-commits a batch of intents in one WAL append, and newer-wins collapses repeats on a name to one effect — the 29-byte coherence message is the cargo.

Notes on ValkeySADD returns the count of members actually added, so a re-add of a present key returns 0 and the Set is unchanged — the idempotence the dirty-set relies on (valkey.io/commands/sadd).

Coalescing is the second saving of write-behind: the buffer batches by flush, the dirty-set batches by key, and a burst on one record costs one write.

§4

Recap — the whole module

buffer · window · coalescing

Write-behind acknowledges a write once the cache holds it, defers the source write to a background flush (the async buffer), accepts a bounded window where the cache holds the only copy (the durability trade-off), and collapses repeated writes to one key into a single flush (coalescing). A burst that would have been a database write per update becomes one batch, one write per hot key — throughput up front, the cost deferred and bounded. Return to the hub to revisit any dive.

This closes R1.03 · Write-behind. Return to the module hub or the Caching chapter to continue.

§5

References

sources · related

Sources

Related in this course