-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathcaching_interactive.html
More file actions
514 lines (486 loc) · 29.7 KB
/
Copy pathcaching_interactive.html
File metadata and controls
514 lines (486 loc) · 29.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Caching — HTTP, Redis, CDN, patterns</title>
<style>
:root{--bg:#f8fafc;--card:#fff;--border:#e2e8f0;--text:#1e293b;--muted:#64748b;--acc:#ef4444;--acc2:#dc2626;--blue600:#2563eb;--green:#16a34a;--red:#dc2626;--orange:#ea580c;--purple:#7c3aed;--grad1:#1a0808;--grad2:#330f0f}
@media(prefers-color-scheme:dark){:root{--bg:#0d0d0d;--card:#1a1a1a;--border:#2a2a2a;--text:#e2e8f0;--muted:#94a3b8}}
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:'Segoe UI',system-ui,sans-serif;background:var(--bg);color:var(--text);line-height:1.6}
header{background:linear-gradient(135deg,var(--grad1),var(--grad2));padding:2rem 1rem;text-align:center;border-bottom:2px solid var(--acc)}
header h1{font-size:2rem;font-weight:800;color:#fff}
header p{color:#fca5a5;margin-top:.4rem;font-size:.95rem}
nav{position:sticky;top:0;z-index:100;background:var(--card);border-bottom:1px solid var(--border);padding:.5rem 1rem;display:flex;gap:.5rem;flex-wrap:wrap;justify-content:center}
.tab-btn{padding:.4rem 1rem;border:none;border-radius:999px;cursor:pointer;font-size:.85rem;font-weight:600;background:var(--border);color:var(--muted);transition:all .2s}
.tab-btn.active{background:var(--blue600);color:#fff}
.tab-btn:hover:not(.active){background:var(--acc);color:#fff}
section{display:none;max-width:1100px;margin:0 auto;padding:1.5rem 1rem}
section.active{display:block}
h2{font-size:1.4rem;font-weight:700;margin-bottom:1rem;color:var(--acc)}
h3{font-size:1.1rem;font-weight:600;margin:1.2rem 0 .6rem}
.grid{display:grid;gap:1rem}.g2{grid-template-columns:repeat(auto-fit,minmax(280px,1fr))}.g3{grid-template-columns:repeat(auto-fit,minmax(220px,1fr))}
.card{background:var(--card);border:1px solid var(--border);border-radius:.75rem;padding:1.2rem}
.card h4{font-size:1rem;font-weight:700;margin-bottom:.5rem;color:var(--acc)}
.codeblk{background:#1e1e2e;color:#cdd6f4;border-radius:.5rem;padding:1rem;font-family:'Cascadia Code','Fira Code',monospace;font-size:.82rem;overflow-x:auto;margin:.5rem 0;line-height:1.7;white-space:pre-wrap}
.c-kw{color:#89b4fa}.c-str{color:#a6e3a1}.c-comment{color:#6c7086;font-style:italic}.c-val{color:#f9e2af}.c-fn{color:#cba6f7}
table{width:100%;border-collapse:collapse;font-size:.85rem;margin:.5rem 0}
th{background:var(--acc);color:#fff;padding:.5rem .75rem;text-align:left}
td{padding:.45rem .75rem;border-bottom:1px solid var(--border)}
tr:hover td{background:var(--border)}
.info-box{background:var(--card);border-left:4px solid var(--acc);border-radius:.5rem;padding:.8rem 1rem;margin:.5rem 0}
.info-box.green{border-color:var(--green)}.info-box.red{border-color:var(--red)}.info-box.blue{border-color:var(--blue600)}.info-box.orange{border-color:var(--orange)}
.quiz-q{background:var(--card);border:1px solid var(--border);border-radius:.75rem;padding:1.2rem;margin:.8rem 0}
.quiz-q p{font-weight:600;margin-bottom:.7rem}
.quiz-opt{padding:.5rem .8rem;border:1px solid var(--border);border-radius:.4rem;margin:.3rem 0;cursor:pointer;transition:all .2s}
.quiz-opt:hover{border-color:var(--acc);background:rgba(239,68,68,.05)}
.quiz-opt.correct{background:#dcfce7;border-color:var(--green);color:#166534}
.quiz-opt.wrong{background:#fee2e2;border-color:var(--red);color:#991b1b}
@media(prefers-color-scheme:dark){.quiz-opt.correct{background:#14532d;color:#86efac}.quiz-opt.wrong{background:#450a0a;color:#fca5a5}}
#quiz-score{display:none;text-align:center;padding:1.5rem;background:var(--card);border-radius:.75rem;margin-top:1rem;font-size:1.1rem;font-weight:700}
</style>
</head>
<body>
<header>
<h1>Caching</h1>
<p>HTTP cache, Redis, CDN, patterns d'invalidation, écueils classiques</p>
</header>
<nav>
<button class="tab-btn active" onclick="showTab('overview')">Vue d'ensemble</button>
<button class="tab-btn" onclick="showTab('http')">HTTP cache</button>
<button class="tab-btn" onclick="showTab('redis')">Redis / Memcached</button>
<button class="tab-btn" onclick="showTab('cdn')">CDN</button>
<button class="tab-btn" onclick="showTab('patterns')">Patterns</button>
<button class="tab-btn" onclick="showTab('pitfalls')">Écueils</button>
<button class="tab-btn" onclick="showTab('quiz')">Quiz</button>
</nav>
<section id="overview" class="active">
<h2>Vue d'ensemble</h2>
<div class="grid g2">
<div class="card">
<h4>Pourquoi cacher ?</h4>
<div class="info-box">
<strong>Le cache = le compromis le plus puissant en performance.</strong> Stocker la réponse pour ne pas la recalculer/refetcher.
</div>
<ul style="font-size:.88rem;padding-left:1.2rem;margin-top:.5rem;line-height:1.9">
<li><strong>Latence</strong> : RAM 100 ns vs disque 1 ms vs réseau 100 ms — gain ×10⁴</li>
<li><strong>Coût</strong> : moins d'appels API payants, moins de DB</li>
<li><strong>Scaling</strong> : absorbe les pics</li>
<li><strong>Résilience</strong> : sert en mode dégradé si la source plante</li>
</ul>
<div class="info-box red" style="margin-top:.4rem;font-size:.82rem">
<em>"There are only two hard things in computer science: cache invalidation and naming things."</em> — Phil Karlton
</div>
</div>
<div class="card">
<h4>Niveaux de cache (du plus proche au plus loin)</h4>
<table>
<tr><th>Niveau</th><th>Latence</th><th>Exemple</th></tr>
<tr><td>CPU L1/L2/L3</td><td>1-10 ns</td><td>Cache mémoire processeur</td></tr>
<tr><td>Application memory</td><td>100 ns</td><td>Map, LRU in-memory</td></tr>
<tr><td>Local disk</td><td>0,1-10 ms</td><td>SQLite, fichier</td></tr>
<tr><td>Network cache</td><td>1-5 ms</td><td>Redis, Memcached</td></tr>
<tr><td>CDN edge</td><td>10-50 ms</td><td>Cloudflare, Fastly, CloudFront</td></tr>
<tr><td>Origine</td><td>50-500 ms</td><td>Datacenter source</td></tr>
</table>
</div>
</div>
<div class="card" style="margin-top:1rem">
<h4>Métriques clés</h4>
<div class="grid g3">
<div class="info-box"><strong>Hit ratio</strong><br>hits / (hits + misses). Cible : > 80% pour rendre le cache pertinent.</div>
<div class="info-box blue"><strong>Latency P99</strong><br>Latence du 99e percentile — révèle les misses qui pénalisent.</div>
<div class="info-box green"><strong>Eviction rate</strong><br>Combien d'entrées sont évincées/sec. Trop = cache trop petit.</div>
<div class="info-box orange"><strong>Origin offload</strong><br>% de requêtes absorbées par le cache, ne touchant pas l'origine.</div>
</div>
</div>
</section>
<section id="http">
<h2>HTTP cache</h2>
<div class="grid g2">
<div class="card">
<h4>Cache-Control — directives</h4>
<div class="codeblk"><span class="c-comment"># Réponse serveur</span>
Cache-Control: <span class="c-kw">public</span>, <span class="c-kw">max-age</span>=<span class="c-val">3600</span>
<span class="c-comment"># Cacheable partout, 1h de fraîcheur</span>
Cache-Control: <span class="c-kw">private</span>, <span class="c-kw">max-age</span>=<span class="c-val">600</span>
<span class="c-comment"># Cache navigateur seulement, 10 min</span>
Cache-Control: <span class="c-kw">no-store</span>
<span class="c-comment"># Jamais stocker (page bancaire, page perso)</span>
Cache-Control: <span class="c-kw">no-cache</span>
<span class="c-comment"># Peut stocker mais doit revalider à chaque accès</span>
Cache-Control: <span class="c-kw">public</span>, <span class="c-kw">max-age</span>=<span class="c-val">31536000</span>, <span class="c-kw">immutable</span>
<span class="c-comment"># Pour assets versionnés (bundle.abc123.js) — 1 an, jamais ne change</span>
Cache-Control: <span class="c-kw">s-maxage</span>=<span class="c-val">86400</span>, <span class="c-kw">max-age</span>=<span class="c-val">60</span>
<span class="c-comment"># CDN cache 24h, navigateur 60s</span>
Cache-Control: <span class="c-kw">stale-while-revalidate</span>=<span class="c-val">60</span>,
<span class="c-kw">stale-if-error</span>=<span class="c-val">86400</span>
<span class="c-comment"># Sert stale 60s pendant refresh, ou 24h si origine KO</span></div>
</div>
<div class="card">
<h4>Validation conditionnelle</h4>
<div class="codeblk"><span class="c-comment"># Le serveur fournit un identifiant de version</span>
Last-Modified: Sun, 12 May 2026 10:00:00 GMT
ETag: <span class="c-str">"abc123def456"</span>
<span class="c-comment"># Le client revalide avec ces headers</span>
<span class="c-kw">GET</span> /resource HTTP/1.1
If-Modified-Since: Sun, 12 May 2026 10:00:00 GMT
If-None-Match: <span class="c-str">"abc123def456"</span>
<span class="c-comment"># Si la ressource n'a pas changé</span>
HTTP/1.1 <span class="c-kw">304 Not Modified</span>
<span class="c-comment"># Pas de body — économie de bande passante</span>
<span class="c-comment"># Si elle a changé</span>
HTTP/1.1 <span class="c-kw">200 OK</span>
ETag: <span class="c-str">"new-version"</span>
Content-Type: ...
<span class="c-comment">[nouveau corps]</span></div>
<div class="info-box green" style="margin-top:.4rem;font-size:.82rem">
<strong>Strong vs Weak ETag :</strong> <code>"abc"</code> = strong (byte-exact). <code>W/"abc"</code> = weak (sémantiquement équivalent, autorise compression différente).
</div>
</div>
</div>
<div class="grid g2" style="margin-top:1rem">
<div class="card">
<h4>Vary — variantes de la ressource</h4>
<div class="codeblk">Cache-Control: public, max-age=<span class="c-val">3600</span>
<span class="c-kw">Vary</span>: Accept-Encoding, Accept-Language
<span class="c-comment"># Le cache stocke une version par combinaison
# (Accept-Encoding × Accept-Language)</span>
<span class="c-comment"># Piège : Vary: User-Agent
# → explosion de la cardinalité (chaque UA = entrée différente)</span></div>
<div class="info-box red" style="margin-top:.4rem;font-size:.82rem">
Header Vary trop large = cache miss systématique. À utiliser avec parcimonie.
</div>
</div>
<div class="card">
<h4>Browser cache hiérarchie</h4>
<ol style="font-size:.85rem;padding-left:1.2rem;line-height:1.9">
<li><strong>Memory cache</strong> : pendant la session, RAM</li>
<li><strong>Disk cache</strong> : persistant entre sessions</li>
<li><strong>Service Worker cache</strong> : programmable (offline-first PWA)</li>
<li><strong>Push cache (HTTP/2)</strong> : éphémère, par connexion</li>
</ol>
<div class="codeblk" style="margin-top:.4rem"><span class="c-comment">// Service Worker : intercepter et cacher</span>
self.<span class="c-fn">addEventListener</span>(<span class="c-str">'fetch'</span>, e => {
e.<span class="c-fn">respondWith</span>(
caches.<span class="c-fn">match</span>(e.request).<span class="c-fn">then</span>(r =>
r || <span class="c-fn">fetch</span>(e.request)
)
);
});</div>
</div>
</div>
</section>
<section id="redis">
<h2>Redis & Memcached</h2>
<div class="grid g2">
<div class="card">
<h4>Comparatif</h4>
<table>
<tr><th>Aspect</th><th>Redis</th><th>Memcached</th></tr>
<tr><td>Type</td><td>Key-value + structures (list, hash, set, sorted set, stream, bitmap, geo)</td><td>Key-value pur</td></tr>
<tr><td>Persistance</td><td>RDB snapshots + AOF (append-only log)</td><td>Aucune (RAM seule)</td></tr>
<tr><td>Threading</td><td>Single-threaded principal (Redis 6+ : I/O threads)</td><td>Multi-threaded</td></tr>
<tr><td>Réplication</td><td>Async, sentinel ou cluster</td><td>Non native</td></tr>
<tr><td>Cluster</td><td>Redis Cluster (16384 slots)</td><td>Sharding côté client</td></tr>
<tr><td>Pub/Sub</td><td>Oui (+ Streams)</td><td>Non</td></tr>
<tr><td>Scripts</td><td>Lua, modules</td><td>Non</td></tr>
<tr><td>TTL</td><td>Par clé (EXPIRE)</td><td>Par item</td></tr>
<tr><td>Eviction</td><td>8 policies (LRU, LFU, TTL, random, no-evict)</td><td>LRU</td></tr>
</table>
</div>
<div class="card">
<h4>Commandes Redis usuelles</h4>
<div class="codeblk"><span class="c-comment"># Strings</span>
<span class="c-kw">SET</span> user:42 <span class="c-str">"alice"</span> <span class="c-kw">EX</span> <span class="c-val">3600</span>
<span class="c-kw">GET</span> user:42
<span class="c-kw">INCR</span> counter:hits
<span class="c-comment"># Hash (≈ objet)</span>
<span class="c-kw">HSET</span> user:42 name <span class="c-str">"Alice"</span> email <span class="c-str">"a@ex.com"</span>
<span class="c-kw">HGETALL</span> user:42
<span class="c-kw">HINCRBY</span> user:42 score <span class="c-val">10</span>
<span class="c-comment"># Lists (queue)</span>
<span class="c-kw">LPUSH</span> queue:jobs <span class="c-str">"job1"</span>
<span class="c-kw">BRPOP</span> queue:jobs <span class="c-val">0</span> <span class="c-comment"># bloquant</span>
<span class="c-comment"># Sets</span>
<span class="c-kw">SADD</span> tags:42 <span class="c-str">"red"</span> <span class="c-str">"blue"</span>
<span class="c-kw">SINTER</span> tags:42 tags:43 <span class="c-comment"># intersection</span>
<span class="c-comment"># Sorted set (leaderboard)</span>
<span class="c-kw">ZADD</span> scores <span class="c-val">100</span> alice <span class="c-val">85</span> bob
<span class="c-kw">ZRANGE</span> scores <span class="c-val">0 -1</span> <span class="c-kw">WITHSCORES</span>
<span class="c-kw">ZRANGEBYSCORE</span> scores <span class="c-val">80 100</span>
<span class="c-comment"># Pub/Sub</span>
<span class="c-kw">SUBSCRIBE</span> channel:news
<span class="c-kw">PUBLISH</span> channel:news <span class="c-str">"hello"</span>
<span class="c-comment"># Pipeline (batching réseau)</span>
MULTI ... EXEC
<span class="c-comment"># Atomicité avec Lua</span>
EVAL <span class="c-str">"local v=redis.call('GET',KEYS[1])..."</span> <span class="c-val">1</span> mykey</div>
</div>
</div>
<div class="grid g2" style="margin-top:1rem">
<div class="card">
<h4>Policies d'éviction Redis</h4>
<table>
<tr><th>Policy</th><th>Comportement</th></tr>
<tr><td>noeviction</td><td>Refuse les writes quand plein (défaut)</td></tr>
<tr><td>allkeys-lru</td><td>Évince les moins récemment utilisées (toutes clés)</td></tr>
<tr><td>allkeys-lfu</td><td>Évince les moins fréquemment utilisées</td></tr>
<tr><td>allkeys-random</td><td>Évince au hasard</td></tr>
<tr><td>volatile-lru</td><td>LRU seulement parmi clés avec TTL</td></tr>
<tr><td>volatile-lfu</td><td>LFU parmi clés avec TTL</td></tr>
<tr><td>volatile-ttl</td><td>Évince celles qui expirent en premier</td></tr>
<tr><td>volatile-random</td><td>Random parmi clés avec TTL</td></tr>
</table>
<div class="info-box blue" style="margin-top:.4rem;font-size:.82rem">
Pour un cache pur : <code>allkeys-lru</code> ou <code>allkeys-lfu</code>. Pour un cache + persistance : <code>volatile-*</code>.
</div>
</div>
<div class="card">
<h4>Cluster Redis</h4>
<div class="info-box">
Redis Cluster shard les données sur N nodes via 16384 slots de hash. La clé est hashée (CRC16) modulo 16384 → slot → node.
</div>
<div class="codeblk" style="margin-top:.4rem"><span class="c-comment"># Forcer 2 clés sur le même slot</span>
SET {user:42}:profile <span class="c-str">"..."</span>
SET {user:42}:orders <span class="c-str">"..."</span>
<span class="c-comment"># Le hash est calculé sur {user:42} uniquement</span>
<span class="c-comment"># → les 2 clés sont sur le même node</span>
<span class="c-comment"># → permet MULTI/EXEC, Lua atomiques</span></div>
<div class="info-box orange" style="margin-top:.4rem;font-size:.82rem">
Sans hashtag <code>{...}</code>, les opérations multi-clés cross-slot échouent. À planifier en amont.
</div>
</div>
</div>
</section>
<section id="cdn">
<h2>CDN — Content Delivery Network</h2>
<div class="grid g2">
<div class="card">
<h4>Principe</h4>
<p style="font-size:.88rem;margin-bottom:.5rem">Un CDN distribue le contenu sur des serveurs <strong>edge</strong> proches géographiquement des utilisateurs. Plus la latence est faible, meilleure est l'UX.</p>
<div class="info-box">
<strong>2 types de contenus :</strong><br>
• <strong>Statique</strong> : images, CSS, JS, vidéos — cachable agressivement<br>
• <strong>Dynamique</strong> : HTML personnalisé, API — cache fin ou edge compute
</div>
<h4 style="margin-top:1rem">Acteurs majeurs</h4>
<table>
<tr><th>CDN</th><th>Forces</th></tr>
<tr><td>Cloudflare</td><td>Plus de 300 PoPs, gratuit jusqu'à un certain niveau, Workers</td></tr>
<tr><td>Fastly</td><td>VCL ultra-flexible, instant purge</td></tr>
<tr><td>Akamai</td><td>Historique, enterprise, sécurité</td></tr>
<tr><td>AWS CloudFront</td><td>Intégration AWS, Lambda@Edge</td></tr>
<tr><td>BunnyCDN</td><td>Bas coût, simple</td></tr>
<tr><td>Vercel / Netlify</td><td>Devx-first, Jamstack</td></tr>
</table>
</div>
<div class="card">
<h4>Cache keys CDN</h4>
<p style="font-size:.88rem;margin-bottom:.5rem">Par défaut, la clé est <code>scheme + host + path + query</code>. Personnalisable :</p>
<table>
<tr><th>Inclusion</th><th>Effet</th></tr>
<tr><td>Query string</td><td>?id=42 ≠ ?id=43 (peut exploser la cardinalité)</td></tr>
<tr><td>Header device-type</td><td>Mobile/desktop séparés</td></tr>
<tr><td>Header geo</td><td>FR/US versions différentes</td></tr>
<tr><td>Cookie session</td><td>À ÉVITER en général (cache miss systématique)</td></tr>
</table>
<div class="info-box red" style="margin-top:.4rem;font-size:.82rem">
<strong>Piège classique :</strong> tracking params (gclid, utm_*) dans la clé → 1 cache entry par campagne marketing. Strippe-les avant.
</div>
</div>
</div>
<div class="grid g2" style="margin-top:1rem">
<div class="card">
<h4>Purge & invalidation</h4>
<table>
<tr><th>Méthode</th><th>Description</th></tr>
<tr><td>TTL classique</td><td>Attendre l'expiration (lent)</td></tr>
<tr><td>Purge by URL</td><td>API purge sur 1 URL exacte (rapide)</td></tr>
<tr><td>Purge by tag</td><td>Cache-Tag header — purger tous les objets taggés (Fastly, Cloudflare)</td></tr>
<tr><td>Purge all</td><td>Atomique, instantané — à manier avec précaution</td></tr>
<tr><td>Versioned URL</td><td>bundle.<strong>abc123</strong>.js — change l'URL = nouveau cache</td></tr>
</table>
<div class="codeblk" style="margin-top:.4rem"><span class="c-comment"># Cloudflare : header de tags</span>
Cache-Tag: <span class="c-str">"product-42, category-shoes, page-home"</span>
<span class="c-comment"># Purge par tag (API)</span>
POST /zones/{zone}/purge_cache
{ <span class="c-str">"tags"</span>: [<span class="c-str">"product-42"</span>] }</div>
</div>
<div class="card">
<h4>Edge compute</h4>
<p style="font-size:.88rem;margin-bottom:.5rem">Exécuter du code à l'edge, au plus près de l'utilisateur, avant ou à la place de l'origine.</p>
<table>
<tr><th>Plateforme</th><th>Runtime</th></tr>
<tr><td>Cloudflare Workers</td><td>V8 isolates (JS, Wasm), ~10ms cold start</td></tr>
<tr><td>Fastly Compute@Edge</td><td>WebAssembly (Rust, AssemblyScript)</td></tr>
<tr><td>AWS Lambda@Edge</td><td>Node.js, Python (~100ms cold start)</td></tr>
<tr><td>Vercel Edge Functions</td><td>V8 isolates (Next.js)</td></tr>
<tr><td>Deno Deploy</td><td>V8 + TypeScript natif</td></tr>
</table>
<div class="info-box green" style="margin-top:.4rem;font-size:.82rem">
<strong>Usage :</strong> A/B testing, géo-routing, transformation d'images, auth tokens, rate limiting, real-time personalization.
</div>
</div>
</div>
</section>
<section id="patterns">
<h2>Patterns de cache</h2>
<div class="grid g2">
<div class="card">
<h4>Cache-aside (lazy loading)</h4>
<div class="info-box green">
L'application gère le cache. Lecture : check cache → si miss, fetch DB → écrit dans le cache. C'est le plus courant.
</div>
<div class="codeblk" style="margin-top:.4rem"><span class="c-kw">def</span> <span class="c-fn">get_user</span>(id):
key = <span class="c-fn">f</span><span class="c-str">"user:{id}"</span>
val = redis.<span class="c-fn">get</span>(key)
<span class="c-kw">if</span> val:
<span class="c-kw">return</span> json.<span class="c-fn">loads</span>(val)
<span class="c-comment"># Cache miss</span>
val = db.<span class="c-fn">query</span>(<span class="c-str">"SELECT * FROM users WHERE id=%s"</span>, id)
redis.<span class="c-fn">setex</span>(key, <span class="c-val">3600</span>, json.<span class="c-fn">dumps</span>(val))
<span class="c-kw">return</span> val</div>
<div class="info-box red" style="margin-top:.4rem;font-size:.82rem">
<strong>Inconvénients :</strong> stale data possible jusqu'au TTL. Première lecture lente (miss).
</div>
</div>
<div class="card">
<h4>Write-through</h4>
<div class="info-box">
Toutes les writes passent par le cache, qui se charge d'écrire en DB. Cache toujours à jour.
</div>
<div class="codeblk" style="margin-top:.4rem"><span class="c-kw">def</span> <span class="c-fn">update_user</span>(id, data):
db.<span class="c-fn">update</span>(id, data)
redis.<span class="c-fn">setex</span>(<span class="c-fn">f</span><span class="c-str">"user:{id}"</span>, <span class="c-val">3600</span>, json.<span class="c-fn">dumps</span>(data))</div>
<div class="info-box orange" style="margin-top:.4rem;font-size:.82rem">
Writes plus lentes (2 systèmes). Bon pour read-heavy workloads.
</div>
</div>
</div>
<div class="grid g2" style="margin-top:1rem">
<div class="card">
<h4>Write-behind / Write-back</h4>
<div class="info-box blue">
L'app écrit dans le cache. Le cache flush en DB de façon asynchrone (batch).
</div>
<div class="info-box red" style="margin-top:.4rem;font-size:.82rem">
Très rapide en write, mais <strong>perte de données possible</strong> si le cache crash avant flush.
</div>
<h4 style="margin-top:1rem">Refresh-ahead</h4>
<div class="info-box green">
Le cache rafraîchit les entrées populaires avant leur expiration. Évite les misses sur les hot keys.
</div>
</div>
<div class="card">
<h4>Invalidation patterns</h4>
<table>
<tr><th>Pattern</th><th>Quand</th></tr>
<tr><td>TTL-only</td><td>Données peu critiques, OK avec staleness</td></tr>
<tr><td>Explicit invalidate</td><td>Sur write : DEL cache:key</td></tr>
<tr><td>Pub/Sub invalidate</td><td>Multi-server : event publié, tous invalident</td></tr>
<tr><td>Versioned keys</td><td>Append version au key (user:42:v3)</td></tr>
<tr><td>Surrogate keys / tags</td><td>Tag = group d'objets — invalidate par tag</td></tr>
</table>
</div>
</div>
</section>
<section id="pitfalls">
<h2>Écueils classiques</h2>
<div class="grid g2">
<div class="card">
<h4>Thundering herd / Cache stampede</h4>
<div class="info-box red">
Une clé hot expire. 10 000 requêtes simultanées la veulent. Toutes constatent un miss et frappent la DB → DB explose.
</div>
<div class="info-box green" style="margin-top:.4rem">
<strong>Solutions :</strong>
<ul style="padding-left:1.2rem;margin-top:.3rem">
<li><strong>Lock + single-flight</strong> : seule 1 requête refresh, les autres attendent</li>
<li><strong>Probabilistic early expiration</strong> : refresh tôt et aléatoirement</li>
<li><strong>Stale-while-revalidate</strong> : sert le stale pendant le refresh background</li>
<li><strong>Jittered TTL</strong> : TTL=3600+rand(0,300) pour étaler les expirations</li>
</ul>
</div>
</div>
<div class="card">
<h4>Cache penetration</h4>
<div class="info-box red">
Un attaquant demande systématiquement des clés inexistantes (cache miss systématique → DB stress).
</div>
<div class="info-box green" style="margin-top:.4rem">
<strong>Solutions :</strong>
<ul style="padding-left:1.2rem;margin-top:.3rem">
<li><strong>Cacher les "not found"</strong> aussi (avec TTL court)</li>
<li><strong>Bloom filter</strong> : vérifier l'existence avant DB</li>
<li><strong>Rate limiting</strong></li>
</ul>
</div>
</div>
</div>
<div class="grid g2" style="margin-top:1rem">
<div class="card">
<h4>Cache avalanche</h4>
<div class="info-box red">
Tous les objets expirent en même temps (lors d'un redémarrage, ou parce qu'ils ont été créés en batch avec le même TTL).
</div>
<div class="info-box green" style="margin-top:.4rem">
Jitter sur les TTL + warm-up progressif au démarrage.
</div>
<h4 style="margin-top:1rem">Données obsolètes</h4>
<div class="info-box orange">
<strong>Délai d'inconsistance :</strong> entre l'update DB et l'invalidation cache, un user peut lire l'ancien. Plus le système est distribué, plus c'est dur à éviter à 100%.
</div>
</div>
<div class="card">
<h4>Cache et données sensibles</h4>
<div class="info-box red">
Ne JAMAIS cacher publiquement :
<ul style="padding-left:1.2rem;margin-top:.3rem">
<li>Réponses personnalisées (dashboard utilisateur)</li>
<li>Tokens dans les headers/cookies</li>
<li>Données soumises à RGPD/HIPAA</li>
</ul>
</div>
<div class="info-box blue" style="margin-top:.4rem">
<strong>Bonnes pratiques :</strong>
<ul style="padding-left:1.2rem;margin-top:.3rem">
<li><code>Cache-Control: private</code> pour le contenu user-specific</li>
<li><code>Cache-Control: no-store</code> pour les pages bancaires</li>
<li>Chiffrer Redis si données sensibles (TLS, encryption at rest)</li>
<li>Vary: Cookie ou Authorization si pertinent</li>
</ul>
</div>
</div>
</div>
</section>
<section id="quiz">
<h2>Quiz — Caching</h2>
<div id="quiz-container"></div>
<div id="quiz-score"></div>
</section>
<script>
function showTab(id){document.querySelectorAll('section').forEach(s=>s.classList.remove('active'));document.querySelectorAll('.tab-btn').forEach(b=>b.classList.remove('active'));document.getElementById(id).classList.add('active');event.target.classList.add('active')}
const quizData=[
{q:"Quelle directive HTTP rend un asset jamais cacheable ?",opts:["Cache-Control: public","Cache-Control: no-store","Cache-Control: max-age=0","Cache-Control: immutable"],a:1},
{q:"À quoi sert le header ETag ?",opts:["Authentifier","Identifiant de version pour validation conditionnelle","Compresser","Chiffrer"],a:1},
{q:"Quelle réponse HTTP indique 'pas changé depuis la dernière fois' ?",opts:["200","204","304","404"],a:2},
{q:"Quelle policy d'éviction Redis évince les clés les moins fréquemment utilisées ?",opts:["allkeys-lru","allkeys-lfu","volatile-ttl","noeviction"],a:1},
{q:"Comment forcer 2 clés Redis sur le même slot pour MULTI/EXEC ?",opts:["Impossible","Utiliser un hashtag {user:42} dans la clé","Désactiver le cluster","Hashage manuel"],a:1},
{q:"Qu'est-ce que le 'thundering herd' ?",opts:["Une attaque DDoS","Une clé hot expire et 10000 requêtes simultanées frappent la DB","Une migration","Une panne réseau"],a:1},
{q:"Quel pattern sert le contenu stale pendant un refresh background ?",opts:["TTL-only","stale-while-revalidate","Write-through","Random eviction"],a:1},
{q:"Pourquoi ajouter un jitter au TTL ?",opts:["Pour la sécurité","Éviter qu'un grand nombre d'objets expirent en même temps (cache avalanche)","Compression","Aucun intérêt"],a:1},
{q:"Quelle est la différence entre cache-aside et write-through ?",opts:["Aucune","Cache-aside : l'app remplit le cache au miss ; write-through : les writes passent par le cache","Performance","Cache-aside utilise Redis"],a:1},
{q:"À quoi sert Cache-Control: immutable ?",opts:["Empêcher la modification du contenu","Indique au navigateur que la ressource ne changera jamais — pas de revalidation","Chiffrer","Compresser"],a:1},
{q:"Quel CDN exécute du code à l'edge via V8 isolates ?",opts:["AWS CloudFront","Cloudflare Workers / Vercel Edge","Akamai","BunnyCDN"],a:1},
{q:"Pour invalider des objets cache groupés, on utilise...",opts:["Purge all","Cache tags / surrogate keys","Vary","ETag"],a:1},
{q:"Une clé inexistante demandée massivement par un attaquant cause...",opts:["Cache poisoning","Cache penetration — solution : cacher les 'not found' ou Bloom filter","Cache stampede","Rien"],a:1}
];
let answers=new Array(quizData.length).fill(null);
function buildQuiz(){const c=document.getElementById('quiz-container');c.innerHTML=quizData.map((q,i)=>`<div class="quiz-q"><p>${i+1}. ${q.q}</p>${q.opts.map((o,j)=>`<div class="quiz-opt" onclick="answer(this,${i},${j})">${o}</div>`).join('')}</div>`).join('')}
function answer(el,qi,oi){const parent=el.parentElement;if(parent.querySelector('.correct,.wrong'))return;answers[qi]=oi;const correct=quizData[qi].a;parent.querySelectorAll('.quiz-opt').forEach((o,j)=>{if(j===correct)o.classList.add('correct');else if(j===oi&&oi!==correct)o.classList.add('wrong')});if(answers.every(a=>a!==null)){const score=answers.filter((a,i)=>a===quizData[i].a).length;const sc=document.getElementById('quiz-score');sc.style.display='block';sc.innerHTML=`Score : ${score}/${quizData.length} ${score>=10?'Excellent !':score>=7?'Bien !':'À revoir'}`}}
buildQuiz();
</script>
</body>
</html>