Keeping an in-memory database alive; caching and memcached

After this lesson you will say what Redis does so that a crash or a dead machine costs little — snapshots, logs, slaves, Sentinel, Cluster and its 16384 slots — and when a cache that forgets on purpose, like memcached, is worth having at all.

Two programs keep all their data in RAM, and you pull the plug on both. The slide of the first is a list of machinery for surviving exactly that: snapshots on disk, a log of every write, copies on other machines, and automatic ways to put a copy in charge when a machine dies (p. 359). The slide of the second reads like a confession: "No persistency (if your server goes down, data in memcached is gone)", "No redundancy / fail-over", "No replication (single item in cache lives on one server only)" (p. 381).

Yet four slides earlier the second program is "used by Netlog, Facebook, Flickr, Wikipedia, Twitter, YouTube" (p. 377). Which of the two is badly designed?

Neither — and the reason sits in a single word of the definition of a cache. By the end of the lesson you will know that word, the four kinds of machinery the first program needs, and the one ratio that decides whether the second is worth having at all.

Four ways to keep RAM from forgetting

Redis answers from memory, which is why it is fast (lesson 17). Memory has two weaknesses: it forgets everything when the process or the machine stops, and one machine has only so much of it. The lecture is frank about the price of fixing the first: "increased persistency will tend to increase latency and decrease throughput" (p. 349).

Page 359, "Scaling Redis", answers with four headings, and each one guards against a different accident (Fig. 1):

Persistence — "Redis provides two mechanisms to deal with persistence: Redis database snapshots (RDB) and append-only files (AOF)." Against: the process stops and the RAM is wiped.

Replication — an instance known as the master "ensures that one or more instances … the slaves, become exact copies of the master. Clients can connect to the master or to the slaves. Slaves are read only by default." Against: a whole machine is lost.

Partitioning — "Breaking up data and distributing it across different hosts in a cluster", in one of three layers: the client ("Partitioning on client-side code"), a proxy ("An extra layer that proxies all redis queries and performs partitioning", e.g. Twemproxy) or a query router ("instances will make sure to forward the query to the right node", e.g. Redis Cluster). Against: the data outgrows one machine.

Failover — "Manual", "Automatic with Redis Sentinel (for master-slave topology)" or "Automatic with Redis Cluster (for cluster topology)". Against: the master dies and nobody accepts writes.

RAM wiped Redis RDB · AOF persistence machine lost master slave replication too much data part 1 part 2 part 3 partitioning master dies promote master slave, now master failover
Fig. 1. The four headings of p. 359, each against its own accident. A dashed ochre outline marks the master: the copy that takes the writes.

Persistence survives a restart, replication survives a lost machine, partitioning survives growth, failover keeps the writes going when the master dies.

Snapshot or log?

The slide only names the two; the mechanics are from the Redis documentation. An RDB snapshot is a photograph: every few minutes, or sooner when many keys change, Redis writes a point-in-time copy of the whole dataset into one compact file. An AOF is a ledger: every write command is appended to a log, which Redis replays at restart.

A crash therefore costs different amounts (Fig. 2). The snapshot loses everything written since it was taken — minutes. The log loses only what had not reached the disk: about a second with the default policy (one flush per second), nothing if the file is synced after every write. That last setting is the "increased persistency" of p. 349: every write waits for the disk. In exchange, a snapshot is smaller and faster to reload than a log of every command. With both switched on, Redis restarts from the log, the more complete of the two.

writes before the crash snapshot crash time after the restart lost

RDB: a snapshot every few minutes

writes, each appended to the log crash time after the restart: the log replayed lost: last 1 s

AOF: a log of every write

Fig. 2. The same eight writes and the same crash. Left: the restart reloads the last snapshot, and everything written after it is gone. Right: the restart replays the log; with the default policy (flush once a second) at most the last second is lost. The slide names the two files; the mechanics are from the Redis documentation.

Four topologies on one two-by-two

Page 360 lists four arrangements, each with its promise in brackets (the rendering jumbles the words): "I. Standalone", "II. Sentinel (automatic failover)", "III. Twemproxy (distribute data)", "IV. Cluster (automatic failover and distribute data)". Two capabilities, four combinations.

I · Standalone: copies, and nobody to promote them

Page 361 draws three shapes: a master alone, a master with slaves, and slaves of slaves ("multi-level"). The slaves give "data redundancy, reads offloading and save-to-disk offloading": a copy if the master's machine dies, more machines for reads, and a machine that writes the snapshots and logs instead of the master. "Clients can connect to the Master for read/write operations or to the Slaves for read operations." Then the line that matters: "There is no automatic failover."

Why read-only slaves? One machine orders the writes, so the copies never disagree about them. The copying is asynchronous (from the Redis documentation): the master answers before the slaves have the write, so a slave promoted after a crash may lack the last writes.

II · Sentinel: a watchdog that promotes

"Redis Sentinel provides a reliable automatic failover in a master/slave topology, automatically promoting a slave to master if the existing master fails" (p. 362). The figure puts a Sentinel on each of three servers, watching the master and the slaves and talking to each other. Why several? So that one Sentinel with a broken cable cannot depose a healthy master: the Redis documentation asks for at least three, and a failover needs the vote of a majority of them. The second bullet: "Sentinel does not distribute data across nodes" — every slave still holds the whole dataset.

III · Twemproxy: a proxy that splits

"Twemproxy works as a proxy between the clients and many Redis instances" — the second picture runs three of them behind a load balancer — and "Is able to automatically distribute data among different standalone Redis instances" (p. 363). It "Supports consistent hashing": servers and keys are hashed onto the same circle, and a key belongs to the first server point after it, clockwise; when a server joins or leaves, only the keys next to its points move, about one in n for n servers, where "hash mod the number of servers" would move almost all. Twemproxy "is a project from Twitter and is not part of redis"; "Multi-key commands and transactions are not supported" — such a command may need several servers — and there is no failover: a dead instance takes its keys with it.

"Not supported" — really none of them?

Twemproxy's own notes are finer: it splits MGET, MSET and DEL by server (MSET is then not atomic); SINTER or RPOPLPUSH work only when all their keys hash to one server, which hash tags arrange; MULTI/EXEC transactions do not work at all. At the exam, give the slide's sentence.

IV · Cluster: slots with owners

"Redis Cluster distributes data across different Redis instances and perform automatic failover if any problem happens to any master instance" (p. 364). "All nodes are directly connected with a service channel": they check on each other and agree on who is alive. "The keyspace is divided into hash slots. Different nodes will hold a subset of hash slots." The figure adds the numbers — "Slots 0-6000", "Slots 6001-12000", "Slots 12001-16383": 16384 hash slots in all, each owned by one master. Which slot a key falls into is fixed by a formula of the Redis Cluster specification:

\[ \text{slot}(k) = \text{CRC16}(k) \bmod 16384 \]
\(k\)
the key, as a string
CRC16
a 16-bit checksum of the key's bytes, a number from 0 to 65535

Key foo lands in slot 12182, so in the slide's cluster it lives on master C. When a master fails, one of its slaves is promoted and takes over its range of slots; the formula never changes, so every key keeps its slot. The last bullet: "Multi-key commands are only allowed for keys in the same hash slot." A command that needs two keys must run on one machine, atomically. user:42:name and user:42:city land in slots 6755 and 8210 — both on master B, yet the command is refused, because slots, not machines, are the unit that moves between masters:

redis> MGET user:42:name user:42:city
(error) CROSSSLOT Keys in request don't hash to the same slot

The way out is a hash tag: when a key contains {…}, only the part inside the first pair of braces is hashed. {user:42}:name and {user:42}:city both hash user:42 and share slot 15880:

redis> MSET {user:42}:name "Ada" {user:42}:city "Milano"
OK

Now kill some masters yourself. The widget shows one insight: copies are not failover, and failover is not distribution — only Cluster has both.

Kill the master: the four topologies, one key

Pick a topology and the keys of one command (or type your own). The drawing shows which machine answers your first key; below it, where the keys fall: in a hash slot of the Cluster, or on the ring of the proxy. Then kill the master that holds your key and read what still works. Slots are computed by the widget with CRC16, as Redis Cluster does; the proxy's ring is a simplified illustration, hashed with CRC16 too, with two points per master.

  1. Start with "I · standalone: master-slave" and press Kill the master that holds your key. Before you look: can clients still read? Can they write?
    What you should see

    Reads, yes: the slaves still answer from their copies, which may miss the last writes. Writes, no: "Slaves are read only by default" (p. 359) and "There is no automatic failover" (p. 361). Press Promote a slave by hand: writes come back, after an outage as long as it takes a human to notice. That is the "Manual" failover of p. 359.

  2. Switch to "II · Sentinel" and kill the master again. Predict who takes over, and whether the readout "the data" changes.
    What you should see

    Slave 1 becomes master with nobody pressing a button: the Sentinels agree that the master is down and promote it, and slave 2 starts copying the new master. "The data" still says every key is on one master: "Sentinel does not distribute data across nodes" (p. 362).

  3. "III · Twemproxy" with the key foo: kill the master that holds it. Which keys become unreachable, and does anything take over?
    What you should see

    Only the keys of that one master — on this ring, foo (position 44950) belongs to master 2. The other two masters keep answering, and nobody takes over: Twemproxy distributes data but has no failover. Distribution and failover are two separate capabilities, which is the whole point of the two-by-two below.

  4. "IV · Cluster" with user:42:name user:42:city. Predict whether a command on both keys runs. Then choose the pair with {user:42} and kill the master that holds it.
    What you should see

    Refused: slots 6755 and 8210 are different, although both sit on master B. With the hash tag both keys hash user:42, slot 15880, and the command runs. Killing master C promotes its slave C1, which now owns slots 12001–16383: the slots are re-owned, and the key's slot does not move.

The four topologies of p. 360 on two separate questions.
every master holds all the datadata distributed over masters
no automatic failoverI · Standalone (master only, master-slave, multi-level)III · Twemproxy (consistent hashing; no multi-key commands or transactions)
automatic failoverII · Sentinel (slaves promoted by vote)IV · Cluster (16384 slots; multi-key only within one slot)

"Redis is highly available." The advantages slide (p. 365) lists "Availability" and "Fault-Tolerance" — true only for the bottom row of the table: of a standalone deployment p. 361 says "There is no automatic failover."

When a cache pays

Back to the hook's second program. The slides define a cache with Wikipedia (p. 367): "A cache is a collection of data duplicating original values stored elsewhere or computed earlier, where the original data is expensive to fetch (owing to longer access time) or to compute, compared to the cost of reading the cache" — a term they credit to IBM, in the 1960s. Its anatomy (p. 368) is the key-value store of lesson 17 cut to the bone: "simple key/value storage" and three operations, save, get, delete.

The vocabulary of p. 369, with what each name means:

Storage cost — the memory spent on holding the copies.

Retrieval cost — the price of one look into the cache: "network load / algorithm load", a hop to another server or the work of the lookup itself.

Invalidation — "keeping data up to date / removing irrelevant data": throwing away copies whose original has changed.

Replacement policy — which entry to evict when the cache is full: "FIFO/LFU/LRU/MRU/RANDOM vs. Belady's algorithm". FIFO evicts the oldest entry, LFU the least frequently used, LRU the least recently used, MRU the most recently used, RANDOM any entry. Belady's algorithm (Laszlo Belady, IBM, 1966) evicts the entry that will be needed again furthest in the future: the best possible choice, and impossible in practice, because it needs to know the future. It is the yardstick for the others, the way the Bayes classifier of statistics is for real classifiers: optimal, and out of reach without information you do not have.

Cold cache / warm cache — just started and empty, so nearly every request misses; or filled with the entries the requests keep asking for.

A request that finds its key is a cache hit; one that does not is a miss, and goes to the original. Page 370 takes 45 hits and 10 misses:

\[ \begin{aligned} h &= \frac{\text{hits}}{\text{hits} + \text{misses}} \\ &= \frac{45}{45 + 10} \approx 0.818 \end{aligned} \] \[ \text{miss ratio} = 1 - h \approx 0.182 \]

The slide rounds them to "82% hit ratio" and "18% miss ratio".

More hits than misses?

Page 371, "When to cache?", gives a principle and a shortcut: "caches are only efficient when the benefits of faster access outweigh the overhead of checking and keeping your cache up to date" — "more cache hits then cache misses". The derivation below, not on the slides, turns the principle into a number. Call \(t_c\) the cost of one look into the cache and \(t_o\) the cost of the original, the database query:

  1. A hit costs only the look: \(t_c\). A miss costs the look and then the trip to the original: \(t_c + t_o\).

  2. A fraction \(h\) of the requests hit and \(1 - h\) miss, so the average request costs

    \[ T = h\,t_c + (1-h)(t_c + t_o) . \]
  3. Collect \(t_c\), which every request pays:

    \[ T = t_c + (1-h)\,t_o . \]
  4. Without the cache every request costs \(t_o\). The cache pays when \(T \lt t_o\):

    \[ t_c + (1-h)\,t_o \lt t_o \iff h \gt \frac{t_c}{t_o} . \]
  5. So the break-even hit ratio is \(h^* = t_c / t_o\): the price of a look, measured in trips to the original. "More hits than misses", \(h \gt 0.5\), is the same rule only when a look costs half a trip. (Writing the copies and invalidating them adds to \(t_c\): the "keeping your cache up to date" of the slide.)

A cache in front of a database: hits, misses and stale reads

A stream of 3000 requests for the profiles of 100 users (the getUserData of p. 386), ten per second; about 2 in 100 are writes, a user changing a preference. The skew sets how much the popular users dominate. The plot shows the average time per read, computed from the simulated hit ratio and your two costs; the strip shows the first and the last 60 reads. Nothing here is a number of the slides: the stream is simulated, and the costs are yours.

  1. Set the skew to 0: all users equally popular. Before looking, will the hit ratio be above or below 0.5 — and will the cache still pay at \(t_c = 1\) ms, \(t_o = 50\) ms?
    What you should see

    About 0.2 (0.198): far fewer hits than misses. The cache still pays: \(1 + 0.802 \times 50 \approx 41\) ms instead of 50 ms, because the break-even is \(1/50 = 0.02\). This is the insight of the widget: "more hits than misses" was never the condition, \(h \gt t_c / t_o\) is. The first row of the strip shows the cold start: an empty cache misses almost everything.

  2. Skew 1.2 gives more hits than misses (\(h \approx 0.70\)). Now make the database fast and the cache far: \(t_o = 2\) ms, \(t_c = 1.5\) ms. Predict the verdict.
    What you should see

    The cache loses: \(1.5 + 0.296 \times 2 \approx 2.09\) ms against 2 ms. The break-even is \(1.5 / 2 = 0.75\), above the 0.70 this cache reaches — more hits than misses, and still slower than no cache at all.

  3. Press Back to the defaults, set the skew to 1.0 and the size to 10. Rank FIFO, LRU, LFU, MRU, RANDOM and Belady before you try them.
    What you should see

    Belady 0.611, LFU 0.513, LRU 0.379, FIFO 0.336, RANDOM 0.332, MRU 0.144. Belady is the ceiling: it knows the future. In this stream the popularity never changes, so LFU, which keeps the most popular users, comes closest. MRU throws away exactly the entry just used — the worst choice when the same users come back. (In real traffic popularity drifts, and LRU follows it better than LFU.)

  4. Set the size to 40 (skew 1.0, LRU, TTL "never expires") and read the stale hits. Then set the TTL to 10 s; then switch to event-based invalidation.
    What you should see

    Never expiring: \(h \approx 0.75\), but 1182 stale hits, 40 % of all reads — popular users stay cached for ever and their changes never show. TTL 10 s: 149 stale hits (5 %), but \(h\) drops to 0.64. Event-based: zero stale hits with \(h \approx 0.735\). A shorter TTL buys freshness with hits and never reaches zero; deleting on every write reaches zero at the price of one miss after each write.

Where caches live

Caches sit wherever something slow stands behind something fast. Page 372 lists hardware ("cpu, hdd"), operating systems ("ram"), the "web stack", applications — even "your own short term vs long term memory". The web stack holds a ladder of them (p. 373): browser cache, DNS cache, Content Delivery Networks, proxy servers and, in the application, "full output caching" and the data caching of section 5. Along the ladder (p. 374), "the closer to the original request(er), the faster" — "but probably also the harder to get it right", since "the closer to the requester the more parameters the cache depends on": a copy in your browser depends on you, your cookies, your language; one next to the database serves every user.

Memcached: a cache that forgets on purpose

Memcached is the cache of the slides: a "Free & open source, high-performance, distributed memory object caching system", "Generic in nature, intended for use in speeding up dynamic web applications by alleviating database load", a "key/value dictionary" (p. 376). It was "Developed by Brad Fitzpatrick for LiveJournal in 2003" (p. 377).

LiveJournal then ran on about 70 machines for 2.5 million accounts. In his 2004 article in Linux Journal, Fitzpatrick wrote that he wanted "a global hash table that all Web processes on all machines could access simultaneously", and reported servers up 4–5 months at a time, averaging 7,000 queries per second.

"Technically" (p. 378): "It's a server", reached by "Client access over TCP or UDP". "Servers can run in pools": "eg. 3 servers with 64GB mem each give you a single pool of 192GB storage for caching". And the design decision: "Servers are independent, clients manage the pool". No server knows the others; the client library hashes the key and picks the server (Fig. 3) — the client layer of partitioning from p. 359. Each key has one home, so every server added adds its whole memory.

client get('user_42') server 1 · 64 GB server 2 · 64 GB server 3 · 64 GB 3 × 64 GB = one pool of 192 GB the client picks the server
Fig. 3. A memcached pool as p. 378 describes it. The servers never talk to each other: the client library hashes the key and sends the request to the one server that holds it.

What deserves a place in it (pp. 379–380): data that is in "high demand (used often)", "expensive (hard to compute)" and "common", shared across users — "Best: All three". The typical tenants: "user sessions (often)", "user data (often, shared)", "homepage data (eg. often, shared, expensive)".

Its principles are its limits

Page 381 opens with one strength, "Fast network access (memcached servers close to other application servers)", then four refusals: "No persistency (if your server goes down, data in memcached is gone)", "No redundancy / fail-over", "No replication (single item in cache lives on one server only)", "No authentication (not in shared environments)". Page 382 adds limits and two more refusals: "1 key is maximum 1MB", "keys are strings of 250 characters (in application typically MD5 of user readable string)", "No enumeration of keys (thus no list of valid keys in cache at certain moment)", "No active clean-up (only clean up when more space needed, LRU: Least Recently Used )".

Read the list against p. 359: by design, memcached lacks three of the four kinds of machinery of section 1 — persistence, replication, failover — and keeps only the simplest form of the fourth, partitioning done by the client. It does not need the other three: every value in it duplicates something stored elsewhere, so a dead server costs time — its keys miss until reloaded from the database — not data. Its client API (p. 383) is the small vocabulary of any key-value store: add, set, get, getMulti, delete, increment, decrement, flush, plus addServer to grow the pool and getStats to watch it.

Memcached and Redis side by side

Page 365 lists Redis's advantages: "Performance", "Availability", "Fault-Tolerance", "Scalability (adaptability)", "Portability"; "Support for complex data types and structures", "Atomic operations and Transactions", "Pipelining (send multiple commands at once)", "LUA scripting support", "LRU eviction of keys", "Keys with limited time-to-live"; and an open-source server, simple to set up, with clients in many languages. Against memcached's principles:

What memcached refuses and what Redis offers, page by page.
memcachedRedis
valuesa "key/value dictionary" of opaque items, at most 1 MB each (pp. 376, 382)data structures: strings, hashes, lists, sets, sorted sets… (p. 354)
persistence"No persistency" (p. 381)RDB snapshots and AOF log (p. 359)
replication"No replication": one server per item (p. 381)master → read-only slaves (p. 359)
failover"No redundancy / fail-over" (p. 381)manual, Sentinel or Cluster (p. 359)
partitioning"clients manage the pool" (p. 378)client, proxy (Twemproxy) or Cluster (p. 359)
clean-upLRU, only when space is needed (p. 382)"LRU eviction of keys", "Keys with limited time-to-live" (p. 365)
speed (Fig. 4)80,00080,000; pipelined 250,000

The last row is the chart of Redis's advantages slide (p. 365), redrawn in Fig. 4. It prints no unit and no description of the benchmark, and comes from a vendor's blog (redislabs.com): read it for orders of magnitude.

Redis pipelined Redis Memcached Couchbase Memcached VoltDB/MemSQL/HANA Cassandra MongoDB Riak/Dynamo MySQL 250,000 80,000 80,000 44,000 17,000 12,000 3,500 2,700 900 3 times more no unit printed on the slide
Fig. 4. The chart of p. 365, "NoSQL & SQL response performance comparison", values as printed; solid bars are Redis. The slide also prints the response time of each figure: under 1 ms for Redis and Memcached, under 20 ms for Couchbase Memcached, 10–50 ms for VoltDB/MemSQL/HANA and Cassandra, under 100 ms for MongoDB and Riak/Dynamo, over 100 ms for MySQL.

Two facts to take from it. Redis and Memcached tie at 80,000: memcached's refusals buy simplicity, not speed. And "Redis pipelined" reaches 250,000, three times as much. Pipelining, "send multiple commands at once" (p. 365), removes waiting: instead of one network round trip per command, the client sends a batch and collects the replies together. Page 393 makes the same point for reads: even a hit costs a trip to the memcached server, so use multiget, "fetch multiple keys from memcached in one single call" — the "result is array of items". Redis's MGET does the same. In PHP it is the getMulti of p. 383 (the line is ours):

<?php
// multiget: three keys, one round trip; returns an array of the items found
$users = $cache->getMulti(array('user_1', 'user_2', 'user_3'));
?>

Cache-aside in code, and the two hard things

The slides show memcached at work in PHP. The first program caches a whole page, the "full output caching" of p. 373 (p. 384):

<?php
// p. 384: prints the page, built only on a miss
$html = $cache->get('mypage');
if (!$html)
{
  ob_start();
  echo "<html>";
  // all the fancy stuff goes here
  echo "</html>";
  $html = ob_get_contents();
  ob_end_clean();
  $cache->set('mypage', $html);
}
echo $html;
?>

Read it aloud: ask the cache for 'mypage'; if nothing comes back, build the page (PHP's output buffer, ob_start … ob_get_contents, captures everything echoed in between), store it under the key, print it. The next visitor gets the stored copy. This shape — look in the cache; on a miss compute or fetch, store, return — is called cache-aside (the name is not on the slides; Fig. 5).

get(key) hit? return value query the database set(key, value) hit miss store a copy the next get(key) is a hit
Fig. 5. Cache-aside, the pattern of every PHP example of the slides: the application, not the cache, goes to the database on a miss and stores what it found.

Caching a whole page is fast but fragile: the page depends on everything shown on it. Page 385 moves the cache "on a lower level", to data caching: "easier to find all dependencies", the "ideal solution for offloading database queries", because "the database is almost always the biggest bottleneck in backend performance problems". The slide's code caches one user's row (p. 386; the key in set is restored, see the red pen):

<?php
// p. 386: the row of user $UID, from the cache if there
function getUserData($UID)
{
  $key = 'user_' . $UID;
  $userData = $cache->get($key);
  if (!$userData)
  {
    $queryResult = Database::query(
      "SELECT * FROM USERS WHERE uid = " . (int) $UID);
    $userData = $queryResult->getRow();
    $cache->set($key, $userData);
  }
  return $userData;
}
?>

The key is the user's business key, prefixed: 'user_' . $UID. On a miss one query by primary key runs — (int) lets only a number reach the SQL — and the row is stored.

The two hard things

Page 387 is one quote on a blackboard: "There are only two hard things in Computer Science: cache invalidation and naming things." — Phil Karlton. For the first, the slides give two strategies (Fig. 6).

Time-based (pp. 388–389): "Caching for a certain amount of time", "eg. 10 minutes"; "don't delete caches"; "thus: You can't trust that data coming from cache is correct". It is "Great for summaries", an "Overview", "Pages where it's not that big a problem if data is a little bit out of date (eg. search results)", and "Good for quick and dirty optimizations". In Redis this is the TTL of lesson 18: SET key value EX 600.

Event-based (pp. 390–392): "Store forever, and expire on certain events" — "store userdata for ever", and "when user changes any of his preferences, throw cache away". The read side keeps the query result with no expiry, and can be told to invalidate instead (p. 391):

<?php
// p. 391: the row of user $UID, or drop its cached copy
function getUserData($UID, $invalidateCache = false)
{
  $db = DB::getInstance();
  $db->prepare("SELECT * FROM USERS WHERE uid = {UID}");
  $db->assignInt('UID', $UID);
  $db->setCacheTTL(0); // cache forever
  if ($invalidateCache)
  {
    return $db->invalidateCache();
  }
  $db->execute(); return $db->getRow();
}
?>

and the write side throws the copy away after every update (p. 392):

<?php
// p. 392: update the row, then drop its cached copy
function updateUserData($UID, $data)
{
  $db = DB::getInstance();
  $db->prepare("UPDATE USERS SET ... WHERE uid = {UID}");
  ...
  getUserData($UID, true); // invalidate cache
  return $result;
}
?>

The cache lives inside a database wrapper (setCacheTTL(0), "cache forever"; invalidateCache()) that the slides do not show: read the pattern, not the API. After an update the next read misses and reloads the fresh row: the old copy is gone as soon as the write drops it, at the price of a delete per write and a miss after it. In the widget of section 3, a TTL traded stale reads for misses and never reached zero; deleting on write reached zero and kept nearly the whole hit ratio.

database cache reads write v1 v2 v1 v2 TTL: 10 min stale time

Time-based: a 10-minute TTL

database cache reads write v1 v2 v1 v2 deleted on the write no stale read time

Event-based: delete on every write

Fig. 6. The same write, two strategies. Filled dots are hits, a red ring is a hit on an old copy, a hollow dot is a miss. Left: the copy lives out its TTL, so for the rest of it readers get the old value (p. 388). Right: the write deletes the copy; the next read misses and loads the new value (p. 390).

The deck closes with three tips (p. 394): be careful when security matters — remember "no authentication" (SASL authentication was then in the works); "Caching is not an excuse not to do database tuning. (Remember cold cache?)" — after a restart every request misses and the database takes the whole load; and write unit tests for your caching code, because "Debugging problems related to out-of-date cache data is hard and boring. Very boring."

Back to the hook. The word is duplicating: a cache holds "data duplicating original values stored elsewhere" (p. 367). When memcached loses a server, the originals are still in the database: the loss is time — a cold cache, a database under full load — never data. Redis, used as a database, may hold the only copy, so a crash would be a real loss: hence snapshots, logs, slaves, Sentinel and Cluster. Neither program is badly designed; they answer different questions. And whether a cache is worth having is one ratio: its hit ratio against the break-even \(t_c / t_o\).

Exam-style questions

Theory questions in the forms the readme of the past papers names — multiple choice, true or false, open answers; wrong answers may carry a penalty, announced just before the exam — then a computation the slides make themselves, and one practical exercise worded like the Redis exercises of the past papers.

Which Redis topology provides both automatic failover and the distribution of data across nodes?

  • Standalone master-slave Slaves give redundancy and read offloading, but "There is no automatic failover" (p. 361), and every slave holds all the data.
  • Sentinel Automatic failover only: "Sentinel does not distribute data across nodes" (p. 362).
  • Twemproxy It distributes data by consistent hashing but has no failover: a dead instance's keys are unreachable.
  • Cluster "Cluster (automatic failover and distribute data)" (p. 360): hash slots spread the data, and a slave is promoted when its master fails (p. 364).
Show model answer

Redis Cluster. Its keyspace is divided into 16384 hash slots shared among the masters, and if a master fails one of its slaves is promoted automatically. Sentinel gives only automatic failover, Twemproxy only data distribution, and the standalone topology neither.

The two-by-two of p. 360 is the whole question. The trap to avoid is Sentinel: it is about availability, never about distribution.

Which of the following statements are true? Select all that apply.

  • Redis Sentinel distributes data across the nodes of a master/slave topology. False: "Sentinel does not distribute data across nodes" (p. 362).
  • In Redis Cluster, multi-key commands are only allowed for keys in the same hash slot. True, verbatim from p. 364; hash tags put related keys in one slot.
  • By default, Redis slaves are read only. True: "Slaves are read only by default" (p. 359).
  • In the standalone master-slave topology, a slave is promoted automatically when the master fails. False: "There is no automatic failover" (p. 361); that is Sentinel's job.
  • In memcached a single item lives on one server only, and is lost if that server goes down. True: "No replication (single item in cache lives on one server only)" and "No persistency" (p. 381).
  • Twemproxy is part of Redis and performs automatic failover. False twice: "Twemproxy is a project from Twitter and is not part of redis" (p. 363), and it only distributes data.
Show model answer

True: the second, the third and the fifth. Sentinel provides automatic failover without distributing data; the standalone topology has no automatic failover; Twemproxy is an external proxy from Twitter that distributes data and has no failover.

With a possible penalty for wrong answers, tick only what a slide sentence supports. Each false statement here swaps one capability of the two-by-two for another.

Redis keeps its data in memory. Describe the two mechanisms it provides for persistence, what each of them can lose in a crash, and the trade-off between persistence and performance.

Show model answer

Redis provides Redis database snapshots (RDB) and append-only files (AOF). An RDB snapshot is a point-in-time copy of the whole dataset, written to disk periodically: it is compact and fast to reload, but everything written after the last snapshot is lost in a crash, typically minutes of data. The AOF appends every write command to a log that is replayed at restart: it loses at most what was not yet flushed to disk (about one second with the default once-per-second flush, nothing if the file is synced after every write), but the file is larger and slower to replay. Both can be enabled together. In general, increased persistency tends to increase latency and decrease throughput — syncing after every write makes each write wait for the disk. Redis is therefore not a replacement for relational databases or document stores: it is best used for rapidly changing data whose size is foreseeable and fits mostly in memory, often next to such a store.

The slides give the two names (p. 359) and the trade-off sentence (p. 349); the rest of the answer explains them, as a theory answer should. Three things to earn: both mechanisms, what each loses, and the persistence-versus-latency trade-off.

A cache served 45 cache hits and 10 cache misses. What is its hit ratio, in percent?

Show model answer

Hit ratio = hits / (hits + misses) = 45 / (45 + 10) = 0.818, about 82 %. The miss ratio is 1 − 0.818, about 18 %.

The numbers of p. 370, which prints 82 % and 18 %. Write the brackets: the slide's "hits / hits + misses", read literally, is not a ratio. If a question goes on to ask whether such a cache pays, the answer depends on the hit ratio against \(t_c / t_o\), not on 82 % being above 50 %.

Memcached offers no persistence, no replication, no fail-over and no authentication. Explain why it is nevertheless widely used, and compare it with Redis.

Show model answer

Memcached is a free and open-source, high-performance, distributed in-memory key/value cache, intended to speed up dynamic web applications by alleviating database load. Its servers are independent: the client manages the pool and sends each key to one server (for example three servers of 64 GB form one 192 GB pool). Items are at most 1 MB, keys at most 250 characters; keys cannot be enumerated, and space is reclaimed with LRU only when needed. It can do without persistence, replication and fail-over because it is a cache: every item duplicates data whose original is stored elsewhere, usually in the database, so losing a server costs time — the next requests miss and reload from the database — but not data. Redis instead offers persistence (RDB snapshots, AOF), replication to read-only slaves, automatic failover (Sentinel, Cluster), partitioning by proxy or by Cluster, and values that are data structures. In the benchmark shown in the lecture the two are equally fast (80,000 each). Memcached fits a pure cache; Redis fits data that must survive or needs structures.

The point the examiner looks for is the link between the "No"s and being a cache. A list of features without that "why" answers only half the question.

An exercise in the style of the past papers' Redis exercises. Consider the ER model of a hospital heating management system (the scenario of the Exam of 30 Jan 2026): Hospital (Hospital_ID, Name) has Departments (Department_ID, Specialty, N_Beds, Head_Physician_ID), Buildings and Heating Systems; Persons (P_ID, Email) work for Departments with a Role. A dashboard shows, for each Department, a summary page computed by a slow query over its floors and heaters.

Consider the Department entity and write the Redis commands for the following operations. (a) Cache the summary of Department "D7" under a key, so that it expires after 10 minutes. (b) Check how long the cached summary will remain valid. (c) The Head Physician of "D7" changes: invalidate the cached summary immediately. (d) The dashboard runs on a Redis Cluster: store the Specialty "Cardiology" and the N_Beds 24 of "D7" under two keys that a single command can read together, and read them.

Show model answer
SET department:D7:summary "Cardiology - 3 floors - 24 beds - 2 heaters" EX 600
TTL department:D7:summary
DEL department:D7:summary
MSET {department:D7}:specialty "Cardiology" {department:D7}:n_beds 24
MGET {department:D7}:specialty {department:D7}:n_beds

(a) A time-based invalidation: 10 minutes = 600 seconds (equivalently SET followed by EXPIRE department:D7:summary 600). (b) TTL returns the remaining seconds, at most 600, and -2 once the key has expired. (c) An event-based invalidation: DEL returns 1, and the next read misses and recomputes the summary. (d) The hash tag {department:D7} makes both keys hash the same string, so they share a hash slot and the multi-key commands are allowed; MGET returns "Cardiology" and "24".

The exercise is ours, built on the past papers' Redis wording ("Create a key-value pair … with TTL equal to …", "check its lifespan"). Three ideas are checked: TTL in seconds, invalidation on the event as an explicit DEL, and the same-slot rule of Redis Cluster with its hash tag — without the braces, MGET on two keys in different slots is refused.

Check yourself

A standalone master-slave deployment: the master crashes at 3 a.m. What do the clients experience until someone intervenes?

  • A slave is promoted within seconds and writes resume That needs Sentinel or Cluster; the standalone topology waits for a human.
  • Everything fails, reads included "Clients can connect … to the Slaves for read operations" (p. 361), and the slaves are still alive.
  • Reads keep working on the slaves; writes fail Slaves are read only by default (p. 359), and "There is no automatic failover" (p. 361): the copies answer reads, nobody accepts writes.
  • Writes go to the slaves and are copied back when the master returns Slaves are read only by default, and nothing queues writes for a dead master.

You need automatic failover. Which topologies give it to you? Select all that apply.

  • Twemproxy It distributes data, nothing more: a dead instance takes its keys with it.
  • Sentinel "automatically promoting a slave to master if the existing master fails" (p. 362).
  • Master-slave multi-level More copies, same standalone topology: "There is no automatic failover" (p. 361).
  • Cluster It "perform[s] automatic failover if any problem happens to any master instance" (p. 364).

Failover and distribution are two separate questions; only Cluster answers yes to both.

In a Redis Cluster, user:42:name is in slot 6755 and user:42:city in slot 8210 — both owned by master B. Does a command on both keys run?

  • Yes, because both keys live on the same master The rule is about slots: "Multi-key commands are only allowed for keys in the same hash slot" (p. 364). Slots can be moved to different masters one by one.
  • No: the keys are in different slots; a hash tag such as {user:42} in both names would put them in one slot With the tag, only user:42 is hashed, and both keys share slot 15880.
  • No: multi-key commands are never allowed in Redis Cluster They are allowed when all the keys hash to one slot — that is what hash tags are for.
  • Yes, but only if the cluster has exactly one master The check is on slots, not on machines, whatever the number of masters.

A look into the cache costs \(t_c = 2\) ms, a database query \(t_o = 10\) ms, and the hit ratio is 0.3. What does the cache do to the average read?

  • It makes it slower, since there are fewer hits than misses The condition is \(h \gt t_c / t_o\), here 0.2, not \(h \gt 0.5\): the red pen of p. 371.
  • It saves \(0.3 \times 10 = 3\) ms per read This forgets that every read pays the look \(t_c\), hits and misses alike.
  • Nothing can be said without the size of the cache The size acts through the hit ratio; once \(h\) is given, \(t_c\) and \(t_o\) decide.
  • It makes it slightly faster: \(2 + 0.7 \times 10 = 9\) ms instead of 10 ms The break-even is \(t_c / t_o = 0.2\), and 0.3 is above it.

Why can memcached afford "No persistency" and "No replication"?

  • Its servers run in pools, so one of them always has a second copy The pool shares out the keys, it does not copy them: "single item in cache lives on one server only" (p. 381).
  • Every item is a copy of data stored elsewhere: a lost server costs time, not data A cache is "a collection of data duplicating original values stored elsewhere" (p. 367); the misses reload from the database.
  • The client keeps a copy of every item it reads The client only decides which server holds a key; it stores nothing.
  • It writes everything to disk anyway "No persistency (if your server goes down, data in memcached is gone)" (p. 381).

User data is cached with time-based invalidation, TTL 10 minutes. A user changes their name. Which statements are true? Select all that apply.

  • For up to 10 minutes, some readers may see the old name "You can't trust that data coming from cache is correct" (p. 388).
  • The cache notices the change and refreshes the entry at once That is event-based invalidation (p. 390), which this setup does not do.
  • The cached entry is not deleted when the name changes "don't delete caches" (p. 388): the entry just lives out its TTL.
  • A shorter TTL would make stale reads impossible It narrows the window and costs hits; it never closes the window — the widget still showed stale hits at 10 s.

Belady's algorithm is on the replacement-policy slide (p. 369), yet no real cache runs it. Why?

  • It takes too long to compute The rule is simple; what is missing is the information, the future requests.
  • It does worse than LRU It does better than every other policy — in the widget it was the ceiling.
  • It evicts the entry needed again furthest in the future, which only a simulation can know It is the optimum, used as a yardstick for FIFO, LFU, LRU, MRU and RANDOM.
  • It works only for CPU caches It applies to any cache; it was proposed in 1966 for the memory of a computer, and the widget runs it on a cache of user profiles.

Key take-aways

What to watch and read