Key-value і кеші
Навіщо це
Section titled “Навіщо це”Картку товару на «Крамниці» рахує функція product_card(): рейтинг і кількість відгуків, 300 мс. Перед нею поставили Valkey із TTL 5 с. Поки ключ живий, запити обслуговуються за 2 мс і базу не чіпають. У мить, коли ключ зникає, 200 одночасних запитів не знаходять його, і всі 200 викликають функцію: відповіді приходять до 3.4 с. Кеш, який мав розвантажити базу, на мить навантажив її сильніше, ніж без нього. Це cache stampede.
Передумови. Втрачене оновлення: модуль 10. fsync і журнал: модуль 11. Асинхронна реплікація: модуль 12. Шардинг за ключем: модуль 13.
Ключ і значення
Section titled “Ключ і значення”Сховище «ключ → значення» робить дві речі: кладе значення за ключем і віддає його назад. Ключ відомий наперед, значення читають і пишуть цілком, дані вміщаються в пам’ять, а відповісти треба за долі мілісекунди. Це патерн доступу (access pattern) такого сховища. На «Крамниці» так працюють лічильник переглядів, кошик, сесія, черга листів, топ товарів, готова картка товару. У PostgreSQL кожна з цих задач можлива, але лічильник там означає запис із журналом і версіями рядків (модулі 10 і 11), а топ означає агрегацію.
У курсі це Valkey, форк Redis: протокол і команди ті самі (чому форк, нижче). Найпростіший приклад, лічильник переглядів товару 39:
127.0.0.1:6379> INCR views:product:39(integer) 1127.0.0.1:6379> INCRBY views:product:39 9(integer) 10Кожна команда виконується цілком і без втручання інших, тож INCR не втрачає оновлень, як у модулі 10: читання, додавання й запис тут одна неподільна дія.
Ціна простоти: шукати можна лише за ключем, схеми немає, дані обмежені пам’яттю, а транзакції зводяться до однієї команди чи невеликого пакета. Тому Valkey стоїть поруч із базою: джерело правди — PostgreSQL, а у Valkey лежить те, що можна перерахувати.
Структури, коли рядка замало
Section titled “Структури, коли рядка замало”Кошику чи топу рядка не вистачає: довелося б читати все значення, міняти в застосунку й писати назад. Тому є ще структури.
Хеш. Кошик: поле — товар, значення — кількість. Одне поле міняється без читання решти:
127.0.0.1:6379> HSET cart:1001 39 2 55 1(integer) 2127.0.0.1:6379> HINCRBY cart:1001 39 1(integer) 3127.0.0.1:6379> HGETALL cart:10011) "39"2) "3"3) "55"4) "1"Список і stream. Черга листів на списку: LPUSH додає, BRPOP бере, і якщо виконавець упав після BRPOP, лист втрачено. Stream тримає прочитане, поки виконавець не підтвердить:
127.0.0.1:6379> XGROUP CREATE events:shop workers $ MKSTREAMOK127.0.0.1:6379> XADD events:shop * type order_paid order_id 21728"1790811431843-0"127.0.0.1:6379> XREADGROUP GROUP workers w1 COUNT 1 STREAMS events:shop >1) 1) "events:shop" 2) 1) 1) "1790811431843-0" 2) 1) "type" 2) "order_paid" 3) "order_id" 4) "21728"127.0.0.1:6379> XPENDING events:shop workers1) (integer) 1127.0.0.1:6379> XACK events:shop workers 1790811431843-0(integer) 1До XACK повідомлення числиться за w1 як непідтверджене; якщо w1 помер, інший виконавець забере його командою XAUTOCLAIM. Це доставка «принаймні один раз»: повідомлення можуть повторитися, тож обробка має бути безпечною для повтору, як у модулі 10.
Множина. Хто вподобав товар (likes:55 заповнено так само). Повторний SADD того самого покупця нічого не міняє, а перетин відповідає на запит «хто вподобав обидва»:
127.0.0.1:6379> SADD likes:39 c:17 c:42 c:99(integer) 3127.0.0.1:6379> SINTER likes:39 likes:551) "c:42"2) "c:99"Sorted set. Топ товарів за переглядами. Кожен елемент має рахунок, і набір завжди впорядкований за ним, тож топ — це читання початку. Те саме в PostgreSQL на view_events — агрегація по всій таблиці (розміри датасету пояснено на сторінці Датасет):
product_id | views------------+------- 2410 | 3199 465 | 2959 58 | 1644 602 | 950 450 | 931Те саме з top:views, куди кожну подію перегляду додає ZINCRBY top:views 1 <product_id>:
127.0.0.1:6379> ZREVRANGE top:views 0 2 WITHSCORES1) "2410"2) "3199"3) "465"4) "2959"5) "58"6) "1644"Перші три рядки збігаються. Час: на small (52 918 переглядів) PostgreSQL 18.6 виконує запит за 7.7–30 мс (шість запусків), на medium (610 тис. подій, паралельний план) за 102–165 мс; ZREVRANGE за valkey-benchmark має медіану 0.09 мс. Різниця на два порядки, але порівняння нечесне: запит читає всі події, а набір уже містить підсумок, за який заплачено командою на кожну з них. Якщо топ потрібен раз на хвилину, вистачить матеріалізованого представлення (модуль 4); sorted set виграє, коли його читають часто, а рахунки постійно міняються.
HyperLogLog. Скільки унікальних відвідувачів переглянуло товар. Точний count(DISTINCT session_id) потребує пам’яті пропорційно до кількості сесій. HyperLogLog тримає фіксовані ≈ 12 КБ на лічильник і відповідає наближено, документація обіцяє стандартну похибку 0.81 %. На наших даних (session_id подій перегляду):
| Розмір | Точно | PFCOUNT |
Похибка |
|---|---|---|---|
small, усі товари |
21 522 | 21 516 | −0.03 % |
medium, усі товари |
214 707 | 217 549 | +1.32 % |
Окремо по товарах (medium): для 40 товарів із понад 1000 унікальних середня похибка 0.50 %, максимум 2.03 %. Лічильник uv:all зайняв 14 368 байтів за MEMORY USAGE, малі лічильники Valkey ущільнює (72 байти; найгірший випадок 3 проти 2). Для «приблизно 217 тисяч» цього досить, для рахунку грошей ні.
TTL і витіснення
Section titled “TTL і витіснення”Ключ може мати час життя: SET ключ значення EX 60 або EXPIRE. Прострочений ключ зникає, коли до нього звернулись, і під час фонового вибіркового перегляду ключів із TTL. Коли пам’ять упирається в maxmemory, maxmemory-policy вирішує, що витіснити. Виміряно на екземплярах із maxmemory 16mb: 3000 «сесій» без TTL по 1 КБ, потім 40 000 карток із TTL від 1 до 60 хвилин, 500 із них «гарячі» й читаються регулярно.
| Політика | Сесій вціліло з 3000 | Промахів по гарячих картках | Інше |
|---|---|---|---|
noeviction |
3000 | 0 | 28 524 записи відхилено помилкою OOM |
allkeys-lru |
1 | 0 | витіснено 28 619 ключів |
volatile-lru |
3000 | 0 | витіснено лише ключі з TTL |
volatile-ttl |
3000 | 26 % | серед карток вціліли переважно з довгим TTL: 9677 проти 858 |
allkeys-lru залишив гарячі картки, але викинув майже всі сесії: ключ без TTL, який давно не читали, нічим не відрізняється від непотрібного кешу. volatile-ttl береже ключі без TTL, проте першими викидає найкоротші TTL, тож гаряча картка з TTL 60 с вилітає раніше за холодну з TTL 3600 с. LRU у Valkey наближений: береться вибірка з maxmemory-samples ключів (типово 5) і викидається найстаріший із неї. Кеш і дані, які не можна перерахувати, в одному екземплярі не тримають; для чистого кеша ставлять allkeys-lru або allkeys-lfu.
Кеш перед базою
Section titled “Кеш перед базою”У cache-aside кеш про базу нічого не знає, усе робить застосунок: питає кеш, на промаху бере з бази й кладе в кеш.
async function getCard(id) { const hit = await redis.get(`card:${id}`); if (hit !== null) return JSON.parse(hit); const card = await loadCard(id); // SELECT * FROM product_card($1) if (card) await redis.set(`card:${id}`, JSON.stringify(card), { EX: 60 }); return card;}У write-through кожен запис іде і в базу, і в кеш, тож кеш завжди теплий. Ціна: два записи, між якими процес може померти, і в кеш потрапляє те, що ніхто не читатиме.
Інвалідація — найважче в кешуванні. Після нового відгуку картка в кеші бреше, і є два захисти: видалення ключа після запису в базу й TTL як страхувальна сітка. Спершу INSERT, потім DEL: якщо перевернути, читач між ними візьме стару картку з бази й покладе її в кеш уже після DEL. Те саме буває й за правильного порядку, коли читач узяв стару картку до коміту відгуку, а поклав після DEL: кеш бреше до кінця TTL, тож TTL тут не формальність. Facebook у статті про Memcache для цього описав «оренди» (leases): кеш видає читачеві токен, і запис із застарілим токеном відхиляється.
Cache stampede
Section titled “Cache stampede”Cache-aside ламається там, де почалася ця сторінка: коли ключ зникає під навантаженням.
Вимірювання на PostgreSQL 18.6 і Valkey 9.1.2, застосунок із L8 на Node.js, TTL 5 с, перерахунок 300 мс. Перша серія: ключ дожив до кінця, тоді 200 запитів одночасно.
| Захист | Викликів product_card() |
Відповідь p50 |
|---|---|---|
| немає | 200 | 2.0 с (усі за 3.5 с) |
| блокування на перерахунок | 1 | 0.69 с |
| stale-while-revalidate з блокуванням | 1 | 0.31 с |
| stale-while-revalidate без блокування | 200 | 0.23 с |
| раннє ймовірнісне оновлення | 200 | 2.0 с |
Затримки містять 0.1–0.45 с на сам вибух нових з’єднань (стільки ж і за свіжого кеша). Друга серія ближча до життя: 100 запитів на секунду на одну картку 30 секунд, п’ять закінчень TTL.
| Захист | Викликів | Запитів повільніше 200 мс | p99 |
|---|---|---|---|
| немає | 140 | 140 | 394 мс |
| блокування | 5 | 69 | 269 мс |
| stale-while-revalidate з блокуванням | 5 | 0 | 13 мс |
| stale-while-revalidate без блокування | 134 | 0 | 13 мс |
| раннє ймовірнісне оновлення | 11 | 11 | 15 мс |
Блокування на перерахунок. Хто побачив промах, бере SET lock:card:39 <токен> NX PX 5000. Решта чекає на значення або йде в базу після таймауту. PX потрібен, щоб блокування згасло, коли власник помер; знімати його треба порівнянням токена, а не просто DEL, бо інакше власник після закінчення PX зніме вже чуже блокування. Недолік видно в таблиці: 69 запитів чекали.
Stale-while-revalidate. Назву взято з розширення HTTP Cache-Control (RFC 5861). У кеші лежить значення з позначкою «свіже до». Прострочене віддається одразу, а перерахунок запускає один запит у фоні. Користувачі не чекають, але блокування все одно потрібне: без нього база отримала ті самі 134 виклики, лише користувачі цього не відчули. Перший запит на порожній кеш (холодний старт) не захищений: у прогоні 29 запитів вийшли в базу водночас.
Раннє ймовірнісне оновлення обходиться без блокування: читач із ймовірністю, що росте до кінця TTL, оновлює значення раніше. За роботою Vattani, Chierichetti, Lowenstein (VLDB 2015) він оновлює, якщо тепер − δ · β · ln(rand) ≥ момент_закінчення, де δ — час останнього перерахунку. Працює, коли запити йдуть безперервно (11 викликів замість 140), і марне, коли ключ нікому не потрібен до самого кінця (перша серія: 200).
До TTL варто додавати випадковий розкид, щоб ключі, записані разом, не вмирали разом.
Розподілене блокування
Section titled “Розподілене блокування”SET lock:order:21728 <токен> NX PX 10000 бере блокування: другий виклик повертає (nil), поки перше не знято або не згасло. Для stampede цього вистачає: якщо блокування дісталося двом, база отримає два виклики замість одного. Інша річ, коли блокування захищає коректність. Тут PX 1 с, власник A зависає на 1.5 с (пауза збирача сміття, своп, затримка мережі), B бере блокування й пише, а потім прокидається A і пише теж:
1 мс A взяв блокування (PX 1000), fencing-токен 1 1013 мс B взяв блокування (PX 1000), fencing-токен 2 1115 мс B прокинувся й пише 1115 мс сховище прийняло запис B: "запис від B" 1502 мс A прокинувся й пише 1502 мс сховище прийняло запис A: "запис від A" 1503 мс A знімає блокування: воно вже не наше, чуже не чіпаємоПеревірка токена при знятті врятувала блокування B, але запис A уже в сховищі й затер B. Захищає лише перевірка на боці сховища: кожне блокування видає зростаючий fencing-токен (тут INCR), і сховище відхиляє запис із токеном, меншим за вже бачений (токен 1 < 2).
Один потік і цикл подій
Section titled “Один потік і цикл подій”Тепер про те, чому Valkey швидкий. Команди виконує один потік. Він крутить цикл подій над epoll (модуль 13 курсу ОС): бере готові сокети, читає команди, виконує по одній, пише відповіді. Швидким це робить те, що між командами немає блокувань і перемикань контексту, дані лежать у пам’яті, а протокол простий. На нашій машині (Docker Desktop, valkey-benchmark, 50 клієнтів) SET, GET і INCR дають близько 200 тисяч операцій на секунду, а з конвеєром із 16 команд SET доходить до 1.8 мільйона.
Зворотний бік: поки виконується повільна команда, чекають усі. На 777 тисячах ключів і множині з мільйона елементів: одне з’єднання шле PING щомілісекунди, інше виконує важку команду.
| Команда | Час команди | Найгірший PING |
|---|---|---|
| без навантаження | 8.7 мс | |
KEYS * (777 тис. ключів) |
323 мс | 160 мс |
SCAN COUNT 1000, цілий обхід |
488 мс | 3.3 мс |
SMEMBERS (1 млн елементів) |
296 мс | 185 мс |
DEL (1 млн, lazyfree-lazy-user-del no) |
78 мс | 77 мс |
UNLINK (1 млн) |
1 мс | 1.2 мс |
KEYS * потрапив у SLOWLOG із часом 75 548 мкс (поріг slowlog-log-slower-than типово 10 000 мкс). Звідси правила: замість KEYS ходити SCAN, великі ключі видаляти UNLINK (звичайний DEL у Valkey 9.1 теж звільняє пам’ять у фоні, бо lazyfree-lazy-user-del типово yes), SMEMBERS, HGETALL і LRANGE 0 -1 на великих колекціях не викликати.
Параметр io-threads типово 1. Додаткові потоки читають, розбирають команди й пишуть відповіді, а виконує їх усе одно головний; valkey.conf радить вмикати їх лише на машинах хоча б із трьома ядрами. Valkey 8.0 (2024) цю частину переробив: за блогом проєкту, SET на AWS c7g.16xlarge із 8 потоками зріс з 360 тис. до 1.19 млн запитів на секунду. У нашому Docker із 200 клієнтами io-threads 4 дав 56–99 тис. операцій на секунду проти 36–55 тис. з одним потоком.
Персистентність: RDB і AOF
Section titled “Персистентність: RDB і AOF”Пам’ять зникає разом із процесом, тож є два способи тримати копію на диску: RDB — знімок усього набору: Valkey робить fork, дочірній процес пише файл, а батьківський працює далі. Типові правила знімків: save 3600 1 300 100 60 10000. На 776 629 ключах (59.4 МБ у пам’яті) файл вийшов 39.6 МБ, BGSAVE завершився менш ніж за секунду. AOF — журнал команд: кожну зміну дописують у файл, а appendfsync вирішує, коли робити fsync (модуль 11). Вартість за valkey-benchmark (50 клієнтів, SET, по два запуски):
| Режим | Операцій/с | З конвеєром 16 |
|---|---|---|
| без персистентності | 135–152 тис. | 1.45 млн |
| RDB, типові правила | 134–176 тис. | 1.50 млн |
AOF everysec (типовий) |
145–170 тис. | 1.03 млн |
AOF no |
178–195 тис. | 0.95 млн |
AOF always |
16–18 тис. | 166 тис. |
always відповідає лише після fsync (на цій машині близько 400 мкс), тож пропускна здатність падає в дев’ять разів. everysec робить fsync раз на секунду, і документація допускає втрату секунди записів при аварії. Втрату перевірено так: клієнт пише SET seq i по одному запису, рахує підтверджені, а контейнер вбивають docker kill (SIGKILL) на четвертій секунді.
| Режим | Підтверджено записів | seq після перезапуску |
|---|---|---|
| без персистентності | 29 186 | 0 |
| RDB, типові правила | 32 439 | 0 |
AOF everysec |
25 303 | 25 303 |
AOF no |
33 276 | 33 276 |
AOF always |
2 665 | 2 665 |
RDB втратив усе: знімок за правилами ще не робився. AOF не втратив нічого навіть із no, але це не доводить, що fsync зайвий: вбито процес, а не машину. Записане write() лежить у кеші сторінок ядра й переживає смерть процесу, а appendfsync захищає від втрати живлення чи падіння ядра, чого в Docker не відтворити без зупинки віртуальної машини.
Реплікація і Cluster
Section titled “Реплікація і Cluster”Реплікація у Valkey асинхронна: мастер виконує команду, відповідає клієнтові й лише потім надсилає її репліці (модуль 12). Скільки це коштує, видно, якщо відрізати репліку від мережі, записати 1000 ключів, вбити мастера й підняти репліку:
127.0.0.1:6379> SET before:1 1OK127.0.0.1:6379> WAIT 1 500(integer) 1$ docker network disconnect m14_default rp-b127.0.0.1:6379> SET after:1 1OK127.0.0.1:6379> WAIT 1 500(integer) 0(0.53s)... ще 999 записів, усі отримали OK$ docker kill rp-a ; valkey-cli -h rp-b REPLICAOF NO ONE127.0.0.1:6379> EXISTS before:1 after:1 after:1000(integer) 1Тисяча підтверджених клієнтові записів зникла. WAIT повертає, скільки реплік підтвердили останній запис цього клієнта: до розриву 1, після 0 через 0.53 с. Він змушує чекати репліку, але лінеаризованості (linearizability: ніби кожна операція діє миттєво) не дає: мастер може вмерти між підтвердженням клієнтові й відправленням репліці, а Valkey Cluster теж реплікує асинхронно й має те саме вікно втрат. Така поведінка називається eventual consistency (модуль 13): репліки збігаються з мастером лише тоді, коли записи припиняться.
Cluster ділить ключі на 16 384 хеш-слоти за CRC16(ключ) mod 16384, і кожен вузол володіє діапазоном слотів: це шардинг за ключем із модуля 13, але з фіксованою кількістю слотів замість кільця. На трьох вузлах:
127.0.0.1:6379> CLUSTER KEYSLOT foo(integer) 12182127.0.0.1:6379> SET foo bar(error) MOVED 12182 172.19.0.6:6379127.0.0.1:6379> MSET a 1 b 2(error) CROSSSLOT Keys in request don't hash to the same slot127.0.0.1:6379> CLUSTER KEYSLOT {cart:1001}:items(integer) 3818127.0.0.1:6379> CLUSTER KEYSLOT {cart:1001}:meta(integer) 3818Слот 12182 належить іншому вузлу, тож клієнт без -c отримує MOVED. Команда з кількома ключами працює лише в межах одного слота, тому ключі називають із хеш-тегом: хешується лише текст у фігурних дужках, і {cart:1001}:items та {cart:1001}:meta лежать разом. Транзакцій через слоти немає.
Redis, Valkey і ліцензії
Section titled “Redis, Valkey і ліцензії”Redis до версії 7.2 поширювався за ліцензією BSD. 20 березня 2024 року Redis Inc. оголосила, що від 7.4 код іде за подвійною ліцензією RSALv2 і SSPLv1; жодна не схвалена OSI, і допис визнає, що Redis більше не є відкритим кодом за визначенням OSI. 28 березня 2024 року Linux Foundation оголосила Valkey: форк Redis 7.2.4 під BSD, який підтримали AWS, Google Cloud, Oracle, Ericsson і Snap. 1 травня 2025 року Redis додав AGPLv3 як ще один варіант ліцензії для Redis 8, куди ввійшли й модулі колишнього Redis Stack.
У нашому docker compose працює Valkey 9.1.2 (профіль redis). Клієнти й redis-cli (у образі є і valkey-cli) працюють без змін. Для сумісності сервер і досі відповідає, що він Redis 7.2.4:
$ docker compose exec redis valkey-cli INFO serverredis_version:7.2.4valkey_version:9.1.2Далі проєкти розходяться: Redis 8 приніс нові типи (наприклад, vector sets), Valkey 8.0 переробив багатопотоковий ввід-вивід (вище). Команду, якої немає в Redis 7.2.4, перевіряють у документації обох. Щоб узяти Redis, у docker-compose.yml підставляють image: redis:8.10.2.
Як це насправді
Section titled “Як це насправді”docker compose --profile postgres --profile redis up -d --waitdocker compose exec redis valkey-cliПісля 12 секунд навантаження в 100 запитів на секунду на одну картку з L8 (TTL 5 с, з блокуванням) INFO stats показує, скільки запитів кеш обслужив:
127.0.0.1:6379> INFO stats...expired_keys:2evicted_keys:0keyspace_hits:1045keyspace_misses:189Відношення hits / (hits + misses) називають hit rate (частка влучань у кеш): головна цифра кеша (тут у промахи входить опитування тих, хто чекав на блокування). evicted_keys росте, коли maxmemory замалий. Для одного ключа є TTL, MEMORY USAGE і OBJECT ENCODING (як ущільнено значення). Виклики повільної функції веде вона сама: SELECT count(*) FROM l08.calls. measure.sh з L8 відтворює обидві серії про stampede.
Розбір: Redlock
Section titled “Розбір: Redlock”Salvatore Sanfilippo (antirez), автор Redis, запропонував Redlock для блокування над кількома незалежними екземплярами: клієнт бере блокування на більшості з пʼяти й вважає його чинним, поки не минув термін мінус витрачений час. У лютому 2016 року Мартін Клеппман у дописі «How to do distributed locking» заперечив: паузи процесу, затримки мережі й стрибки годинника ламають припущення про обмежені інтервали, тож блокування може діяти в двох клієнтів одночасно, а Redlock не видає fencing-токена, який виправив би наслідки. Для блокування «заради ефективності» (як наше блокування на перерахунок) він вважає достатнім одного вузла, а для коректності радить систему з консенсусом (ZooKeeper) і fencing або транзакцію бази. Antirez у відповіді «Is Redlock safe?» того ж місяця заперечив, що Redlock міряє час між кроками й відкидає блокування, яке витратило забагато, що годинники не слід стрибком переводити, а випадковий токен дозволяє перевірку «check and set» і без токена, що зростає.
Висновок практичний: блокування, яке лише зменшує зайву роботу, ставимо у Valkey. Блокування, від якого залежить цілісність даних, замінюємо блокуванням рядка в PostgreSQL (SELECT … FOR UPDATE, модуль 10) або умовним записом із версією.
Типові помилки розуміння
Section titled “Типові помилки розуміння”«Однопотоковий означає повільний». Один потік без блокувань між командами дає сотні тисяч операцій на секунду. Погано лише довгі команди: KEYS * на 777 тисячах ключів зупинив усіх на 160 мс.
«appendfsync everysec не губить даних». При падінні машини вона втрачає до секунди записів, а always коштує дев’ятикратного падіння пропускної здатності.
«Після DEL кеш уже не бреше». Читач, який узяв стару картку до коміту відгуку, кладе її в кеш після DEL, і вона живе до кінця TTL.
«Блокування усуває stampede». Воно зводить виклики до одного, але решта чекає (69 запитів повільніше 200 мс у другій серії), а блокування без PX і перевірки токена лишається назавжди або його знімає чужий клієнт.
Перевір себе
Лабораторна
Section titled “Лабораторна”L8. Кеш перед базою: поставити Valkey перед повільною карткою товару, додати інвалідацію при новому відгуку, захист від stampede і лишити застосунок живим без Valkey.
Джерела
Section titled “Джерела”- Valkey, документація: Cluster specification, PFCOUNT, Persistence, Key eviction, Benchmark; блог проєкту: Unlock 1 Million RPS;
valkey.conf. - Redis, Redis Adopts Dual Source-Available Licensing, 20 березня 2024; Redis is now available under the AGPLv3 open source license, 1 травня 2025.
- Linux Foundation, Linux Foundation Launches Open Source Valkey Community, 28 березня 2024.
- M. Kleppmann, How to do distributed locking, 2016; S. Sanfilippo (antirez), Is Redlock safe?, 2016.
- A. Vattani, F. Chierichetti, K. Lowenstein, Optimal Probabilistic Cache Stampede Prevention, VLDB 2015.
- R. Nishtala та ін., Scaling Memcache at Facebook, NSDI 2013.
- M. Nottingham, L. Eggert, RFC 5861, HTTP Cache-Control Extensions for Stale Content, 2010.
- P. Flajolet, É. Fusy, O. Gandouet, F. Meunier, HyperLogLog: the analysis of a near-optimal cardinality estimation algorithm, 2007.