c96c23a7c5
Deploy / deploy (push) Failing after 1m30s
- 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
2552 lines
122 KiB
HTML
2552 lines
122 KiB
HTML
<!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> |