Files
illustrated-algorithm/cache/index.html
T
枫 c96c23a7c5
Deploy / deploy (push) Failing after 1m30s
feat: add CI/CD pipeline with Docker + Nginx
- Add Gitea Actions workflow for auto-deploy on push to main
- Add Dockerfile (nginx:alpine based, with layer caching)
- Add nginx.conf (gzip, autoindex, static caching)
- Add .dockerignore
- Migrate cache.html → cache/index.html for directory-based routing
2026-08-23 14:24:47 +00:00

2552 lines
122 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>缓存三大问题:击穿、雪崩、穿透 机制与原理详解</title>
<style>
:root {
--primary: #6366f1;
--primary-light: #4f46e5;
--primary-dark: #4338ca;
--success: #16a34a;
--warning: #ca8a04;
--danger: #dc2626;
--info: #2563eb;
--bg: #f8fafc;
--bg-card: #ffffff;
--bg-code: #f1f5f9;
--text: #1e293b;
--text-muted: #64748b;
--border: #e2e8f0;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
background: var(--bg);
color: var(--text);
line-height: 1.8;
min-height: 100vh;
}
.header {
background: linear-gradient(135deg, #eef2ff 0%, #c7d2fe 50%, #eef2ff 100%);
padding: 60px 20px;
text-align: center;
border-bottom: 1px solid var(--border);
position: relative;
overflow: hidden;
}
.header::before {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle, rgba(99, 102, 241, 0.1) 0%, transparent 50%);
animation: pulse 4s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { transform: scale(1); opacity: 0.5; }
50% { transform: scale(1.1); opacity: 1; }
}
.header h1 {
font-size: 2.5rem;
background: linear-gradient(135deg, #4338ca, #6366f1, #4f46e5);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 16px;
position: relative;
}
.header p {
color: var(--text-muted);
font-size: 1.1rem;
position: relative;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 40px 20px;
}
/* Tab Navigation */
.tab-nav {
display: flex;
gap: 4px;
background: var(--bg-card);
border-radius: 16px 16px 0 0;
padding: 8px 8px 0;
border: 1px solid var(--border);
border-bottom: none;
overflow-x: auto;
}
.tab-btn {
flex: 1;
padding: 16px 24px;
border: none;
background: transparent;
color: var(--text-muted);
font-size: 1rem;
font-weight: 600;
cursor: pointer;
border-radius: 12px 12px 0 0;
transition: all 0.3s ease;
white-space: nowrap;
position: relative;
}
.tab-btn:hover {
color: var(--text);
background: rgba(99, 102, 241, 0.06);
}
.tab-btn.active {
color: var(--primary-light);
background: var(--bg);
border: 1px solid var(--border);
border-bottom-color: var(--bg);
}
.tab-btn .tab-icon {
display: block;
font-size: 1.5rem;
margin-bottom: 4px;
}
.tab-btn .tab-badge {
display: inline-block;
padding: 2px 8px;
border-radius: 10px;
font-size: 0.7rem;
margin-left: 8px;
vertical-align: middle;
}
.badge-danger { background: rgba(239, 68, 68, 0.15); color: #dc2626; }
.badge-warning { background: rgba(245, 158, 11, 0.15); color: #ca8a04; }
.badge-info { background: rgba(59, 130, 246, 0.15); color: #2563eb; }
/* Tab Content */
.tab-content {
display: none;
background: var(--bg);
border: 1px solid var(--border);
border-top: none;
border-radius: 0 0 16px 16px;
padding: 40px;
animation: fadeIn 0.3s ease;
}
.tab-content.active {
display: block;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
/* Section Styles */
.section {
margin-bottom: 48px;
}
.section:last-child {
margin-bottom: 0;
}
.section-title {
font-size: 1.5rem;
color: var(--primary-light);
margin-bottom: 20px;
padding-bottom: 12px;
border-bottom: 2px solid var(--border);
display: flex;
align-items: center;
gap: 12px;
}
.section-title .icon {
font-size: 1.8rem;
}
h3 {
font-size: 1.2rem;
color: var(--text);
margin: 24px 0 12px;
}
h4 {
font-size: 1.05rem;
color: var(--primary-light);
margin: 16px 0 8px;
}
p {
margin-bottom: 12px;
color: var(--text);
}
/* Cards */
.card {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 12px;
padding: 24px;
margin: 16px 0;
}
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin: 20px 0;
}
.card-highlight {
border-left: 4px solid var(--primary);
}
.card-danger {
border-left: 4px solid var(--danger);
}
.card-success {
border-left: 4px solid var(--success);
}
.card-warning {
border-left: 4px solid var(--warning);
}
/* Code Block */
.code-block {
background: var(--bg-code);
border: 1px solid var(--border);
border-radius: 12px;
margin: 16px 0;
overflow: hidden;
}
.code-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 20px;
background: rgba(99, 102, 241, 0.06);
border-bottom: 1px solid var(--border);
}
.code-header span {
color: var(--primary-light);
font-weight: 600;
font-size: 0.9rem;
}
.code-dots {
display: flex;
gap: 6px;
}
.code-dots span {
width: 12px;
height: 12px;
border-radius: 50%;
}
.code-dots span:nth-child(1) { background: #ef4444; }
.code-dots span:nth-child(2) { background: #f59e0b; }
.code-dots span:nth-child(3) { background: #10b981; }
pre {
padding: 20px;
overflow-x: auto;
font-size: 0.9rem;
line-height: 1.7;
}
code {
font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace;
}
.code-keyword { color: #a626a4; }
.code-func { color: #4078f2; }
.code-string { color: #50a14f; }
.code-comment { color: #a0a1a7; font-style: italic; }
.code-type { color: #c18401; }
.code-number { color: #986801; }
.code-builtin { color: #0184bc; }
.code-operator { color: #a626a4; }
/* Diagram */
.diagram {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 12px;
padding: 30px;
margin: 20px 0;
text-align: center;
}
.flow-diagram {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
flex-wrap: wrap;
margin: 20px 0;
}
.flow-box {
padding: 12px 20px;
border-radius: 8px;
font-size: 0.9rem;
font-weight: 600;
text-align: center;
min-width: 120px;
}
.flow-arrow {
color: var(--text-muted);
font-size: 1.5rem;
}
.flow-client { background: rgba(99, 102, 241, 0.1); border: 1px solid var(--primary); color: var(--primary-light); }
.flow-cache { background: rgba(239, 68, 68, 0.1); border: 1px solid var(--danger); color: #991b1b; }
.flow-db { background: rgba(16, 185, 129, 0.1); border: 1px solid var(--success); color: #166534; }
.flow-lock { background: rgba(245, 158, 11, 0.1); border: 1px solid var(--warning); color: #92400e; }
.flow-bloom { background: rgba(59, 130, 246, 0.1); border: 1px solid var(--info); color: #1e40af; }
/* Timeline */
.timeline {
position: relative;
padding-left: 30px;
margin: 20px 0;
}
.timeline::before {
content: '';
position: absolute;
left: 8px;
top: 0;
bottom: 0;
width: 2px;
background: var(--border);
}
.timeline-item {
position: relative;
margin-bottom: 20px;
padding: 16px;
background: var(--bg-card);
border-radius: 8px;
border: 1px solid var(--border);
}
.timeline-item::before {
content: '';
position: absolute;
left: -26px;
top: 20px;
width: 12px;
height: 12px;
border-radius: 50%;
background: var(--primary);
border: 2px solid var(--bg-card);
}
.timeline-item .time {
color: var(--primary-light);
font-weight: 600;
font-size: 0.85rem;
margin-bottom: 4px;
}
/* List */
ul, ol {
padding-left: 24px;
margin: 12px 0;
}
li {
margin-bottom: 8px;
color: var(--text);
}
/* Table */
.table-wrapper {
overflow-x: auto;
margin: 16px 0;
}
table {
width: 100%;
border-collapse: collapse;
background: var(--bg-card);
border-radius: 8px;
overflow: hidden;
}
th, td {
padding: 12px 16px;
text-align: left;
border-bottom: 1px solid var(--border);
}
th {
background: rgba(99, 102, 241, 0.06);
color: var(--primary-light);
font-weight: 600;
}
tr:hover td {
background: rgba(99, 102, 241, 0.04);
}
/* Alert */
.alert {
padding: 16px 20px;
border-radius: 8px;
margin: 16px 0;
display: flex;
gap: 12px;
align-items: flex-start;
}
.alert-icon {
font-size: 1.3rem;
flex-shrink: 0;
}
.alert-danger {
background: rgba(239, 68, 68, 0.08);
border: 1px solid rgba(239, 68, 68, 0.25);
color: #991b1b;
}
.alert-success {
background: rgba(16, 185, 129, 0.08);
border: 1px solid rgba(16, 185, 129, 0.25);
color: #166534;
}
.alert-warning {
background: rgba(245, 158, 11, 0.08);
border: 1px solid rgba(245, 158, 11, 0.25);
color: #92400e;
}
.alert-info {
background: rgba(59, 130, 246, 0.08);
border: 1px solid rgba(59, 130, 246, 0.25);
color: #1e40af;
}
/* Comparison */
.comparison {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin: 20px 0;
}
.comparison-item {
padding: 20px;
border-radius: 12px;
border: 1px solid var(--border);
}
.comparison-item.bad {
background: rgba(239, 68, 68, 0.06);
border-color: rgba(239, 68, 68, 0.2);
}
.comparison-item.good {
background: rgba(16, 185, 129, 0.06);
border-color: rgba(16, 185, 129, 0.2);
}
.comparison-title {
font-weight: 700;
margin-bottom: 12px;
display: flex;
align-items: center;
gap: 8px;
}
/* Bloom Filter Visual */
.bloom-visual {
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
margin: 20px 0;
flex-wrap: wrap;
}
.bloom-bit {
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 6px;
font-weight: 700;
font-size: 0.85rem;
transition: all 0.3s ease;
}
.bloom-bit.zero {
background: rgba(100, 116, 139, 0.08);
border: 1px solid var(--border);
color: var(--text-muted);
}
.bloom-bit.one {
background: rgba(99, 102, 241, 0.15);
border: 1px solid var(--primary);
color: var(--primary-light);
}
.bloom-bit.highlight {
background: rgba(239, 68, 68, 0.15);
border: 1px solid var(--danger);
color: #dc2626;
animation: blink 1s ease-in-out infinite;
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
/* Responsive */
@media (max-width: 768px) {
.header h1 { font-size: 1.8rem; }
.tab-content { padding: 20px; }
.comparison { grid-template-columns: 1fr; }
.card-grid { grid-template-columns: 1fr; }
.tab-btn { padding: 12px 16px; font-size: 0.85rem; }
}
/* Scrollbar */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--bg);
}
::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #94a3b8;
}
.inline-code {
background: rgba(99, 102, 241, 0.1);
color: var(--primary-light);
padding: 2px 8px;
border-radius: 4px;
font-family: 'JetBrains Mono', monospace;
font-size: 0.85em;
}
.step-number {
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border-radius: 50%;
background: var(--primary);
color: white;
font-size: 0.85rem;
font-weight: 700;
margin-right: 8px;
}
.architecture-diagram {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 12px;
padding: 30px;
margin: 20px 0;
}
.arch-row {
display: flex;
justify-content: center;
align-items: center;
gap: 16px;
margin: 12px 0;
flex-wrap: wrap;
}
.arch-node {
padding: 10px 18px;
border-radius: 8px;
font-size: 0.85rem;
font-weight: 600;
text-align: center;
}
.arch-connector {
display: flex;
justify-content: center;
color: var(--text-muted);
font-size: 1.2rem;
margin: 4px 0;
}
.tag {
display: inline-block;
padding: 3px 10px;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 600;
margin: 2px;
}
.tag-red { background: rgba(239, 68, 68, 0.12); color: #dc2626; }
.tag-green { background: rgba(16, 185, 129, 0.12); color: #16a34a; }
.tag-blue { background: rgba(59, 130, 246, 0.12); color: #2563eb; }
.tag-yellow { background: rgba(245, 158, 11, 0.12); color: #ca8a04; }
.tag-purple { background: rgba(139, 92, 246, 0.12); color: #7c3aed; }
</style>
</head>
<body>
<div class="header">
<h1>🔥 缓存三大问题:机制与原理详解</h1>
<p>缓存击穿 · 缓存雪崩 · 缓存穿透 — 深入理解问题本质与解决方案</p>
</div>
<div class="container">
<!-- Tab Navigation -->
<div class="tab-nav">
<button class="tab-btn active" onclick="switchTab('breakdown')">
<span class="tab-icon">💥</span>
缓存击穿
<span class="tab-badge badge-danger">Hot Key</span>
</button>
<button class="tab-btn" onclick="switchTab('avalanche')">
<span class="tab-icon">🏔️</span>
缓存雪崩
<span class="tab-badge badge-warning">Mass Expire</span>
</button>
<button class="tab-btn" onclick="switchTab('penetration')">
<span class="tab-icon">🕳️</span>
缓存穿透
<span class="tab-badge badge-info">Invalid Key</span>
</button>
</div>
<!-- ==================== TAB 1: 缓存击穿 ==================== -->
<div class="tab-content active" id="tab-breakdown">
<!-- 问题描述 -->
<div class="section">
<h2 class="section-title"><span class="icon">💥</span> 缓存击穿 — 问题描述</h2>
<div class="alert alert-danger">
<span class="alert-icon">⚠️</span>
<div>
<strong>核心问题:</strong>一个<strong>热点 Key</strong>(被大量并发访问的缓存数据)在过期的瞬间,大量并发请求同时发现缓存失效,全部穿透到数据库,导致数据库压力骤增甚至崩溃。
</div>
</div>
<div class="card card-highlight">
<h3>🎯 问题本质</h3>
<p>缓存击穿的关键词是<strong>"热点"</strong>和<strong>"过期"</strong>。与缓存穿透不同,击穿针对的是<strong>确实存在</strong>的数据,只是恰好在某一时刻缓存失效了。</p>
<ul>
<li><strong>前提条件:</strong>某个 Key 是热点数据,访问量极大(如微博热搜、秒杀商品)</li>
<li><strong>触发条件:</strong>该 Key 的缓存恰好在某一时刻过期</li>
<li><strong>直接后果:</strong>瞬间大量请求同时 miss 缓存,全部打到数据库</li>
<li><strong>最终影响:</strong>数据库连接池耗尽,响应时间飙升,服务不可用</li>
</ul>
</div>
<h3>📊 时序图 — 击穿发生过程</h3>
<div class="diagram">
<div class="timeline">
<div class="timeline-item">
<div class="time">T0 — 正常运行</div>
<p>热点数据 <span class="inline-code">hot_key</span> 存在于缓存中,所有请求直接命中缓存返回,数据库无压力。</p>
</div>
<div class="timeline-item">
<div class="time">T1 — 缓存过期</div>
<p><span class="inline-code">hot_key</span> 的 TTL 到期,缓存中该数据被删除。</p>
</div>
<div class="timeline-item">
<div class="time">T2 — 并发穿透</div>
<p>同一时刻,10000+ 个请求到达,全部 cache miss,同时查询数据库。</p>
</div>
<div class="timeline-item">
<div class="time">T3 — 数据库过载</div>
<p>数据库瞬间承受 10000+ 并发查询,连接池耗尽,响应时间从 5ms 飙升到 5000ms+。</p>
</div>
<div class="timeline-item">
<div class="time">T4 — 恢复</div>
<p>第一个请求完成查询并回写缓存,后续请求重新命中缓存,系统逐渐恢复。</p>
</div>
</div>
</div>
<h3>🔍 典型场景</h3>
<div class="card-grid">
<div class="card card-warning">
<h4>🛒 秒杀活动</h4>
<p>商品详情缓存过期瞬间,数万用户同时请求,全部穿透到数据库。</p>
</div>
<div class="card card-warning">
<h4>📰 热点新闻</h4>
<p>突发新闻页面缓存失效,大量用户刷新页面导致数据库压力暴增。</p>
</div>
<div class="card card-warning">
<h4>👤 大V主页</h4>
<p>知名用户的主页数据缓存过期,粉丝频繁访问导致数据库过载。</p>
</div>
</div>
</div>
<!-- 解决方案总览 -->
<div class="section">
<h2 class="section-title"><span class="icon">🛡️</span> 解决方案总览</h2>
<div class="card card-highlight">
<p>针对缓存击穿问题,核心思路是<strong>避免大量请求同时穿透到数据库</strong>。以下是两种主流解决方案:</p>
</div>
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>方案</th>
<th>核心思路</th>
<th>数据一致性</th>
<th>可用性</th>
<th>适用场景</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>🔒 逻辑过期(永不过期)</strong></td>
<td>缓存不设物理 TTL,在 value 中存储逻辑过期时间,过期后异步重建</td>
<td>弱(可能返回旧数据)</td>
<td>高(不阻塞)</td>
<td>对一致性要求不高的热点数据</td>
</tr>
<tr>
<td><strong>🔐 互斥锁排队</strong></td>
<td>缓存 miss 时用分布式锁控制,只允许一个线程查 DB 重建缓存</td>
<td>强(等待最新数据)</td>
<td>中(可能等待超时)</td>
<td>对一致性要求较高的场景</td>
</tr>
</tbody>
</table>
</div>
<div class="alert alert-info">
<span class="alert-icon">💡</span>
<div>
<strong>选择建议:</strong>如果业务允许短暂返回旧数据(如社交动态、商品浏览),优先选择<strong>逻辑过期</strong>方案保证高可用;如果对数据一致性要求严格(如库存、余额),选择<strong>互斥锁</strong>方案。
</div>
</div>
</div>
<!-- 解决方案一:永不过期 -->
<div class="section">
<h2 class="section-title"><span class="icon">🔒</span> 解决方案一:逻辑过期(永不过期)</h2>
<div class="alert alert-info">
<span class="alert-icon">💡</span>
<div>
<strong>核心思路:</strong>缓存不设置物理 TTL(永不过期),而是在 value 中存储一个<strong>逻辑过期时间</strong>。读取时检查是否逻辑过期,如果过期则由一个线程异步重建缓存,其他线程返回旧数据。
</div>
</div>
<h3>📐 设计原理</h3>
<div class="card card-highlight">
<ol>
<li><strong>缓存不设 TTL:</strong>数据一旦写入缓存,不会被 Redis 自动删除</li>
<li><strong>逻辑过期字段:</strong>在 value 中增加 <span class="inline-code">expire_time</span> 字段,记录业务层面的过期时间</li>
<li><strong>读取时判断:</strong>
<ul>
<li>若未逻辑过期 → 直接返回缓存数据</li>
<li>若已逻辑过期 → 尝试获取互斥锁,获取成功则异步重建缓存,获取失败则返回旧数据</li>
</ul>
</li>
<li><strong>异步重建:</strong>获取到锁的线程从数据库查询最新数据,更新缓存中的值和逻辑过期时间</li>
</ol>
</div>
<h3>🔄 流程图</h3>
<div class="diagram">
<div class="flow-diagram">
<div class="flow-box flow-client">请求到达</div>
<span class="flow-arrow">→</span>
<div class="flow-box flow-cache">查询缓存</div>
<span class="flow-arrow">→</span>
<div class="flow-box flow-lock">检查逻辑过期?</div>
</div>
<div class="flow-diagram" style="margin-top: 16px;">
<div class="flow-box flow-success" style="background: rgba(16,185,129,0.1); border: 1px solid var(--success); color: #16a34a;">未过期 → 直接返回</div>
<span style="color: var(--text-muted); margin: 0 20px;">|</span>
<div class="flow-box flow-lock">已过期 → 获取互斥锁</div>
</div>
<div class="flow-diagram" style="margin-top: 16px;">
<div class="flow-box flow-lock">获取成功 → 查DB → 更新缓存</div>
<span style="color: var(--text-muted); margin: 0 20px;">|</span>
<div class="flow-box flow-cache">获取失败 → 返回旧数据</div>
</div>
</div>
<h3>💻 Go 代码实现</h3>
<div class="code-block">
<div class="code-header">
<div class="code-dots"><span></span><span></span><span></span></div>
<span>logical_expire.go — 逻辑过期方案</span>
</div>
<pre><code><span class="code-keyword">package</span> cache
<span class="code-keyword">import</span> (
<span class="code-string">"context"</span>
<span class="code-string">"encoding/json"</span>
<span class="code-string">"fmt"</span>
<span class="code-string">"sync"</span>
<span class="code-string">"time"</span>
<span class="code-string">"github.com/redis/go-redis/v9"</span>
)
<span class="code-comment">// CacheData 带逻辑过期的缓存数据结构</span>
<span class="code-keyword">type</span> <span class="code-type">CacheData</span> <span class="code-keyword">struct</span> {
Data json.RawMessage <span class="code-string">`json:"data"`</span> <span class="code-comment">// 实际业务数据</span>
ExpireTime <span class="code-type">int64</span> <span class="code-string">`json:"expire_time"`</span> <span class="code-comment">// 逻辑过期时间(Unix时间戳)</span>
}
<span class="code-comment">// LogicalExpireCache 逻辑过期缓存实现</span>
<span class="code-keyword">type</span> <span class="code-type">LogicalExpireCache</span> <span class="code-keyword">struct</span> {
rdb *redis.Client
localMutex <span class="code-type">sync.Map</span> <span class="code-comment">// 本地互斥锁,防止多个goroutine同时重建</span>
}
<span class="code-keyword">func</span> <span class="code-func">NewLogicalExpireCache</span>(rdb *redis.Client) *<span class="code-type">LogicalExpireCache</span> {
<span class="code-keyword">return</span> &<span class="code-type">LogicalExpireCache</span>{rdb: rdb}
}
<span class="code-comment">// Get 获取缓存数据(核心逻辑)</span>
<span class="code-keyword">func</span> (c *<span class="code-type">LogicalExpireCache</span>) <span class="code-func">Get</span>(
ctx context.Context,
key <span class="code-type">string</span>,
logicalTTL time.Duration,
loadFn <span class="code-keyword">func</span>(ctx context.Context) (<span class="code-type">interface</span>{}, <span class="code-type">error</span>),
) (<span class="code-type">interface</span>{}, <span class="code-type">error</span>) {
<span class="code-comment">// Step 1: 从缓存中获取数据</span>
val, err := c.rdb.Get(ctx, key).Result()
<span class="code-keyword">if</span> err == redis.Nil {
<span class="code-comment">// 缓存不存在(首次),需要加载并设置</span>
<span class="code-keyword">return</span> c.loadAndSet(ctx, key, logicalTTL, loadFn)
}
<span class="code-keyword">if</span> err != <span class="code-builtin">nil</span> {
<span class="code-keyword">return</span> <span class="code-builtin">nil</span>, fmt.Errorf(<span class="code-string">"redis get error: %w"</span>, err)
}
<span class="code-comment">// Step 2: 反序列化</span>
<span class="code-keyword">var</span> cacheData <span class="code-type">CacheData</span>
<span class="code-keyword">if</span> err := json.Unmarshal([]<span class="code-type">byte</span>(val), &cacheData); err != <span class="code-builtin">nil</span> {
<span class="code-keyword">return</span> <span class="code-builtin">nil</span>, fmt.Errorf(<span class="code-string">"unmarshal error: %w"</span>, err)
}
<span class="code-comment">// Step 3: 检查是否逻辑过期</span>
<span class="code-keyword">if</span> time.Now().Unix() < cacheData.ExpireTime {
<span class="code-comment">// 未过期,直接返回缓存数据</span>
<span class="code-keyword">var</span> result <span class="code-type">interface</span>{}
json.Unmarshal(cacheData.Data, &result)
<span class="code-keyword">return</span> result, <span class="code-builtin">nil</span>
}
<span class="code-comment">// Step 4: 已过期,尝试获取互斥锁进行异步重建</span>
lockKey := <span class="code-string">"lock:"</span> + key
locked := c.tryLock(ctx, lockKey, <span class="code-number">5</span>*time.Second)
<span class="code-keyword">if</span> locked {
<span class="code-comment">// 获取锁成功,开启goroutine异步重建缓存</span>
<span class="code-keyword">go</span> <span class="code-keyword">func</span>() {
<span class="code-keyword">defer</span> c.unlock(ctx, lockKey)
c.loadAndSet(ctx, key, logicalTTL, loadFn)
}()
}
<span class="code-comment">// 无论是否获取到锁,都返回旧数据(保证可用性)</span>
<span class="code-keyword">var</span> result <span class="code-type">interface</span>{}
json.Unmarshal(cacheData.Data, &result)
<span class="code-keyword">return</span> result, <span class="code-builtin">nil</span>
}
<span class="code-comment">// loadAndSet 从数据库加载数据并写入缓存</span>
<span class="code-keyword">func</span> (c *<span class="code-type">LogicalExpireCache</span>) <span class="code-func">loadAndSet</span>(
ctx context.Context,
key <span class="code-type">string</span>,
logicalTTL time.Duration,
loadFn <span class="code-keyword">func</span>(ctx context.Context) (<span class="code-type">interface</span>{}, <span class="code-type">error</span>),
) (<span class="code-type">interface</span>{}, <span class="code-type">error</span>) {
<span class="code-comment">// 双重检查:再次从缓存获取(可能其他goroutine已重建)</span>
val, _ := c.rdb.Get(ctx, key).Result()
<span class="code-keyword">if</span> val != <span class="code-string">""</span> {
<span class="code-keyword">var</span> cd <span class="code-type">CacheData</span>
json.Unmarshal([]<span class="code-type">byte</span>(val), &cd)
<span class="code-keyword">if</span> time.Now().Unix() < cd.ExpireTime {
<span class="code-keyword">var</span> result <span class="code-type">interface</span>{}
json.Unmarshal(cd.Data, &result)
<span class="code-keyword">return</span> result, <span class="code-builtin">nil</span>
}
}
<span class="code-comment">// 从数据库加载最新数据</span>
data, err := loadFn(ctx)
<span class="code-keyword">if</span> err != <span class="code-builtin">nil</span> {
<span class="code-keyword">return</span> <span class="code-builtin">nil</span>, err
}
<span class="code-comment">// 构造带逻辑过期的缓存值</span>
dataBytes, _ := json.Marshal(data)
cacheData := <span class="code-type">CacheData</span>{
Data: dataBytes,
ExpireTime: time.Now().Add(logicalTTL).Unix(),
}
cacheBytes, _ := json.Marshal(cacheData)
<span class="code-comment">// 写入Redis(不设TTL,永不过期)</span>
c.rdb.Set(ctx, key, cacheBytes, <span class="code-number">0</span>) <span class="code-comment">// TTL=0 表示永不过期</span>
<span class="code-keyword">return</span> data, <span class="code-builtin">nil</span>
}
<span class="code-comment">// tryLock 尝试获取分布式锁</span>
<span class="code-keyword">func</span> (c *<span class="code-type">LogicalExpireCache</span>) <span class="code-func">tryLock</span>(ctx context.Context, key <span class="code-type">string</span>, ttl time.Duration) <span class="code-type">bool</span> {
<span class="code-keyword">return</span> c.rdb.SetNX(ctx, key, <span class="code-string">"1"</span>, ttl).Val()
}
<span class="code-comment">// unlock 释放分布式锁</span>
<span class="code-keyword">func</span> (c *<span class="code-type">LogicalExpireCache</span>) <span class="code-func">unlock</span>(ctx context.Context, key <span class="code-type">string</span>) {
c.rdb.Del(ctx, key)
}</code></pre>
</div>
<div class="alert alert-warning">
<span class="alert-icon">⚡</span>
<div>
<strong>优缺点分析:</strong><br>
✅ <strong>优点:</strong>不会阻塞用户请求,始终返回数据(可能稍旧),可用性高<br>
❌ <strong>缺点:</strong>数据一致性有延迟(过期后一段时间内返回旧数据);需要额外内存存储过期时间字段;缓存不会自动清理,需要额外机制处理不活跃数据
</div>
</div>
</div>
<!-- 解决方案二:互斥锁 -->
<div class="section">
<h2 class="section-title"><span class="icon">🔐</span> 解决方案二:互斥锁排队</h2>
<div class="alert alert-info">
<span class="alert-icon">💡</span>
<div>
<strong>核心思路:</strong>缓存失效时,不立即去查数据库,而是先尝试获取互斥锁。只有获取到锁的线程才去查询数据库并重建缓存,其他线程等待或重试。
</div>
</div>
<h3>📐 设计原理</h3>
<div class="card card-highlight">
<ol>
<li><strong>缓存正常设置 TTL:</strong>使用正常的过期策略</li>
<li><strong>缓存 miss 时获取锁:</strong>使用 Redis 的 <span class="inline-code">SETNX</span> 实现分布式互斥锁</li>
<li><strong>获取锁成功:</strong>查询数据库 → 写入缓存 → 释放锁</li>
<li><strong>获取锁失败:</strong>短暂休眠后重试(sleep + retry),直到缓存重建完成</li>
</ol>
</div>
<h3>💻 Go 代码实现</h3>
<div class="code-block">
<div class="code-header">
<div class="code-dots"><span></span><span></span><span></span></div>
<span>mutex_lock.go — 互斥锁排队方案</span>
</div>
<pre><code><span class="code-keyword">package</span> cache
<span class="code-keyword">import</span> (
<span class="code-string">"context"</span>
<span class="code-string">"errors"</span>
<span class="code-string">"fmt"</span>
<span class="code-string">"time"</span>
<span class="code-string">"github.com/redis/go-redis/v9"</span>
)
<span class="code-keyword">var</span> (
ErrCacheMiss = errors.New(<span class="code-string">"cache miss"</span>)
ErrDBError = errors.New(<span class="code-string">"database error"</span>)
)
<span class="code-comment">// MutexCache 互斥锁缓存实现</span>
<span class="code-keyword">type</span> <span class="code-type">MutexCache</span> <span class="code-keyword">struct</span> {
rdb *redis.Client
retryCount <span class="code-type">int</span> <span class="code-comment">// 重试次数</span>
retryDelay time.Duration <span class="code-comment">// 重试间隔</span>
lockTTL time.Duration <span class="code-comment">// 锁的过期时间</span>
}
<span class="code-keyword">func</span> <span class="code-func">NewMutexCache</span>(rdb *redis.Client) *<span class="code-type">MutexCache</span> {
<span class="code-keyword">return</span> &<span class="code-type">MutexCache</span>{
rdb: rdb,
retryCount: <span class="code-number">10</span>,
retryDelay: <span class="code-number">50</span> * time.Millisecond,
lockTTL: <span class="code-number">10</span> * time.Second,
}
}
<span class="code-comment">// Get 带互斥锁的缓存查询</span>
<span class="code-keyword">func</span> (c *<span class="code-type">MutexCache</span>) <span class="code-func">Get</span>(
ctx context.Context,
key <span class="code-type">string</span>,
ttl time.Duration,
loadFn <span class="code-keyword">func</span>(ctx context.Context) (<span class="code-type">interface</span>{}, <span class="code-type">error</span>),
) (<span class="code-type">interface</span>{}, <span class="code-type">error</span>) {
<span class="code-comment">// Step 1: 尝试从缓存获取</span>
val, err := c.rdb.Get(ctx, key).Result()
<span class="code-keyword">if</span> err == <span class="code-builtin">nil</span> {
<span class="code-comment">// 缓存命中,直接返回</span>
<span class="code-keyword">return</span> val, <span class="code-builtin">nil</span>
}
<span class="code-keyword">if</span> err != redis.Nil {
<span class="code-keyword">return</span> <span class="code-builtin">nil</span>, fmt.Errorf(<span class="code-string">"redis error: %w"</span>, err)
}
<span class="code-comment">// Step 2: 缓存未命中,尝试获取互斥锁</span>
lockKey := <span class="code-string">"lock:"</span> + key
<span class="code-keyword">return</span> c.getWithLock(ctx, key, lockKey, ttl, loadFn)
}
<span class="code-comment">// getWithLock 带锁的缓存重建逻辑</span>
<span class="code-keyword">func</span> (c *<span class="code-type">MutexCache</span>) <span class="code-func">getWithLock</span>(
ctx context.Context,
key, lockKey <span class="code-type">string</span>,
ttl time.Duration,
loadFn <span class="code-keyword">func</span>(ctx context.Context) (<span class="code-type">interface</span>{}, <span class="code-type">error</span>),
) (<span class="code-type">interface</span>{}, <span class="code-type">error</span>) {
<span class="code-keyword">for</span> i := <span class="code-number">0</span>; i < c.retryCount; i++ {
<span class="code-comment">// 尝试获取分布式锁</span>
locked, err := c.rdb.SetNX(ctx, lockKey, <span class="code-string">"1"</span>, c.lockTTL).Result()
<span class="code-keyword">if</span> err != <span class="code-builtin">nil</span> {
<span class="code-keyword">return</span> <span class="code-builtin">nil</span>, fmt.Errorf(<span class="code-string">"set lock error: %w"</span>, err)
}
<span class="code-keyword">if</span> locked {
<span class="code-comment">// 获取锁成功!执行数据库查询和缓存重建</span>
<span class="code-keyword">defer</span> c.rdb.Del(ctx, lockKey) <span class="code-comment">// 确保释放锁</span>
<span class="code-comment">// 双重检查:获取锁后再次检查缓存</span>
<span class="code-comment">// (可能其他线程已在我们等锁期间重建了缓存)</span>
val, err := c.rdb.Get(ctx, key).Result()
<span class="code-keyword">if</span> err == <span class="code-builtin">nil</span> {
<span class="code-keyword">return</span> val, <span class="code-builtin">nil</span>
}
<span class="code-comment">// 从数据库加载数据</span>
data, err := loadFn(ctx)
<span class="code-keyword">if</span> err != <span class="code-builtin">nil</span> {
<span class="code-keyword">return</span> <span class="code-builtin">nil</span>, fmt.Errorf(<span class="code-string">"load from db error: %w"</span>, err)
}
<span class="code-comment">// 写入缓存</span>
c.rdb.Set(ctx, key, data, ttl)
<span class="code-keyword">return</span> data, <span class="code-builtin">nil</span>
}
<span class="code-comment">// 获取锁失败,等待后重试</span>
time.Sleep(c.retryDelay)
<span class="code-comment">// 重试时先检查缓存是否已重建</span>
val, err := c.rdb.Get(ctx, key).Result()
<span class="code-keyword">if</span> err == <span class="code-builtin">nil</span> {
<span class="code-keyword">return</span> val, <span class="code-builtin">nil</span>
}
}
<span class="code-keyword">return</span> <span class="code-builtin">nil</span>, errors.New(<span class="code-string">"timeout: failed to acquire lock after retries"</span>)
}</code></pre>
</div>
<h3>📊 两种方案对比</h3>
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>对比维度</th>
<th>逻辑过期(永不过期)</th>
<th>互斥锁排队</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>数据一致性</strong></td>
<td>弱一致性(可能返回旧数据)</td>
<td>强一致性(等待最新数据)</td>
</tr>
<tr>
<td><strong>可用性</strong></td>
<td>高(不阻塞,始终返回数据)</td>
<td>中(可能等待超时)</td>
</tr>
<tr>
<td><strong>实现复杂度</strong></td>
<td>中等(需维护逻辑过期字段)</td>
<td>中等(需处理锁竞争)</td>
</tr>
<tr>
<td><strong>资源消耗</strong></td>
<td>额外内存(过期时间字段)</td>
<td>线程等待(CPU/内存)</td>
</tr>
<tr>
<td><strong>适用场景</strong></td>
<td>对一致性要求不高的热点数据</td>
<td>对一致性要求较高的场景</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- ==================== TAB 2: 缓存雪崩 ==================== -->
<div class="tab-content" id="tab-avalanche">
<!-- 问题描述 -->
<div class="section">
<h2 class="section-title"><span class="icon">🏔️</span> 缓存雪崩 — 问题描述</h2>
<div class="alert alert-danger">
<span class="alert-icon">⚠️</span>
<div>
<strong>核心问题:</strong>大量缓存 Key 在同一时刻<strong>集中过期</strong>,或者 <strong>Redis 服务整体宕机</strong>,导致所有请求全部穿透到数据库,引发数据库崩溃,进而整个系统瘫痪。
</div>
</div>
<div class="card card-highlight">
<h3>🎯 与缓存击穿的区别</h3>
<div class="comparison">
<div class="comparison-item bad">
<div class="comparison-title">❌ 缓存击穿</div>
<ul>
<li><strong>单个</strong>热点 Key 过期</li>
<li>影响范围:一个数据的并发访问</li>
<li>关键词:<span class="tag tag-red">热点</span> <span class="tag tag-red">单个Key</span></li>
</ul>
</div>
<div class="comparison-item bad">
<div class="comparison-title">❌ 缓存雪崩</div>
<ul>
<li><strong>大量</strong> Key 同时过期</li>
<li>影响范围:整个系统的缓存层</li>
<li>关键词:<span class="tag tag-yellow">批量</span> <span class="tag tag-yellow">同时过期</span></li>
</ul>
</div>
</div>
</div>
<h3>📊 雪崩发生的两种场景</h3>
<div class="card-grid">
<div class="card card-danger">
<h4>场景一:大量 Key 同时过期</h4>
<p>缓存数据在初始化时设置了相同的 TTL(如都是 30 分钟),30 分钟后所有缓存同时失效。</p>
<div style="margin-top: 12px;">
<span class="tag tag-red">TTL 相同</span>
<span class="tag tag-red">同时失效</span>
</div>
</div>
<div class="card card-danger">
<h4>场景二:Redis 服务宕机</h4>
<p>Redis 节点故障、网络分区、内存溢出等原因导致整个缓存服务不可用。</p>
<div style="margin-top: 12px;">
<span class="tag tag-red">服务不可用</span>
<span class="tag tag-red">全部 miss</span>
</div>
</div>
</div>
<h3>📈 雪崩影响示意</h3>
<div class="diagram">
<div class="arch-row">
<div class="arch-node flow-client">用户请求 (10000 QPS)</div>
</div>
<div class="arch-connector">↓</div>
<div class="arch-row">
<div class="arch-node flow-cache" style="opacity: 0.4; text-decoration: line-through;">Redis 缓存层 (全部失效/宕机)</div>
</div>
<div class="arch-connector">↓ 全部穿透</div>
<div class="arch-row">
<div class="arch-node flow-db" style="border-color: var(--danger); color: #991b1b;">MySQL (10000 QPS 直接冲击 💀)</div>
</div>
<div class="arch-connector">↓</div>
<div class="arch-row">
<div class="arch-node" style="background: rgba(239,68,68,0.1); border: 1px solid var(--danger); color: #dc2626;">系统崩溃 ❌</div>
</div>
</div>
</div>
<!-- 解决方案总览 -->
<div class="section">
<h2 class="section-title"><span class="icon">🛡️</span> 解决方案总览</h2>
<div class="card card-highlight">
<p>针对缓存雪崩问题,需要从<strong>预防过期集中</strong>、<strong>限流降级保护</strong>、<strong>高可用架构</strong>三个层面进行防护:</p>
</div>
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>方案</th>
<th>核心思路</th>
<th>防护层面</th>
<th>实现复杂度</th>
<th>适用场景</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>🎲 随机失效时间</strong></td>
<td>在基础 TTL 上加随机值,分散 Key 的过期时间</td>
<td>预防层</td>
<td>极低</td>
<td>所有场景(必做)</td>
</tr>
<tr>
<td><strong>🔐 加锁排队 + 限流降级</strong></td>
<td>通过互斥锁 + 信号量 + 限流器控制并发,保护数据库</td>
<td>保护层</td>
<td>中等</td>
<td>高并发场景</td>
</tr>
<tr>
<td><strong>🏗️ Redis 高可用架构</strong></td>
<td>使用哨兵模式或 Cluster 集群,防止 Redis 整体宕机</td>
<td>架构层</td>
<td>中高</td>
<td>生产环境(推荐)</td>
</tr>
</tbody>
</table>
</div>
<div class="alert alert-info">
<span class="alert-icon">💡</span>
<div>
<strong>最佳实践:</strong>三种方案应<strong>组合使用</strong>。随机 TTL 是基础防御,限流降级是兜底保护,Redis 高可用是架构保障。生产环境建议至少使用<strong>哨兵模式</strong>,大型项目推荐 <strong>Redis Cluster</strong>。
</div>
</div>
</div>
<!-- 解决方案一:随机过期时间 -->
<div class="section">
<h2 class="section-title"><span class="icon">🎲</span> 解决方案一:随机失效时间</h2>
<div class="alert alert-info">
<span class="alert-icon">💡</span>
<div>
<strong>核心思路:</strong>在设置缓存 TTL 时,在基础过期时间上增加一个<strong>随机值</strong>,使得不同 Key 的过期时间分散开,避免同时过期。
</div>
</div>
<h3>📐 原理说明</h3>
<div class="card card-highlight">
<p><strong>公式:</strong><span class="inline-code">实际TTL = 基础TTL + random(0, 随机范围)</span></p>
<p>例如基础 TTL 为 30 分钟,随机范围为 1~5 分钟,则实际过期时间在 31~35 分钟之间随机分布。</p>
<ul>
<li>即使有 100 万个 Key,它们的过期时间也会分散在 4 分钟的窗口内</li>
<li>任何时刻过期的 Key 数量只是总量的很小一部分</li>
<li>数据库压力被平滑分散</li>
</ul>
</div>
<h3>💻 Go 代码实现</h3>
<div class="code-block">
<div class="code-header">
<div class="code-dots"><span></span><span></span><span></span></div>
<span>random_ttl.go — 随机过期时间</span>
</div>
<pre><code><span class="code-keyword">package</span> cache
<span class="code-keyword">import</span> (
<span class="code-string">"context"</span>
<span class="code-string">"math/rand"</span>
<span class="code-string">"time"</span>
<span class="code-string">"github.com/redis/go-redis/v9"</span>
)
<span class="code-comment">// RandomTTLCache 随机TTL缓存实现</span>
<span class="code-keyword">type</span> <span class="code-type">RandomTTLCache</span> <span class="code-keyword">struct</span> {
rdb *redis.Client
baseTTL time.Duration <span class="code-comment">// 基础过期时间</span>
jitterMax time.Duration <span class="code-comment">// 随机抖动最大值</span>
}
<span class="code-keyword">func</span> <span class="code-func">NewRandomTTLCache</span>(rdb *redis.Client, baseTTL, jitterMax time.Duration) *<span class="code-type">RandomTTLCache</span> {
<span class="code-keyword">return</span> &<span class="code-type">RandomTTLCache</span>{
rdb: rdb,
baseTTL: baseTTL,
jitterMax: jitterMax,
}
}
<span class="code-comment">// Set 设置缓存(带随机TTL)</span>
<span class="code-keyword">func</span> (c *<span class="code-type">RandomTTLCache</span>) <span class="code-func">Set</span>(ctx context.Context, key <span class="code-type">string</span>, value <span class="code-type">interface</span>{}) <span class="code-type">error</span> {
<span class="code-comment">// 计算实际TTL = 基础TTL + 随机抖动</span>
actualTTL := c.baseTTL + time.Duration(rand.Int63n(<span class="code-type">int64</span>(c.jitterMax)))
<span class="code-keyword">return</span> c.rdb.Set(ctx, key, value, actualTTL).Err()
}
<span class="code-comment">// SetBatch 批量设置缓存(每个Key独立随机TTL)</span>
<span class="code-keyword">func</span> (c *<span class="code-type">RandomTTLCache</span>) <span class="code-func">SetBatch</span>(ctx context.Context, items <span class="code-keyword">map</span>[<span class="code-type">string</span>]<span class="code-type">interface</span>{}) <span class="code-type">error</span> {
pipe := c.rdb.Pipeline()
<span class="code-keyword">for</span> key, value := <span class="code-keyword">range</span> items {
<span class="code-comment">// 每个Key使用独立的随机TTL</span>
actualTTL := c.baseTTL + time.Duration(rand.Int63n(<span class="code-type">int64</span>(c.jitterMax)))
pipe.Set(ctx, key, value, actualTTL)
}
_, err := pipe.Exec(ctx)
<span class="code-keyword">return</span> err
}
<span class="code-comment">// 使用示例</span>
<span class="code-keyword">func</span> <span class="code-func">Example</span>() {
rdb := redis.NewClient(&redis.Options{Addr: <span class="code-string">"localhost:6379"</span>})
<span class="code-comment">// 基础TTL 30分钟,随机抖动 0~5分钟</span>
cache := NewRandomTTLCache(rdb, <span class="code-number">30</span>*time.Minute, <span class="code-number">5</span>*time.Minute)
<span class="code-comment">// 设置商品缓存</span>
ctx := context.Background()
cache.Set(ctx, <span class="code-string">"product:1001"</span>, productData1) <span class="code-comment">// 可能 32分钟后过期</span>
cache.Set(ctx, <span class="code-string">"product:1002"</span>, productData2) <span class="code-comment">// 可能 34分钟后过期</span>
cache.Set(ctx, <span class="code-string">"product:1003"</span>, productData3) <span class="code-comment">// 可能 31分钟后过期</span>
<span class="code-comment">// ... 每个Key的过期时间都不同,避免雪崩</span>
}</code></pre>
</div>
<h3>📊 效果对比</h3>
<div class="diagram">
<h4 style="margin-bottom: 16px;">固定 TTL vs 随机 TTL — 过期分布</h4>
<div style="text-align: left; padding: 0 20px;">
<p style="color: #dc2626;"><strong>❌ 固定 TTL (全部 30 分钟):</strong></p>
<div style="display: flex; align-items: center; gap: 4px; margin: 8px 0 20px;">
<div style="width: 95%; height: 30px; background: rgba(239,68,68,0.2); border-radius: 4px; display: flex; align-items: center; justify-content: center; font-size: 0.8rem; color: #991b1b; border: 1px solid rgba(239,68,68,0.3);">T=30min: 100% Key 同时过期 💥</div>
</div>
<p style="color: #16a34a;"><strong>✅ 随机 TTL (30~35 分钟):</strong></p>
<div style="display: flex; align-items: end; gap: 2px; margin: 8px 0; height: 60px;">
<div style="width: 8%; height: 15%; background: rgba(16,185,129,0.35); border-radius: 2px;" title="T=30min"></div>
<div style="width: 8%; height: 25%; background: rgba(16,185,129,0.35); border-radius: 2px;" title="T=31min"></div>
<div style="width: 8%; height: 35%; background: rgba(16,185,129,0.35); border-radius: 2px;" title="T=32min"></div>
<div style="width: 8%; height: 50%; background: rgba(16,185,129,0.35); border-radius: 2px;" title="T=33min"></div>
<div style="width: 8%; height: 45%; background: rgba(16,185,129,0.35); border-radius: 2px;" title="T=34min"></div>
<div style="width: 8%; height: 30%; background: rgba(16,185,129,0.35); border-radius: 2px;" title="T=35min"></div>
<div style="width: 8%; height: 10%; background: rgba(16,185,129,0.35); border-radius: 2px;" title="T=36min"></div>
</div>
<p style="font-size: 0.85rem; color: var(--text-muted);">每个时间点只有少量 Key 过期,数据库压力被平滑分散</p>
</div>
</div>
</div>
<!-- 解决方案二:加锁排队 -->
<div class="section">
<h2 class="section-title"><span class="icon">🔐</span> 解决方案二:加锁排队(限流降级)</h2>
<div class="alert alert-info">
<span class="alert-icon">💡</span>
<div>
<strong>核心思路:</strong>即使大量缓存同时失效,通过<strong>互斥锁</strong>控制同时查询数据库的并发数量,配合<strong>限流降级</strong>策略保护数据库。
</div>
</div>
<h3>💻 Go 代码实现 — 多级保护</h3>
<div class="code-block">
<div class="code-header">
<div class="code-dots"><span></span><span></span><span></span></div>
<span>avalanche_protection.go — 雪崩多级保护</span>
</div>
<pre><code><span class="code-keyword">package</span> cache
<span class="code-keyword">import</span> (
<span class="code-string">"context"</span>
<span class="code-string">"errors"</span>
<span class="code-string">"math/rand"</span>
<span class="code-string">"sync"</span>
<span class="code-string">"time"</span>
<span class="code-string">"github.com/redis/go-redis/v9"</span>
<span class="code-string">"golang.org/x/time/rate"</span>
)
<span class="code-comment">// AvalancheProtection 雪崩综合防护</span>
<span class="code-keyword">type</span> <span class="code-type">AvalancheProtection</span> <span class="code-keyword">struct</span> {
rdb *redis.Client
limiter *rate.Limiter <span class="code-comment">// 限流器</span>
semaphore <span class="code-keyword">chan</span> <span class="code-keyword">struct</span>{} <span class="code-comment">// 信号量,控制并发查DB的数量</span>
mu sync.Mutex
baseTTL time.Duration
jitterMax time.Duration
}
<span class="code-keyword">func</span> <span class="code-func">NewAvalancheProtection</span>(rdb *redis.Client, maxDBConcurrency <span class="code-type">int</span>) *<span class="code-type">AvalancheProtection</span> {
<span class="code-keyword">return</span> &<span class="code-type">AvalancheProtection</span>{
rdb: rdb,
limiter: rate.NewLimiter(<span class="code-number">1000</span>, <span class="code-number">2000</span>), <span class="code-comment">// 每秒1000个请求,桶容量2000</span>
semaphore: <span class="code-builtin">make</span>(<span class="code-keyword">chan</span> <span class="code-keyword">struct</span>{}, maxDBConcurrency), <span class="code-comment">// 最多N个goroutine同时查DB</span>
baseTTL: <span class="code-number">30</span> * time.Minute,
jitterMax: <span class="code-number">10</span> * time.Minute,
}
}
<span class="code-comment">// Get 带多级保护的缓存查询</span>
<span class="code-keyword">func</span> (p *<span class="code-type">AvalancheProtection</span>) <span class="code-func">Get</span>(
ctx context.Context,
key <span class="code-type">string</span>,
loadFn <span class="code-keyword">func</span>(ctx context.Context) (<span class="code-type">interface</span>{}, <span class="code-type">error</span>),
) (<span class="code-type">interface</span>{}, <span class="code-type">error</span>) {
<span class="code-comment">// 第一层保护:限流</span>
<span class="code-keyword">if</span> !p.limiter.Allow() {
<span class="code-keyword">return</span> <span class="code-builtin">nil</span>, errors.New(<span class="code-string">"rate limit exceeded, please retry later"</span>)
}
<span class="code-comment">// 尝试从缓存获取</span>
val, err := p.rdb.Get(ctx, key).Result()
<span class="code-keyword">if</span> err == <span class="code-builtin">nil</span> {
<span class="code-keyword">return</span> val, <span class="code-builtin">nil</span>
}
<span class="code-comment">// 第二层保护:信号量控制并发查DB数量</span>
<span class="code-keyword">select</span> {
<span class="code-keyword">case</span> p.semaphore <- <span class="code-keyword">struct</span>{}{}:
<span class="code-keyword">defer</span> <span class="code-keyword">func</span>() { <-p.semaphore }()
<span class="code-keyword">case</span> <-time.After(<span class="code-number">3</span> * time.Second):
<span class="code-comment">// 等待超时,返回降级数据</span>
<span class="code-keyword">return</span> p.getFallbackData(key)
<span class="code-keyword">case</span> <-ctx.Done():
<span class="code-keyword">return</span> <span class="code-builtin">nil</span>, ctx.Err()
}
<span class="code-comment">// 第三层保护:分布式锁,防止同一Key重复查DB</span>
lockKey := <span class="code-string">"lock:"</span> + key
locked := p.rdb.SetNX(ctx, lockKey, <span class="code-string">"1"</span>, <span class="code-number">10</span>*time.Second).Val()
<span class="code-keyword">if</span> !locked {
<span class="code-comment">// 其他goroutine正在重建该Key的缓存,等待</span>
time.Sleep(<span class="code-number">100</span> * time.Millisecond)
<span class="code-comment">// 重试从缓存获取</span>
<span class="code-keyword">return</span> p.rdb.Get(ctx, key).Result()
}
<span class="code-keyword">defer</span> p.rdb.Del(ctx, lockKey)
<span class="code-comment">// 查询数据库</span>
data, err := loadFn(ctx)
<span class="code-keyword">if</span> err != <span class="code-builtin">nil</span> {
<span class="code-keyword">return</span> p.getFallbackData(key) <span class="code-comment">// 查询失败,返回降级数据</span>
}
<span class="code-comment">// 写入缓存(随机TTL)</span>
actualTTL := p.baseTTL + time.Duration(rand.Int63n(<span class="code-type">int64</span>(p.jitterMax)))
p.rdb.Set(ctx, key, data, actualTTL)
<span class="code-keyword">return</span> data, <span class="code-builtin">nil</span>
}
<span class="code-comment">// getFallbackData 获取降级数据</span>
<span class="code-keyword">func</span> (p *<span class="code-type">AvalancheProtection</span>) <span class="code-func">getFallbackData</span>(key <span class="code-type">string</span>) (<span class="code-type">interface</span>{}, <span class="code-type">error</span>) {
<span class="code-comment">// 降级策略:返回默认值/上次缓存的快照/空结果</span>
<span class="code-keyword">return</span> <span class="code-keyword">map</span>[<span class="code-type">string</span>]<span class="code-type">interface</span>{}{
<span class="code-string">"status"</span>: <span class="code-string">"degraded"</span>,
<span class="code-string">"message"</span>: <span class="code-string">"service temporarily unavailable"</span>,
}, <span class="code-builtin">nil</span>
}</code></pre>
</div>
</div>
<!-- 解决方案三:Redis 高可用 -->
<div class="section">
<h2 class="section-title"><span class="icon">🏗️</span> 解决方案三:Redis 高可用架构</h2>
<div class="alert alert-info">
<span class="alert-icon">💡</span>
<div>
<strong>核心思路:</strong>通过 Redis 的高可用架构(主从复制、哨兵模式、Cluster 集群),确保缓存服务本身不会成为单点故障。
</div>
</div>
<h3>📐 三种高可用方案对比</h3>
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>方案</th>
<th>架构</th>
<th>自动故障转移</th>
<th>数据分片</th>
<th>适用规模</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>主从复制</strong></td>
<td>1主N从</td>
<td>❌ 需手动切换</td>
<td>❌ 全量复制</td>
<td>小型项目</td>
</tr>
<tr>
<td><strong>哨兵模式</strong></td>
<td>1主N从+哨兵</td>
<td>✅ 自动切换</td>
<td>❌ 全量复制</td>
<td>中型项目</td>
</tr>
<tr>
<td><strong>Cluster 集群</strong></td>
<td>多主多从</td>
<td>✅ 自动切换</td>
<td>✅ 16384 哈希槽</td>
<td>大型项目</td>
</tr>
</tbody>
</table>
</div>
<h3>🏗️ Redis Cluster 架构图</h3>
<div class="diagram">
<div class="arch-row">
<div class="arch-node flow-client">客户端 (Redis Cluster Client)</div>
</div>
<div class="arch-connector">↓ 根据 CRC16(key) % 16384 定位槽</div>
<div class="arch-row">
<div class="arch-node" style="background: rgba(239,68,68,0.1); border: 1px solid var(--danger); color: #dc2626;">
Master 1<br><small>Slots 0-5460</small>
</div>
<div class="arch-node" style="background: rgba(245,158,11,0.1); border: 1px solid var(--warning); color: #ca8a04;">
Master 2<br><small>Slots 5461-10922</small>
</div>
<div class="arch-node" style="background: rgba(16,185,129,0.1); border: 1px solid var(--success); color: #16a34a;">
Master 3<br><small>Slots 10923-16383</small>
</div>
</div>
<div class="arch-connector">↕ 主从复制</div>
<div class="arch-row">
<div class="arch-node" style="background: rgba(239,68,68,0.05); border: 1px dashed var(--danger); color: #dc2626;">
Slave 1<br><small>热备</small>
</div>
<div class="arch-node" style="background: rgba(245,158,11,0.05); border: 1px dashed var(--warning); color: #ca8a04;">
Slave 2<br><small>热备</small>
</div>
<div class="arch-node" style="background: rgba(16,185,129,0.05); border: 1px dashed var(--success); color: #16a34a;">
Slave 3<br><small>热备</small>
</div>
</div>
<p style="margin-top: 16px; font-size: 0.85rem; color: var(--text-muted);">
任意 Master 宕机,对应 Slave 自动提升为 Master,保证缓存服务持续可用
</p>
</div>
<h3>💻 Go 代码 — Redis Cluster 客户端配置</h3>
<div class="code-block">
<div class="code-header">
<div class="code-dots"><span></span><span></span><span></span></div>
<span>redis_cluster.go — Cluster 高可用配置</span>
</div>
<pre><code><span class="code-keyword">package</span> cache
<span class="code-keyword">import</span> (
<span class="code-string">"context"</span>
<span class="code-string">"fmt"</span>
<span class="code-string">"time"</span>
<span class="code-string">"github.com/redis/go-redis/v9"</span>
)
<span class="code-comment">// NewRedisCluster 创建 Redis Cluster 客户端</span>
<span class="code-keyword">func</span> <span class="code-func">NewRedisCluster</span>() *redis.ClusterClient {
<span class="code-keyword">return</span> redis.NewClusterClient(&redis.ClusterOptions{
<span class="code-comment">// 集群节点地址(至少配置一个,客户端会自动发现其他节点)</span>
Addrs: []<span class="code-type">string</span>{
<span class="code-string">"192.168.1.10:7000"</span>,
<span class="code-string">"192.168.1.10:7001"</span>,
<span class="code-string">"192.168.1.11:7000"</span>,
<span class="code-string">"192.168.1.11:7001"</span>,
<span class="code-string">"192.168.1.12:7000"</span>,
<span class="code-string">"192.168.1.12:7001"</span>,
},
<span class="code-comment">// 密码认证</span>
Password: <span class="code-string">"your_password"</span>,
<span class="code-comment">// 最大重试次数(故障转移期间自动重试)</span>
MaxRedirects: <span class="code-number">3</span>,
<span class="code-comment">// 读取超时</span>
ReadTimeout: <span class="code-number">3</span> * time.Second,
<span class="code-comment">// 写入超时</span>
WriteTimeout: <span class="code-number">3</span> * time.Second,
<span class="code-comment">// 连接池配置</span>
PoolSize: <span class="code-number">100</span>,
MinIdleConns: <span class="code-number">20</span>,
<span class="code-comment">// 路由函数:读请求可以路由到从节点(读写分离)</span>
RouteByLatency: <span class="code-builtin">true</span>,
<span class="code-comment">// 集群拓扑刷新间隔</span>
ClusterSlotsRefreshInterval: <span class="code-number">10</span> * time.Second,
})
}
<span class="code-comment">// SentinelClient 哨兵模式客户端</span>
<span class="code-keyword">func</span> <span class="code-func">NewSentinelClient</span>() *redis.Client {
<span class="code-keyword">return</span> redis.NewFailoverClient(&redis.FailoverOptions{
<span class="code-comment">// 哨兵节点地址</span>
SentinelAddrs: []<span class="code-type">string</span>{
<span class="code-string">"192.168.1.10:26379"</span>,
<span class="code-string">"192.168.1.11:26379"</span>,
<span class="code-string">"192.168.1.12:26379"</span>,
},
<span class="code-comment">// 主节点名称(在哨兵配置中定义的)</span>
MasterName: <span class="code-string">"mymaster"</span>,
<span class="code-comment">// 数据库密码</span>
Password: <span class="code-string">"your_password"</span>,
<span class="code-comment">// 数据库编号</span>
DB: <span class="code-number">0</span>,
<span class="code-comment">// 只读节点(读写分离)</span>
ReplicaOnly: <span class="code-builtin">false</span>,
})
}
<span class="code-comment">// HealthCheck 缓存健康检查</span>
<span class="code-keyword">func</span> <span class="code-func">HealthCheck</span>(ctx context.Context, client *redis.ClusterClient) <span class="code-type">error</span> {
<span class="code-keyword">if</span> err := client.Ping(ctx).Err(); err != <span class="code-builtin">nil</span> {
<span class="code-keyword">return</span> fmt.Errorf(<span class="code-string">"redis cluster health check failed: %w"</span>, err)
}
<span class="code-comment">// 检查集群状态</span>
clusterInfo, err := client.ClusterInfo(ctx).Result()
<span class="code-keyword">if</span> err != <span class="code-builtin">nil</span> {
<span class="code-keyword">return</span> fmt.Errorf(<span class="code-string">"failed to get cluster info: %w"</span>, err)
}
<span class="code-comment">// 验证集群状态是否为ok</span>
<span class="code-keyword">if</span> !contains(clusterInfo, <span class="code-string">"cluster_state:ok"</span>) {
<span class="code-keyword">return</span> fmt.Errorf(<span class="code-string">"cluster state is not ok"</span>)
}
<span class="code-keyword">return</span> <span class="code-builtin">nil</span>
}</code></pre>
</div>
<div class="alert alert-success">
<span class="alert-icon">✅</span>
<div>
<strong>高可用最佳实践总结:</strong>
<ul style="margin-top: 8px;">
<li>生产环境至少使用<strong>哨兵模式</strong>,推荐 <strong>Redis Cluster</strong></li>
<li>配置合理的<strong>连接池</strong>参数,避免连接耗尽</li>
<li>实现<strong>健康检查</strong>和<strong>自动熔断</strong>机制</li>
<li>开启<strong>读写分离</strong>,将读请求分散到从节点</li>
<li>配置<strong>持久化策略</strong>(RDB + AOF),防止数据丢失</li>
</ul>
</div>
</div>
</div>
</div>
<!-- ==================== TAB 3: 缓存穿透 ==================== -->
<div class="tab-content" id="tab-penetration">
<!-- 问题描述 -->
<div class="section">
<h2 class="section-title"><span class="icon">🕳️</span> 缓存穿透 — 问题描述</h2>
<div class="alert alert-danger">
<span class="alert-icon">⚠️</span>
<div>
<strong>核心问题:</strong>请求查询的数据在缓存和数据库中<strong>都不存在</strong>,每次请求都会穿透缓存直接打到数据库。如果这类请求量很大(如恶意攻击),数据库将承受巨大压力。
</div>
</div>
<div class="card card-highlight">
<h3>🎯 问题本质</h3>
<p>缓存穿透的关键词是<strong>"数据不存在"</strong>。攻击者利用不存在的数据 ID 发起大量请求,这些请求在缓存中永远 miss,每次都穿透到数据库。</p>
<ul>
<li><strong>前提条件:</strong>请求的数据在缓存和数据库中都不存在</li>
<li><strong>触发条件:</strong>大量此类请求持续到达</li>
<li><strong>直接后果:</strong>所有请求全部打到数据库,缓存形同虚设</li>
<li><strong>典型攻击:</strong>使用不存在的用户 ID、订单号等频繁查询</li>
</ul>
</div>
<h3>📊 穿透攻击示意</h3>
<div class="diagram">
<div class="arch-row">
<div class="arch-node flow-client">恶意请求: user_id=-1, user_id=999999...</div>
</div>
<div class="arch-connector">↓ 每次请求</div>
<div class="arch-row">
<div class="arch-node flow-cache">Redis 缓存<br><small>❌ 不存在 → miss</small></div>
</div>
<div class="arch-connector">↓ 穿透</div>
<div class="arch-row">
<div class="arch-node flow-db">MySQL 数据库<br><small>❌ 也不存在 → 返回空</small></div>
</div>
<div class="arch-connector">↓ 循环往复</div>
<div class="arch-row">
<div class="arch-node" style="background: rgba(239,68,68,0.1); border: 1px solid var(--danger); color: #dc2626;">
10000 QPS 全部打到数据库 💀<br>
<small>缓存完全没有起到保护作用</small>
</div>
</div>
</div>
</div>
<!-- 解决方案总览 -->
<div class="section">
<h2 class="section-title"><span class="icon">🛡️</span> 解决方案总览</h2>
<div class="card card-highlight">
<p>针对缓存穿透问题,核心思路是<strong>拦截无效请求</strong>,防止其穿透到数据库。推荐<strong>三层防护</strong>组合使用:</p>
</div>
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>方案</th>
<th>核心思路</th>
<th>防护能力</th>
<th>内存开销</th>
<th>适用场景</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>✅ 参数校验</strong></td>
<td>在请求到达缓存前,拦截明显不合法的参数(如负数 ID)</td>
<td>低(仅拦截格式错误)</td>
<td>无</td>
<td>所有场景(必做)</td>
</tr>
<tr>
<td><strong>📦 缓存空对象</strong></td>
<td>数据库查询为空时,缓存空值标记(短 TTL),防止重复穿透</td>
<td>中(防止重复查询)</td>
<td>中(空值占内存)</td>
<td>数据量不大,攻击 ID 有限</td>
</tr>
<tr>
<td><strong>🌸 布隆过滤器</strong></td>
<td>用位数组存储合法 ID,请求到达前先检查,不存在则直接拒绝</td>
<td>高(从根源拦截)</td>
<td>低(位数组省内存)</td>
<td>数据量大,防恶意攻击</td>
</tr>
</tbody>
</table>
</div>
<div class="alert alert-success">
<span class="alert-icon">✅</span>
<div>
<strong>推荐防护顺序:</strong>
<ol style="margin-top: 8px;">
<li><strong>第一层 — 参数校验:</strong>拦截明显不合法请求(零成本,必做)</li>
<li><strong>第二层 — 布隆过滤器:</strong>拦截数据一定不存在的请求(低成本,高效率)</li>
<li><strong>第三层 — 缓存空对象:</strong>兜底保护,处理布隆过滤器误判的情况</li>
</ol>
</div>
</div>
</div>
<!-- 解决方案一:参数校验 -->
<div class="section">
<h2 class="section-title"><span class="icon">✅</span> 解决方案一:参数校验</h2>
<div class="alert alert-info">
<span class="alert-icon">💡</span>
<div>
<strong>核心思路:</strong>在请求到达缓存之前,先对参数进行<strong>合法性校验</strong>,直接拦截明显不合法的请求(如 ID ≤ 0、格式错误等)。
</div>
</div>
<h3>💻 Go 代码实现</h3>
<div class="code-block">
<div class="code-header">
<div class="code-dots"><span></span><span></span><span></span></div>
<span>param_validation.go — 参数校验</span>
</div>
<pre><code><span class="code-keyword">package</span> handler
<span class="code-keyword">import</span> (
<span class="code-string">"errors"</span>
<span class="code-string">"fmt"</span>
<span class="code-string">"regexp"</span>
<span class="code-string">"strconv"</span>
)
<span class="code-keyword">var</span> (
ErrInvalidID = errors.New(<span class="code-string">"invalid parameter: id must be positive"</span>)
ErrInvalidFormat = errors.New(<span class="code-string">"invalid parameter format"</span>)
ErrIDOutOfRange = errors.New(<span class="code-string">"id out of valid range"</span>)
)
<span class="code-comment">// ParamValidator 参数校验器</span>
<span class="code-keyword">type</span> <span class="code-type">ParamValidator</span> <span class="code-keyword">struct</span> {
maxUserID <span class="code-type">int64</span> <span class="code-comment">// 最大合法用户ID(可根据业务设定)</span>
}
<span class="code-keyword">func</span> <span class="code-func">NewParamValidator</span>(maxUserID <span class="code-type">int64</span>) *<span class="code-type">ParamValidator</span> {
<span class="code-keyword">return</span> &<span class="code-type">ParamValidator</span>{maxUserID: maxUserID}
}
<span class="code-comment">// ValidateUserID 校验用户ID</span>
<span class="code-keyword">func</span> (v *<span class="code-type">ParamValidator</span>) <span class="code-func">ValidateUserID</span>(idStr <span class="code-type">string</span>) (<span class="code-type">int64</span>, <span class="code-type">error</span>) {
<span class="code-comment">// 1. 非空校验</span>
<span class="code-keyword">if</span> idStr == <span class="code-string">""</span> {
<span class="code-keyword">return</span> <span class="code-number">0</span>, fmt.Errorf(<span class="code-string">"%w: id is empty"</span>, ErrInvalidFormat)
}
<span class="code-comment">// 2. 格式校验(纯数字)</span>
matched, _ := regexp.MatchString(<span class="code-string">`^\d+$`</span>, idStr)
<span class="code-keyword">if</span> !matched {
<span class="code-keyword">return</span> <span class="code-number">0</span>, fmt.Errorf(<span class="code-string">"%w: id must be numeric"</span>, ErrInvalidFormat)
}
<span class="code-comment">// 3. 数值转换与范围校验</span>
id, err := strconv.ParseInt(idStr, <span class="code-number">10</span>, <span class="code-number">64</span>)
<span class="code-keyword">if</span> err != <span class="code-builtin">nil</span> {
<span class="code-keyword">return</span> <span class="code-number">0</span>, fmt.Errorf(<span class="code-string">"%w: %v"</span>, ErrInvalidFormat, err)
}
<span class="code-comment">// 4. 正数校验</span>
<span class="code-keyword">if</span> id <= <span class="code-number">0</span> {
<span class="code-keyword">return</span> <span class="code-number">0</span>, ErrInvalidID
}
<span class="code-comment">// 5. 范围校验(防止超大ID)</span>
<span class="code-keyword">if</span> id > v.maxUserID {
<span class="code-keyword">return</span> <span class="code-number">0</span>, fmt.Errorf(<span class="code-string">"%w: max is %d"</span>, ErrIDOutOfRange, v.maxUserID)
}
<span class="code-keyword">return</span> id, <span class="code-builtin">nil</span>
}
<span class="code-comment">// GetUserHandler 用户查询接口</span>
<span class="code-keyword">func</span> (h *<span class="code-type">Handler</span>) <span class="code-func">GetUserHandler</span>(idStr <span class="code-type">string</span>) (<span class="code-type">interface</span>{}, <span class="code-type">error</span>) {
<span class="code-comment">// 第一道防线:参数校验</span>
userID, err := h.validator.ValidateUserID(idStr)
<span class="code-keyword">if</span> err != <span class="code-builtin">nil</span> {
<span class="code-comment">// 参数不合法,直接拒绝,不查缓存也不查数据库</span>
<span class="code-keyword">return</span> <span class="code-builtin">nil</span>, fmt.Errorf(<span class="code-string">"bad request: %w"</span>, err)
}
<span class="code-comment">// 参数合法,继续走缓存查询逻辑...</span>
<span class="code-keyword">return</span> h.cache.Get(ctx, fmt.Sprintf(<span class="code-string">"user:%d"</span>, userID), ...)
}</code></pre>
</div>
<div class="alert alert-warning">
<span class="alert-icon">⚡</span>
<div>
<strong>局限性:</strong>参数校验只能拦截<strong>明显不合法</strong>的参数(如负数、格式错误),但无法拦截<strong>格式合法但数据不存在</strong>的请求(如 user_id=999999 格式正确但用户不存在)。因此需要配合其他方案。
</div>
</div>
</div>
<!-- 解决方案二:缓存空对象 -->
<div class="section">
<h2 class="section-title"><span class="icon">📦</span> 解决方案二:缓存空对象</h2>
<div class="alert alert-info">
<span class="alert-icon">💡</span>
<div>
<strong>核心思路:</strong>当数据库查询结果为空时,<strong>仍然在缓存中写入一个空值标记</strong>,并设置较短的 TTL。后续相同请求命中缓存中的空值后直接返回,不再穿透到数据库。
</div>
</div>
<h3>📐 流程说明</h3>
<div class="diagram">
<div class="flow-diagram">
<div class="flow-box flow-client">请求: user_id=999999</div>
<span class="flow-arrow">→</span>
<div class="flow-box flow-cache">查缓存</div>
</div>
<div style="margin: 12px 0; text-align: center; color: var(--text-muted);">
<strong>第一次请求:</strong>缓存 miss → 查 DB → DB 也空 → <span style="color: #16a34a;">写入空值到缓存(TTL=60s)</span> → 返回空
</div>
<div class="flow-diagram">
<div class="flow-box flow-client">请求: user_id=999999</div>
<span class="flow-arrow">→</span>
<div class="flow-box flow-cache">查缓存 → 命中空值</div>
<span class="flow-arrow">→</span>
<div class="flow-box" style="background: rgba(16,185,129,0.1); border: 1px solid var(--success); color: #16a34a;">直接返回空 ✅</div>
</div>
<div style="margin: 12px 0; text-align: center; color: var(--text-muted);">
<strong>后续请求:</strong>在 TTL 内全部命中缓存空值,<span style="color: #16a34a;">不再穿透到数据库</span>
</div>
</div>
<h3>💻 Go 代码实现</h3>
<div class="code-block">
<div class="code-header">
<div class="code-dots"><span></span><span></span><span></span></div>
<span>cache_null.go — 缓存空对象方案</span>
</div>
<pre><code><span class="code-keyword">package</span> cache
<span class="code-keyword">import</span> (
<span class="code-string">"context"</span>
<span class="code-string">"encoding/json"</span>
<span class="code-string">"errors"</span>
<span class="code-string">"time"</span>
<span class="code-string">"github.com/redis/go-redis/v9"</span>
)
<span class="code-keyword">var</span> (
ErrDataNotFound = errors.New(<span class="code-string">"data not found"</span>)
)
<span class="code-comment">// 空值标记常量</span>
<span class="code-keyword">const</span> (
NullValue = <span class="code-string">"@@NULL@@"</span> <span class="code-comment">// 空值标记字符串</span>
NullTTL = <span class="code-number">60</span> * time.Second <span class="code-comment">// 空值的TTL(较短,防止长期占用内存)</span>
NormalTTL = <span class="code-number">30</span> * time.Minute <span class="code-comment">// 正常数据的TTL</span>
)
<span class="code-comment">// NullCache 支持缓存空对象的缓存实现</span>
<span class="code-keyword">type</span> <span class="code-type">NullCache</span> <span class="code-keyword">struct</span> {
rdb *redis.Client
nullTTL time.Duration
normalTTL time.Duration
}
<span class="code-keyword">func</span> <span class="code-func">NewNullCache</span>(rdb *redis.Client) *<span class="code-type">NullCache</span> {
<span class="code-keyword">return</span> &<span class="code-type">NullCache</span>{
rdb: rdb,
nullTTL: NullTTL,
normalTTL: NormalTTL,
}
}
<span class="code-comment">// Get 获取数据(自动处理空值缓存)</span>
<span class="code-keyword">func</span> (c *<span class="code-type">NullCache</span>) <span class="code-func">Get</span>(
ctx context.Context,
key <span class="code-type">string</span>,
loadFn <span class="code-keyword">func</span>(ctx context.Context) (<span class="code-type">interface</span>{}, <span class="code-type">error</span>),
) (<span class="code-type">interface</span>{}, <span class="code-type">error</span>) {
<span class="code-comment">// Step 1: 从缓存获取</span>
val, err := c.rdb.Get(ctx, key).Result()
<span class="code-keyword">if</span> err == <span class="code-builtin">nil</span> {
<span class="code-comment">// 缓存命中</span>
<span class="code-comment">// 检查是否是空值标记</span>
<span class="code-keyword">if</span> val == NullValue {
<span class="code-comment">// 命中空值,直接返回"数据不存在",不查数据库</span>
<span class="code-keyword">return</span> <span class="code-builtin">nil</span>, ErrDataNotFound
}
<span class="code-comment">// 正常数据,反序列化返回</span>
<span class="code-keyword">var</span> result <span class="code-type">interface</span>{}
json.Unmarshal([]<span class="code-type">byte</span>(val), &result)
<span class="code-keyword">return</span> result, <span class="code-builtin">nil</span>
}
<span class="code-keyword">if</span> err != redis.Nil {
<span class="code-keyword">return</span> <span class="code-builtin">nil</span>, err
}
<span class="code-comment">// Step 2: 缓存未命中,查询数据库</span>
data, err := loadFn(ctx)
<span class="code-keyword">if</span> err != <span class="code-builtin">nil</span> {
<span class="code-keyword">return</span> <span class="code-builtin">nil</span>, err
}
<span class="code-comment">// Step 3: 判断数据库查询结果</span>
<span class="code-keyword">if</span> data == <span class="code-builtin">nil</span> {
<span class="code-comment">// 数据库也不存在 → 缓存空值</span>
c.rdb.Set(ctx, key, NullValue, c.nullTTL)
<span class="code-keyword">return</span> <span class="code-builtin">nil</span>, ErrDataNotFound
}
<span class="code-comment">// 数据库存在 → 缓存正常数据</span>
dataBytes, _ := json.Marshal(data)
c.rdb.Set(ctx, key, <span class="code-type">string</span>(dataBytes), c.normalTTL)
<span class="code-keyword">return</span> data, <span class="code-builtin">nil</span>
}
<span class="code-comment">// 使用示例</span>
<span class="code-keyword">func</span> <span class="code-func">Example</span>() {
cache := NewNullCache(rdb)
<span class="code-comment">// 查询用户(用户不存在的情况)</span>
data, err := cache.Get(ctx, <span class="code-string">"user:999999"</span>, <span class="code-keyword">func</span>(ctx context.Context) (<span class="code-type">interface</span>{}, <span class="code-type">error</span>) {
<span class="code-keyword">return</span> db.QueryUser(ctx, <span class="code-number">999999</span>) <span class="code-comment">// 返回 nil, nil</span>
})
<span class="code-keyword">if</span> errors.Is(err, ErrDataNotFound) {
<span class="code-comment">// 数据不存在(已被缓存为空值,后续请求不再查DB)</span>
<span class="code-keyword">return</span> <span class="code-string">"user not found"</span>
}
}</code></pre>
</div>
<div class="card-grid">
<div class="card card-success">
<h4>✅ 优点</h4>
<ul>
<li>实现简单,改动小</li>
<li>有效防止对不存在数据的重复查询</li>
<li>空值 TTL 较短,不会长期占用内存</li>
</ul>
</div>
<div class="card card-danger">
<h4>❌ 缺点</h4>
<ul>
<li>如果攻击者使用<strong>大量不同的随机 ID</strong>,仍会写入大量空值缓存</li>
<li>内存可能被空值占满(内存浪费)</li>
<li>存在短暂的数据不一致窗口(数据新建后,空值缓存未过期)</li>
</ul>
</div>
</div>
</div>
<!-- 解决方案三:布隆过滤器 -->
<div class="section">
<h2 class="section-title"><span class="icon">🌸</span> 解决方案三:布隆过滤器</h2>
<div class="alert alert-info">
<span class="alert-icon">💡</span>
<div>
<strong>核心思路:</strong>使用<strong>布隆过滤器(Bloom Filter)</strong>存储所有合法的数据 ID。请求到达时先查询布隆过滤器,如果判定"不存在"则直接拒绝,从根源上杜绝无效请求穿透到缓存和数据库。
</div>
</div>
<h3>📐 布隆过滤器原理</h3>
<div class="card card-highlight">
<h4>数据结构</h4>
<p>布隆过滤器由一个<strong>很长的二进制位数组</strong>和<strong>多个哈希函数</strong>组成。</p>
<ul>
<li><strong>插入元素:</strong>用 k 个哈希函数计算 k 个位置,将对应位全部置为 1</li>
<li><strong>查询元素:</strong>用 k 个哈希函数计算 k 个位置,检查对应位是否全为 1</li>
</ul>
<h4>核心特性</h4>
<div class="comparison">
<div class="comparison-item good">
<div class="comparison-title">✅ 判定"不存在" — 一定准确</div>
<p>如果任一位为 0,则该元素<strong>一定不存在</strong>。这个判断 100% 准确。</p>
</div>
<div class="comparison-item bad">
<div class="comparison-title">⚠️ 判定"存在" — 可能误判</div>
<p>如果所有位都为 1,该元素<strong>可能存在</strong>(有一定误判率/假阳性率)。</p>
</div>
</div>
</div>
<h3>🔬 布隆过滤器可视化</h3>
<div class="diagram">
<h4 style="margin-bottom: 16px;">位数组状态演示</h4>
<p style="font-size: 0.85rem; color: var(--text-muted); margin-bottom: 12px;">
假设位数组长度 16,使用 3 个哈希函数 H1, H2, H3
</p>
<p style="margin-bottom: 8px;"><strong>初始状态(全 0):</strong></p>
<div class="bloom-visual">
<div class="bloom-bit zero">0</div>
<div class="bloom-bit zero">0</div>
<div class="bloom-bit zero">0</div>
<div class="bloom-bit zero">0</div>
<div class="bloom-bit zero">0</div>
<div class="bloom-bit zero">0</div>
<div class="bloom-bit zero">0</div>
<div class="bloom-bit zero">0</div>
<div class="bloom-bit zero">0</div>
<div class="bloom-bit zero">0</div>
<div class="bloom-bit zero">0</div>
<div class="bloom-bit zero">0</div>
<div class="bloom-bit zero">0</div>
<div class="bloom-bit zero">0</div>
<div class="bloom-bit zero">0</div>
<div class="bloom-bit zero">0</div>
</div>
<p style="margin: 16px 0 8px;"><strong>插入 "user:1001" → H1=1, H2=5, H3=12:</strong></p>
<div class="bloom-visual">
<div class="bloom-bit zero">0</div>
<div class="bloom-bit one">1</div>
<div class="bloom-bit zero">0</div>
<div class="bloom-bit zero">0</div>
<div class="bloom-bit zero">0</div>
<div class="bloom-bit one">1</div>
<div class="bloom-bit zero">0</div>
<div class="bloom-bit zero">0</div>
<div class="bloom-bit zero">0</div>
<div class="bloom-bit zero">0</div>
<div class="bloom-bit zero">0</div>
<div class="bloom-bit zero">0</div>
<div class="bloom-bit one">1</div>
<div class="bloom-bit zero">0</div>
<div class="bloom-bit zero">0</div>
<div class="bloom-bit zero">0</div>
</div>
<p style="margin: 16px 0 8px;"><strong>插入 "user:1002" → H1=3, H2=5, H3=9:</strong></p>
<div class="bloom-visual">
<div class="bloom-bit zero">0</div>
<div class="bloom-bit one">1</div>
<div class="bloom-bit zero">0</div>
<div class="bloom-bit one">1</div>
<div class="bloom-bit zero">0</div>
<div class="bloom-bit one">1</div>
<div class="bloom-bit zero">0</div>
<div class="bloom-bit zero">0</div>
<div class="bloom-bit zero">0</div>
<div class="bloom-bit one">1</div>
<div class="bloom-bit zero">0</div>
<div class="bloom-bit zero">0</div>
<div class="bloom-bit one">1</div>
<div class="bloom-bit zero">0</div>
<div class="bloom-bit zero">0</div>
<div class="bloom-bit zero">0</div>
</div>
<p style="margin: 16px 0 8px;"><strong>查询 "user:9999" → H1=2, H2=7, H3=14:</strong></p>
<div class="bloom-visual">
<div class="bloom-bit zero">0</div>
<div class="bloom-bit one">1</div>
<div class="bloom-bit highlight">0</div>
<div class="bloom-bit one">1</div>
<div class="bloom-bit zero">0</div>
<div class="bloom-bit one">1</div>
<div class="bloom-bit zero">0</div>
<div class="bloom-bit highlight">0</div>
<div class="bloom-bit zero">0</div>
<div class="bloom-bit one">1</div>
<div class="bloom-bit zero">0</div>
<div class="bloom-bit zero">0</div>
<div class="bloom-bit one">1</div>
<div class="bloom-bit zero">0</div>
<div class="bloom-bit highlight">0</div>
<div class="bloom-bit zero">0</div>
</div>
<p style="color: #16a34a; font-weight: 600;">→ 位2、位7、位14 有0 → <strong>一定不存在!直接拒绝 ✅</strong></p>
</div>
<h3>💻 Go 代码实现</h3>
<div class="code-block">
<div class="code-header">
<div class="code-dots"><span></span><span></span><span></span></div>
<span>bloom_filter.go — 布隆过滤器实现</span>
</div>
<pre><code><span class="code-keyword">package</span> cache
<span class="code-keyword">import</span> (
<span class="code-string">"context"</span>
<span class="code-string">"hash/fnv"</span>
<span class="code-string">"math"</span>
<span class="code-string">"github.com/redis/go-redis/v9"</span>
)
<span class="code-comment">// BloomFilter 基于 Redis 的布隆过滤器实现</span>
<span class="code-keyword">type</span> <span class="code-type">BloomFilter</span> <span class="code-keyword">struct</span> {
rdb *redis.Client
key <span class="code-type">string</span> <span class="code-comment">// Redis 中存储位数组的 key</span>
bitSize <span class="code-type">uint</span> <span class="code-comment">// 位数组大小</span>
hashNum <span class="code-type">uint</span> <span class="code-comment">// 哈希函数个数</span>
}
<span class="code-comment">// NewBloomFilter 创建布隆过滤器</span>
<span class="code-comment">// expectedInsertions: 预计插入的元素数量</span>
<span class="code-comment">// falsePositiveRate: 期望的误判率(如 0.01 表示 1%)</span>
<span class="code-keyword">func</span> <span class="code-func">NewBloomFilter</span>(
rdb *redis.Client,
key <span class="code-type">string</span>,
expectedInsertions <span class="code-type">uint64</span>,
falsePositiveRate <span class="code-type">float64</span>,
) *<span class="code-type">BloomFilter</span> {
<span class="code-comment">// 计算最优位数组大小: m = -(n * ln(p)) / (ln(2))^2</span>
bitSize := <span class="code-type">uint</span>(math.Ceil(
-<span class="code-type">float64</span>(expectedInsertions) * math.Log(falsePositiveRate) /
(math.Log(<span class="code-number">2</span>) * math.Log(<span class="code-number">2</span>)),
))
<span class="code-comment">// 计算最优哈希函数数量: k = (m/n) * ln(2)</span>
hashNum := <span class="code-type">uint</span>(math.Ceil(
(<span class="code-type">float64</span>(bitSize) / <span class="code-type">float64</span>(expectedInsertions)) * math.Log(<span class="code-number">2</span>),
))
<span class="code-keyword">return</span> &<span class="code-type">BloomFilter</span>{
rdb: rdb,
key: key,
bitSize: bitSize,
hashNum: hashNum,
}
}
<span class="code-comment">// Add 添加元素到布隆过滤器</span>
<span class="code-keyword">func</span> (bf *<span class="code-type">BloomFilter</span>) <span class="code-func">Add</span>(ctx context.Context, value <span class="code-type">string</span>) <span class="code-type">error</span> {
pipe := bf.rdb.Pipeline()
<span class="code-keyword">for</span> i := <span class="code-type">uint</span>(<span class="code-number">0</span>); i < bf.hashNum; i++ {
index := bf.hash(value, i)
pipe.SetBit(ctx, bf.key, <span class="code-type">int64</span>(index), <span class="code-number">1</span>)
}
_, err := pipe.Exec(ctx)
<span class="code-keyword">return</span> err
}
<span class="code-comment">// AddBatch 批量添加元素</span>
<span class="code-keyword">func</span> (bf *<span class="code-type">BloomFilter</span>) <span class="code-func">AddBatch</span>(ctx context.Context, values []<span class="code-type">string</span>) <span class="code-type">error</span> {
pipe := bf.rdb.Pipeline()
<span class="code-keyword">for</span> _, value := <span class="code-keyword">range</span> values {
<span class="code-keyword">for</span> i := <span class="code-type">uint</span>(<span class="code-number">0</span>); i < bf.hashNum; i++ {
index := bf.hash(value, i)
pipe.SetBit(ctx, bf.key, <span class="code-type">int64</span>(index), <span class="code-number">1</span>)
}
}
_, err := pipe.Exec(ctx)
<span class="code-keyword">return</span> err
}
<span class="code-comment">// Exists 检查元素是否可能存在</span>
<span class="code-comment">// 返回 true: 元素可能存在(需要继续查缓存/DB)</span>
<span class="code-comment">// 返回 false: 元素一定不存在(直接拒绝)</span>
<span class="code-keyword">func</span> (bf *<span class="code-type">BloomFilter</span>) <span class="code-func">Exists</span>(ctx context.Context, value <span class="code-type">string</span>) (<span class="code-type">bool</span>, <span class="code-type">error</span>) {
<span class="code-keyword">for</span> i := <span class="code-type">uint</span>(<span class="code-number">0</span>); i < bf.hashNum; i++ {
index := bf.hash(value, i)
bit, err := bf.rdb.GetBit(ctx, bf.key, <span class="code-type">int64</span>(index)).Result()
<span class="code-keyword">if</span> err != <span class="code-builtin">nil</span> {
<span class="code-keyword">return</span> <span class="code-builtin">false</span>, err
}
<span class="code-keyword">if</span> bit == <span class="code-number">0</span> {
<span class="code-comment">// 任一位为0,元素一定不存在</span>
<span class="code-keyword">return</span> <span class="code-builtin">false</span>, <span class="code-builtin">nil</span>
}
}
<span class="code-comment">// 所有位都为1,元素可能存在</span>
<span class="code-keyword">return</span> <span class="code-builtin">true</span>, <span class="code-builtin">nil</span>
}
<span class="code-comment">// hash 使用 FNV 哈希 + 种子模拟多个哈希函数</span>
<span class="code-keyword">func</span> (bf *<span class="code-type">BloomFilter</span>) <span class="code-func">hash</span>(value <span class="code-type">string</span>, seed <span class="code-type">uint</span>) <span class="code-type">uint</span> {
h := fnv.New64a()
h.Write([]<span class="code-type">byte</span>(value))
<span class="code-comment">// 加入种子产生不同的哈希值</span>
h.Write([]<span class="code-type">byte</span>{<span class="code-type">byte</span>(seed)})
<span class="code-keyword">return</span> <span class="code-type">uint</span>(h.Sum64()) % bf.bitSize
}
<span class="code-comment">// InitFromDB 从数据库加载所有合法ID到布隆过滤器</span>
<span class="code-keyword">func</span> (bf *<span class="code-type">BloomFilter</span>) <span class="code-func">InitFromDB</span>(ctx context.Context, loadAllIDs <span class="code-keyword">func</span>() ([]<span class="code-type">string</span>, <span class="code-type">error</span>)) <span class="code-type">error</span> {
ids, err := loadAllIDs()
<span class="code-keyword">if</span> err != <span class="code-builtin">nil</span> {
<span class="code-keyword">return</span> err
}
<span class="code-comment">// 批量添加到布隆过滤器</span>
batchSize := <span class="code-number">1000</span>
<span class="code-keyword">for</span> i := <span class="code-number">0</span>; i < <span class="code-builtin">len</span>(ids); i += batchSize {
end := i + batchSize
<span class="code-keyword">if</span> end > <span class="code-builtin">len</span>(ids) {
end = <span class="code-builtin">len</span>(ids)
}
<span class="code-keyword">if</span> err := bf.AddBatch(ctx, ids[i:end]); err != <span class="code-builtin">nil</span> {
<span class="code-keyword">return</span> err
}
}
<span class="code-keyword">return</span> <span class="code-builtin">nil</span>
}</code></pre>
</div>
<h3>💻 完整集成方案 — 三层防护</h3>
<div class="code-block">
<div class="code-header">
<div class="code-dots"><span></span><span></span><span></span></div>
<span>penetration_full.go — 穿透三层防护完整方案</span>
</div>
<pre><code><span class="code-keyword">package</span> cache
<span class="code-keyword">import</span> (
<span class="code-string">"context"</span>
<span class="code-string">"errors"</span>
<span class="code-string">"fmt"</span>
<span class="code-string">"time"</span>
)
<span class="code-keyword">var</span> (
ErrInvalidParam = errors.New(<span class="code-string">"invalid parameter"</span>)
ErrDataNotExist = errors.New(<span class="code-string">"data does not exist"</span>)
ErrServiceDegraded = errors.New(<span class="code-string">"service degraded"</span>)
)
<span class="code-comment">// PenetrationGuard 缓存穿透防护器(三层防护)</span>
<span class="code-keyword">type</span> <span class="code-type">PenetrationGuard</span> <span class="code-keyword">struct</span> {
bloomFilter *<span class="code-type">BloomFilter</span> <span class="code-comment">// 第三层:布隆过滤器</span>
nullCache *<span class="code-type">NullCache</span> <span class="code-comment">// 第二层:空值缓存</span>
}
<span class="code-comment">// Get 带三层防护的缓存查询</span>
<span class="code-keyword">func</span> (g *<span class="code-type">PenetrationGuard</span>) <span class="code-func">Get</span>(
ctx context.Context,
key <span class="code-type">string</span>,
loadFn <span class="code-keyword">func</span>(ctx context.Context) (<span class="code-type">interface</span>{}, <span class="code-type">error</span>),
) (<span class="code-type">interface</span>{}, <span class="code-type">error</span>) {
<span class="code-comment">// ========== 第一层:参数校验 ==========</span>
<span class="code-comment">// (在调用此方法之前已完成,此处省略)</span>
<span class="code-comment">// ========== 第二层:布隆过滤器检查 ==========</span>
exists, err := g.bloomFilter.Exists(ctx, key)
<span class="code-keyword">if</span> err != <span class="code-builtin">nil</span> {
<span class="code-comment">// 布隆过滤器查询失败,降级处理(放行,让后续层处理)</span>
<span class="code-comment">// 这里选择放行而不是拒绝,保证可用性</span>
}
<span class="code-keyword">if</span> !exists {
<span class="code-comment">// 布隆过滤器判定不存在 → 一定不存在 → 直接拒绝</span>
<span class="code-keyword">return</span> <span class="code-builtin">nil</span>, ErrDataNotExist
}
<span class="code-comment">// ========== 第三层:空值缓存 ==========</span>
<span class="code-comment">// 布隆过滤器说可能存在,再查缓存(含空值缓存)</span>
data, err := g.nullCache.Get(ctx, key, loadFn)
<span class="code-keyword">if</span> err != <span class="code-builtin">nil</span> {
<span class="code-keyword">if</span> errors.Is(err, ErrDataNotFound) {
<span class="code-comment">// 数据库也不存在</span>
<span class="code-keyword">return</span> <span class="code-builtin">nil</span>, ErrDataNotExist
}
<span class="code-keyword">return</span> <span class="code-builtin">nil</span>, err
}
<span class="code-keyword">return</span> data, <span class="code-builtin">nil</span>
}
<span class="code-comment">// ============ 完整使用示例 ============</span>
<span class="code-keyword">func</span> <span class="code-func">GetUserExample</span>() {
<span class="code-comment">// 初始化</span>
guard := &<span class="code-type">PenetrationGuard</span>{
bloomFilter: NewBloomFilter(rdb, <span class="code-string">"bloom:users"</span>, <span class="code-number">1000000</span>, <span class="code-number">0.01</span>),
nullCache: NewNullCache(rdb),
}
<span class="code-comment">// 启动时从数据库加载所有合法用户ID到布隆过滤器</span>
guard.bloomFilter.InitFromDB(ctx, <span class="code-keyword">func</span>() ([]<span class="code-type">string</span>, <span class="code-type">error</span>) {
<span class="code-keyword">return</span> db.GetAllUserIDs()
})
<span class="code-comment">// 处理请求</span>
userID := <span class="code-string">"-1"</span> <span class="code-comment">// 恶意请求</span>
<span class="code-comment">// 第一层:参数校验</span>
<span class="code-keyword">if</span> id, err := validateUserID(userID); err != <span class="code-builtin">nil</span> {
<span class="code-keyword">return</span> <span class="code-string">"参数不合法"</span> <span class="code-comment">// 直接拒绝,-1 不合法</span>
}
<span class="code-comment">// 第二层+第三层:布隆过滤器 + 空值缓存</span>
data, err := guard.Get(ctx, <span class="code-string">"user:999999"</span>, <span class="code-keyword">func</span>(ctx context.Context) (<span class="code-type">interface</span>{}, <span class="code-type">error</span>) {
<span class="code-keyword">return</span> db.GetUser(ctx, <span class="code-number">999999</span>)
})
<span class="code-keyword">if</span> errors.Is(err, ErrDataNotExist) {
<span class="code-keyword">return</span> <span class="code-string">"用户不存在"</span> <span class="code-comment">// 布隆过滤器或空值缓存拦截</span>
}
}</code></pre>
</div>
<h3>📊 三种方案综合对比</h3>
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>方案</th>
<th>防护能力</th>
<th>内存开销</th>
<th>实现复杂度</th>
<th>适用场景</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>参数校验</strong></td>
<td>低(只能拦截格式错误)</td>
<td>无</td>
<td>极低</td>
<td>所有场景(必做)</td>
</tr>
<tr>
<td><strong>缓存空对象</strong></td>
<td>中(防止重复查询)</td>
<td>中(空值占内存)</td>
<td>低</td>
<td>数据量不大,攻击ID有限</td>
</tr>
<tr>
<td><strong>布隆过滤器</strong></td>
<td>高(从根源拦截)</td>
<td>低(位数组很省内存)</td>
<td>中高</td>
<td>数据量大,防恶意攻击</td>
</tr>
</tbody>
</table>
</div>
<div class="alert alert-success">
<span class="alert-icon">✅</span>
<div>
<strong>最佳实践:三种方案组合使用!</strong>
<ol style="margin-top: 8px;">
<li><strong>第一层 — 参数校验:</strong>拦截明显不合法请求(零成本)</li>
<li><strong>第二层 — 布隆过滤器:</strong>拦截数据一定不存在的请求(低成本,高效率)</li>
<li><strong>第三层 — 缓存空对象:</strong>兜底保护,处理布隆过滤器误判的情况</li>
</ol>
</div>
</div>
</div>
<!-- 布隆过滤器维护 -->
<div class="section">
<h2 class="section-title"><span class="icon">🔧</span> 布隆过滤器的维护与注意事项</h2>
<div class="card-grid">
<div class="card card-warning">
<h4>⚠️ 不能删除元素</h4>
<p>标准布隆过滤器不支持删除操作。如果需要删除,可使用<strong>计数布隆过滤器(Counting Bloom Filter)</strong>,用计数器代替单个位。</p>
</div>
<div class="card card-warning">
<h4>⚠️ 误判率随元素增多而升高</h4>
<p>当插入元素超过预期数量时,误判率会显著上升。需要定期重建或动态扩容。</p>
</div>
<div class="card card-success">
<h4>✅ 定期重建策略</h4>
<p>每隔一定时间(如每天凌晨),从数据库重新加载所有合法 ID 到新的布隆过滤器,然后切换。</p>
</div>
<div class="card card-success">
<h4>✅ 增量更新</h4>
<p>新增数据时同步添加到布隆过滤器。删除数据时,由于不能直接删除,可以标记为"逻辑删除",定期重建时过滤掉。</p>
</div>
</div>
<div class="code-block">
<div class="code-header">
<div class="code-dots"><span></span><span></span><span></span></div>
<span>bloom_maintenance.go — 布隆过滤器维护</span>
</div>
<pre><code><span class="code-comment">// RebuildBloomFilter 定期重建布隆过滤器</span>
<span class="code-keyword">func</span> <span class="code-func">RebuildBloomFilter</span>(ctx context.Context, bf *<span class="code-type">BloomFilter</span>) <span class="code-type">error</span> {
<span class="code-comment">// 使用新key创建新的布隆过滤器</span>
newKey := bf.key + <span class="code-string">":new"</span>
newBF := NewBloomFilter(bf.rdb, newKey, bf.estimatedCapacity, bf.falsePositiveRate)
<span class="code-comment">// 从数据库加载最新数据</span>
err := newBF.InitFromDB(ctx, <span class="code-keyword">func</span>() ([]<span class="code-type">string</span>, <span class="code-type">error</span>) {
<span class="code-keyword">return</span> db.GetAllValidIDs(ctx) <span class="code-comment">// 只加载有效数据</span>
})
<span class="code-keyword">if</span> err != <span class="code-builtin">nil</span> {
<span class="code-keyword">return</span> err
}
<span class="code-comment">// 原子切换:rename 操作在 Redis 中是原子的</span>
<span class="code-comment">// 将旧key备份,新key替换为正式key</span>
pipe := bf.rdb.Pipeline()
pipe.Rename(ctx, bf.key, bf.key+<span class="code-string">":old"</span>)
pipe.Rename(ctx, newKey, bf.key)
_, err = pipe.Exec(ctx)
<span class="code-keyword">if</span> err != <span class="code-builtin">nil</span> {
<span class="code-keyword">return</span> err
}
<span class="code-comment">// 延迟删除旧key</span>
<span class="code-keyword">go</span> <span class="code-keyword">func</span>() {
time.Sleep(<span class="code-number">1</span> * time.Minute)
bf.rdb.Del(context.Background(), bf.key+<span class="code-string">":old"</span>)
}()
<span class="code-keyword">return</span> <span class="code-builtin">nil</span>
}
<span class="code-comment">// StartPeriodicRebuild 启动定时重建任务</span>
<span class="code-keyword">func</span> <span class="code-func">StartPeriodicRebuild</span>(ctx context.Context, bf *<span class="code-type">BloomFilter</span>, interval time.Duration) {
ticker := time.NewTicker(interval)
<span class="code-keyword">defer</span> ticker.Stop()
<span class="code-keyword">for</span> {
<span class="code-keyword">select</span> {
<span class="code-keyword">case</span> <-ticker.C:
<span class="code-keyword">if</span> err := RebuildBloomFilter(ctx, bf); err != <span class="code-builtin">nil</span> {
log.Printf(<span class="code-string">"failed to rebuild bloom filter: %v"</span>, err)
} <span class="code-keyword">else</span> {
log.Printf(<span class="code-string">"bloom filter rebuilt successfully"</span>)
}
<span class="code-keyword">case</span> <-ctx.Done():
<span class="code-keyword">return</span>
}
}
}</code></pre>
</div>
</div>
</div>
</div>
<script>
function switchTab(tabName) {
// 隐藏所有 tab content
document.querySelectorAll('.tab-content').forEach(content => {
content.classList.remove('active');
});
// 取消所有 tab button 的 active 状态
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.classList.remove('active');
});
// 显示选中的 tab content
document.getElementById('tab-' + tabName).classList.add('active');
// 设置选中的 tab button 为 active
event.currentTarget.classList.add('active');
}
</script>
</body>
</html>