Skip to content
About
Main content

Versioned keys are MVCC, and the bill goes somewhere new

Who pays for old versions in a versioned cache, and which session guarantees come for free?

MVCC (multi-version concurrency control) is how many databases let readers and writers work at the same time: each change creates a new version of a row, old versions stay until nobody needs them, and a reader sees a consistent . A versioned is the same thing. The mapping is exact, not just an analogy:

CacheTransaction manager
version embedded in the keyversion chain
superseded entries, now unreachabledead tuples (old row versions)
eviction policyvacuum or undo purge (the cleanup of old versions)
or table fingerprinttransaction or snapshot id
read of (key, v)snapshot read

This is one more appearance of the pattern " storage + an indirection layer + a garbage collector", now inside a cache. It has the same failure mode: produced. High churn produces dead versions fastest, and high churn is exactly when the matters most.

But the question who pays for old versions? gets a new answer here. PostgreSQL bills the writer: old row versions stay in the table and cause bloat. Oracle bills the reader: if a reader needs a version that has been purged, it fails with the error ORA-01555 ("snapshot too old"). A versioned cache bills neither. Dead versions simply use capacity, so the effective capacity falls from C to C/V, where V is the average number of stored versions per key. By The second copy's arithmetic (h ≈ ln C / ln N), the hit rate falls by

versions per key
items in the corpus
N = 10⁶N = 10⁹
Data table
Hit-rate points lost to retained versions (Zipf α ≈ 1)
Versions per key VseriesHit rate lost (points)
1N = 10⁶0
1.04N = 10⁶0.271
1.08N = 10⁶0.542
1.12N = 10⁶0.813
1.16N = 10⁶1.08
1.21N = 10⁶1.36
1.25N = 10⁶1.63
1.3N = 10⁶1.9
1.35N = 10⁶2.17
1.4N = 10⁶2.44
1.45N = 10⁶2.71
1.51N = 10⁶2.98
1.57N = 10⁶3.25
1.63N = 10⁶3.52
1.69N = 10⁶3.79
1.75N = 10⁶4.07
1.82N = 10⁶4.34
1.89N = 10⁶4.61
1.96N = 10⁶4.88
2.04N = 10⁶5.15
2.11N = 10⁶5.42
2.2N = 10⁶5.69
2.28N = 10⁶5.96
2.37N = 10⁶6.23
2.46N = 10⁶6.51
2.55N = 10⁶6.78
2.65N = 10⁶7.05
2.75N = 10⁶7.32
2.85N = 10⁶7.59
2.96N = 10⁶7.86
3.08N = 10⁶8.13
3.19N = 10⁶8.4
3.31N = 10⁶8.67
3.44N = 10⁶8.94
3.57N = 10⁶9.22
3.71N = 10⁶9.49
3.85N = 10⁶9.76
4N = 10⁶10
4.15N = 10⁶10.3
4.31N = 10⁶10.6
4.47N = 10⁶10.8
4.64N = 10⁶11.1
4.82N = 10⁶11.4
5N = 10⁶11.7
5.19N = 10⁶11.9
5.39N = 10⁶12.2
5.6N = 10⁶12.5
5.81N = 10⁶12.7
6.03N = 10⁶13
6.26N = 10⁶13.3
6.5N = 10⁶13.6
6.75N = 10⁶13.8
7.01N = 10⁶14.1
7.28N = 10⁶14.4
7.55N = 10⁶14.6
7.84N = 10⁶14.9
8.14N = 10⁶15.2
8.45N = 10⁶15.4
8.77N = 10⁶15.7
9.11N = 10⁶16
9.46N = 10⁶16.3
9.82N = 10⁶16.5
10.2N = 10⁶16.8
10.6N = 10⁶17.1
11N = 10⁶17.3
11.4N = 10⁶17.6
11.8N = 10⁶17.9
12.3N = 10⁶18.2
12.8N = 10⁶18.4
13.2N = 10⁶18.7
13.8N = 10⁶19
14.3N = 10⁶19.2
14.8N = 10⁶19.5
15.4N = 10⁶19.8
16N = 10⁶20.1
16.6N = 10⁶20.3
17.2N = 10⁶20.6
17.9N = 10⁶20.9
18.6N = 10⁶21.1
19.3N = 10⁶21.4
20N = 10⁶21.7
1N = 10⁹0
1.04N = 10⁹0.181
1.08N = 10⁹0.361
1.12N = 10⁹0.542
1.16N = 10⁹0.723
1.21N = 10⁹0.903
1.25N = 10⁹1.08
1.3N = 10⁹1.26
1.35N = 10⁹1.45
1.4N = 10⁹1.63
1.45N = 10⁹1.81
1.51N = 10⁹1.99
1.57N = 10⁹2.17
1.63N = 10⁹2.35
1.69N = 10⁹2.53
1.75N = 10⁹2.71
1.82N = 10⁹2.89
1.89N = 10⁹3.07
1.96N = 10⁹3.25
2.04N = 10⁹3.43
2.11N = 10⁹3.61
2.2N = 10⁹3.79
2.28N = 10⁹3.98
2.37N = 10⁹4.16
2.46N = 10⁹4.34
2.55N = 10⁹4.52
2.65N = 10⁹4.7
2.75N = 10⁹4.88
2.85N = 10⁹5.06
2.96N = 10⁹5.24
3.08N = 10⁹5.42
3.19N = 10⁹5.6
3.31N = 10⁹5.78
3.44N = 10⁹5.96
3.57N = 10⁹6.14
3.71N = 10⁹6.32
3.85N = 10⁹6.51
4N = 10⁹6.69
4.15N = 10⁹6.87
4.31N = 10⁹7.05
4.47N = 10⁹7.23
4.64N = 10⁹7.41
4.82N = 10⁹7.59
5N = 10⁹7.77
5.19N = 10⁹7.95
5.39N = 10⁹8.13
5.6N = 10⁹8.31
5.81N = 10⁹8.49
6.03N = 10⁹8.67
6.26N = 10⁹8.85
6.5N = 10⁹9.03
6.75N = 10⁹9.22
7.01N = 10⁹9.4
7.28N = 10⁹9.58
7.55N = 10⁹9.76
7.84N = 10⁹9.94
8.14N = 10⁹10.1
8.45N = 10⁹10.3
8.77N = 10⁹10.5
9.11N = 10⁹10.7
9.46N = 10⁹10.8
9.82N = 10⁹11
10.2N = 10⁹11.2
10.6N = 10⁹11.4
11N = 10⁹11.6
11.4N = 10⁹11.7
11.8N = 10⁹11.9
12.3N = 10⁹12.1
12.8N = 10⁹12.3
13.2N = 10⁹12.5
13.8N = 10⁹12.6
14.3N = 10⁹12.8
14.8N = 10⁹13
15.4N = 10⁹13.2
16N = 10⁹13.4
16.6N = 10⁹13.6
17.2N = 10⁹13.7
17.9N = 10⁹13.9
18.6N = 10⁹14.1
19.3N = 10⁹14.3
20N = 10⁹14.5

That cost is spread over the whole population of readers as a slightly worse experience. It is the clearest example of a cost that is relocated onto everybody, diffusely (costs are conserved and only move). It is also the hardest kind of cost to attribute, so it is the kind most likely to last for years. Nobody files a ticket about five points of hit rate they never knew they had.

Session guarantees come for free

Versioned keys also give you two session guarantees at no extra cost:

  • Read-your-writes: after you write, your own reads see your write. If the client keeps the version token from its last write and puts it in the key, then a cache that does not have that version simply misses and goes to the store.
  • Monotonic reads: you never see data go backwards in time. The client refuses to accept a version lower than the highest it has already seen.

Both use only client-side state, which is why they are cheap. They are also the only sound way to get session guarantees over an incoherent cache. Sticky routing (always sending one user to the same cache node) is the weaker substitute. It breaks at the first rebalance, and Making it fit shows that rebalances are routine, not exceptional.

Compression: Versioned keys are MVCC with eviction as the vacuum. The retention cost is shared by all readers as ln V / ln N points of hit rate. The version token in the client's hand buys .