feat: update docs and components, fix DLQ demo bug

This commit is contained in:
sanbuphy
2026-01-18 12:21:49 +08:00
parent 26ed39e1eb
commit e41063a1cd
159 changed files with 54236 additions and 2525 deletions
@@ -0,0 +1,339 @@
<!--
CacheArchitectureDemo.vue
缓存架构概览 - 展示缓存在系统中的位置和作用
-->
<template>
<div class="cache-architecture-demo">
<div class="header">
<div class="title">缓存架构概览</div>
<div class="subtitle">数据访问的"高速公路系统"</div>
</div>
<div class="architecture-flow">
<div class="flow-layer user">
<div class="layer-label">用户请求</div>
<div class="request-icon">👤</div>
</div>
<div class="arrow"></div>
<div
class="flow-layer cache"
:class="{ active: currentLayer === 'cache' }"
>
<div class="layer-label">缓存层 (Cache)</div>
<div class="cache-box">
<div class="cache-icon"></div>
<div class="cache-stats">
<div>命中率: {{ hitRate }}%</div>
<div>响应时间: ~1ms</div>
</div>
</div>
</div>
<div class="arrow">
<span v-if="showMiss" class="miss-text">未命中</span>
</div>
<div
class="flow-layer database"
:class="{ active: currentLayer === 'database' }"
>
<div class="layer-label">数据库层 (Database)</div>
<div class="database-box">
<div class="database-icon">🗄</div>
<div class="database-stats">
<div>响应时间: ~50ms</div>
<div>持久化存储</div>
</div>
</div>
</div>
</div>
<div class="comparison">
<div class="comparison-title">访问速度对比</div>
<div class="speed-bars">
<div class="speed-item">
<div class="label">缓存命中</div>
<div class="bar-container">
<div class="bar fast" :style="{ width: '5%' }"></div>
</div>
<div class="time">~1ms</div>
</div>
<div class="speed-item">
<div class="label">数据库查询</div>
<div class="bar-container">
<div class="bar slow" :style="{ width: '100%' }"></div>
</div>
<div class="time">~50ms</div>
</div>
</div>
<div class="conclusion">
缓存命中时响应速度提升 <strong>{{ speedup }}x</strong>
</div>
</div>
<div class="interactive-demo">
<button class="demo-btn" @click="simulateRequest">模拟请求</button>
<div class="demo-result" v-if="lastResult">
<span :class="{ hit: lastResult.hit, miss: !lastResult.hit }">
{{ lastResult.hit ? '✅ 缓存命中' : '❌ 缓存未命中,访问数据库' }}
</span>
<span class="response-time">{{ lastResult.time }}ms</span>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const currentLayer = ref('cache')
const showMiss = ref(false)
const lastResult = ref(null)
const hitRate = ref(75)
const speedup = computed(() => Math.round(50 / 1))
const simulateRequest = () => {
const hit = Math.random() * 100 < hitRate.value
lastResult.value = {
hit,
time: hit
? Math.floor(Math.random() * 3) + 1
: Math.floor(Math.random() * 20) + 40
}
currentLayer.value = 'cache'
showMiss.value = false
if (!hit) {
setTimeout(() => {
showMiss.value = true
currentLayer.value = 'database'
}, 300)
}
}
</script>
<style scoped>
.cache-architecture-demo {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
border-radius: 12px;
padding: 1.5rem;
margin: 1.5rem 0;
font-family: var(--vp-font-family-base);
}
.header {
margin-bottom: 1.5rem;
}
.title {
font-weight: 700;
font-size: 1.1rem;
margin-bottom: 0.25rem;
}
.subtitle {
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.architecture-flow {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
margin-bottom: 2rem;
}
.flow-layer {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
padding: 1rem;
border-radius: 12px;
transition: all 0.3s;
}
.flow-layer.active {
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-brand);
}
.layer-label {
font-size: 0.85rem;
font-weight: 600;
color: var(--vp-c-text-1);
}
.cache-box {
display: flex;
align-items: center;
gap: 1rem;
background: linear-gradient(135deg, #fef3c7, #fde68a);
padding: 1rem 1.5rem;
border-radius: 12px;
border: 2px solid #f59e0b;
}
.cache-icon {
font-size: 2rem;
}
.cache-stats {
font-size: 0.85rem;
line-height: 1.6;
}
.database-box {
display: flex;
align-items: center;
gap: 1rem;
background: linear-gradient(135deg, #dbeafe, #bfdbfe);
padding: 1rem 1.5rem;
border-radius: 12px;
border: 2px solid #3b82f6;
}
.database-icon {
font-size: 2rem;
}
.database-stats {
font-size: 0.85rem;
line-height: 1.6;
}
.arrow {
font-size: 1.5rem;
color: var(--vp-c-text-2);
position: relative;
}
.miss-text {
position: absolute;
right: -80px;
top: 50%;
transform: translateY(-50%);
font-size: 0.75rem;
color: #ef4444;
white-space: nowrap;
}
.comparison {
background: var(--vp-c-bg);
border-radius: 10px;
padding: 1.25rem;
margin-bottom: 1.5rem;
border: 1px solid var(--vp-c-divider);
}
.comparison-title {
font-weight: 600;
margin-bottom: 1rem;
font-size: 0.95rem;
}
.speed-bars {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.speed-item {
display: grid;
grid-template-columns: 100px 1fr 60px;
align-items: center;
gap: 1rem;
}
.label {
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
.bar-container {
height: 20px;
background: var(--vp-c-bg-soft);
border-radius: 999px;
overflow: hidden;
}
.bar {
height: 100%;
border-radius: 999px;
transition: width 0.5s;
}
.bar.fast {
background: linear-gradient(90deg, #22c55e, #16a34a);
}
.bar.slow {
background: linear-gradient(90deg, #f59e0b, #ef4444);
}
.time {
font-size: 0.85rem;
font-weight: 600;
color: var(--vp-c-text-1);
}
.conclusion {
margin-top: 1rem;
padding-top: 1rem;
border-top: 1px solid var(--vp-c-divider);
font-size: 0.9rem;
color: var(--vp-c-text-1);
text-align: center;
}
.interactive-demo {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
}
.demo-btn {
padding: 0.75rem 2rem;
background: var(--vp-c-brand);
color: white;
border: none;
border-radius: 8px;
font-size: 0.95rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.demo-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.demo-result {
display: flex;
align-items: center;
gap: 1rem;
font-size: 0.9rem;
}
.hit {
color: #22c55e;
font-weight: 600;
}
.miss {
color: #ef4444;
font-weight: 600;
}
.response-time {
color: var(--vp-c-text-2);
}
</style>
@@ -0,0 +1,621 @@
<!--
CacheLifecycleDemo.vue
缓存生命周期演示 - 展示缓存条目的写入命中过期淘汰过程
-->
<template>
<div class="cache-lifecycle-demo">
<div class="header">
<div class="title">缓存生命周期演示</div>
<div class="subtitle">观察缓存条目从创建到淘汰的完整过程</div>
</div>
<div class="cache-container">
<div class="cache-header">
<div class="cache-title">
缓存存储 (容量: {{ cacheSize }}/{{ maxCacheSize }})
</div>
<div class="cache-stats">
<span>命中率: {{ hitRate }}%</span>
<span>淘汰: {{ evictionCount }}</span>
</div>
</div>
<div class="cache-entries">
<div
v-for="entry in cacheEntries"
:key="entry.id"
class="cache-entry"
:class="{
hit: entry.status === 'hit',
expiring: entry.status === 'expiring',
evicting: entry.status === 'evicting',
new: entry.status === 'new'
}"
>
<div class="entry-header">
<div class="entry-id">{{ entry.key }}</div>
<div class="entry-status">
<span v-if="entry.status === 'new'" class="status-badge new"
>NEW</span
>
<span v-if="entry.status === 'hit'" class="status-badge hit"
>HIT</span
>
<span
v-if="entry.status === 'expiring'"
class="status-badge expiring"
>EXPIRING</span
>
<span
v-if="entry.status === 'evicting'"
class="status-badge evicting"
>EVICTING</span
>
</div>
</div>
<div class="entry-ttl">
<div class="ttl-bar">
<div
class="ttl-fill"
:style="{ width: entry.ttlPercent + '%' }"
></div>
</div>
<div class="ttl-text">TTL: {{ entry.ttl }}s</div>
</div>
<div class="entry-meta">
<span>命中: {{ entry.hits }}</span>
<span>访问: {{ entry.lastAccess }}s前</span>
</div>
</div>
</div>
</div>
<div class="controls">
<div class="control-group">
<label>操作</label>
<button class="action-btn read" @click="readData">读取数据</button>
<button class="action-btn write" @click="writeData">写入新数据</button>
</div>
<div class="control-group">
<label>自动模拟</label>
<button
class="action-btn auto"
:class="{ active: autoMode }"
@click="toggleAuto"
>
{{ autoMode ? '停止' : '开始' }}自动模拟
</button>
</div>
</div>
<div class="timeline">
<div class="timeline-title">事件时间线</div>
<div class="timeline-events">
<div
v-for="(event, index) in events"
:key="index"
class="event"
:class="event.type"
>
<div class="event-time">{{ event.time }}</div>
<div class="event-content">
<span class="event-icon">{{ event.icon }}</span>
<span class="event-text">{{ event.text }}</span>
</div>
</div>
</div>
</div>
<div class="legend">
<div class="legend-item">
<span class="legend-color new"></span>
<span>新写入</span>
</div>
<div class="legend-item">
<span class="legend-color hit"></span>
<span>缓存命中</span>
</div>
<div class="legend-item">
<span class="legend-color expiring"></span>
<span>即将过期</span>
</div>
<div class="legend-item">
<span class="legend-color evicting"></span>
<span>淘汰中</span>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onUnmounted } from 'vue'
const maxCacheSize = 6
const cacheEntries = ref([])
const events = ref([])
const autoMode = ref(false)
let autoInterval = null
let eventCounter = 0
const cacheSize = computed(() => cacheEntries.value.length)
const hitRate = computed(() => {
const hitEvents = events.value.filter((e) => e.type === 'hit').length
const totalEvents = events.value.filter(
(e) => e.type === 'hit' || e.type === 'miss'
).length
return totalEvents > 0 ? Math.round((hitEvents / totalEvents) * 100) : 0
})
const evictionCount = computed(
() => events.value.filter((e) => e.type === 'eviction').length
)
const addEvent = (type, icon, text) => {
const now = new Date()
events.value.unshift({
time: `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}:${now.getSeconds().toString().padStart(2, '0')}`,
type,
icon,
text
})
if (events.value.length > 10) {
events.value.pop()
}
}
const writeData = () => {
if (cacheEntries.value.length >= maxCacheSize) {
// LRU: Remove least recently used
const lruIndex = cacheEntries.value.reduce(
(minIdx, entry, idx, arr) =>
entry.lastAccess > arr[minIdx].lastAccess ? minIdx : idx,
0
)
const evicting = cacheEntries.value[lruIndex]
evicting.status = 'evicting'
addEvent('eviction', '🗑️', `淘汰 ${evicting.key} (LRU)`)
setTimeout(() => {
cacheEntries.value.splice(lruIndex, 1)
}, 500)
}
const newId = `key_${++eventCounter}`
const newEntry = {
key: newId,
status: 'new',
ttl: 30,
ttlPercent: 100,
hits: 0,
lastAccess: 0
}
cacheEntries.value.push(newEntry)
addEvent('write', '✨', `写入 ${newId}`)
setTimeout(() => {
newEntry.status = null
}, 500)
startTTLDecay(newEntry)
}
const readData = () => {
if (cacheEntries.value.length === 0) {
addEvent('miss', '❌', '缓存为空,未命中')
return
}
const randomIndex = Math.floor(Math.random() * cacheEntries.value.length)
const entry = cacheEntries.value[randomIndex]
entry.status = 'hit'
entry.hits++
entry.lastAccess = 0
entry.ttl = Math.min(entry.ttl + 5, 30) // Refresh TTL on hit
entry.ttlPercent = (entry.ttl / 30) * 100
addEvent('hit', '✅', `命中 ${entry.key} (第${entry.hits}次)`)
setTimeout(() => {
entry.status = null
}, 500)
}
const startTTLDecay = (entry) => {
const interval = setInterval(() => {
if (!cacheEntries.value.includes(entry)) {
clearInterval(interval)
return
}
entry.lastAccess++
entry.ttl--
entry.ttlPercent = (entry.ttl / 30) * 100
if (entry.ttl <= 10) {
entry.status = 'expiring'
}
if (entry.ttl <= 0) {
addEvent('expiration', '⏰', `${entry.key} 过期`)
const idx = cacheEntries.value.indexOf(entry)
if (idx !== -1) {
cacheEntries.value.splice(idx, 1)
}
clearInterval(interval)
}
}, 1000)
}
const toggleAuto = () => {
autoMode.value = !autoMode.value
if (autoMode.value) {
autoInterval = setInterval(() => {
const action = Math.random()
if (action < 0.4 || cacheEntries.value.length === 0) {
writeData()
} else {
readData()
}
}, 1500)
} else {
if (autoInterval) {
clearInterval(autoInterval)
autoInterval = null
}
}
}
onUnmounted(() => {
if (autoInterval) {
clearInterval(autoInterval)
}
})
</script>
<style scoped>
.cache-lifecycle-demo {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
border-radius: 12px;
padding: 1.5rem;
margin: 1.5rem 0;
font-family: var(--vp-font-family-base);
}
.header {
margin-bottom: 1.5rem;
}
.title {
font-weight: 700;
font-size: 1.05rem;
}
.subtitle {
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.cache-container {
background: var(--vp-c-bg);
border-radius: 10px;
padding: 1.25rem;
margin-bottom: 1.5rem;
border: 1px solid var(--vp-c-divider);
}
.cache-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.cache-title {
font-weight: 600;
font-size: 0.95rem;
}
.cache-stats {
display: flex;
gap: 1rem;
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
.cache-entries {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 1rem;
min-height: 150px;
}
.cache-entry {
background: var(--vp-c-bg-soft);
border: 2px solid var(--vp-c-divider);
border-radius: 8px;
padding: 0.75rem;
transition: all 0.3s;
}
.cache-entry.new {
border-color: #22c55e;
background: #f0fdf4;
animation: slideIn 0.3s;
}
.cache-entry.hit {
border-color: #3b82f6;
background: #eff6ff;
}
.cache-entry.expiring {
border-color: #f59e0b;
background: #fef3c7;
}
.cache-entry.evicting {
border-color: #ef4444;
background: #fef2f2;
animation: shake 0.5s;
}
.entry-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
}
.entry-id {
font-weight: 600;
font-size: 0.9rem;
}
.status-badge {
padding: 0.15rem 0.4rem;
border-radius: 4px;
font-size: 0.7rem;
font-weight: 600;
}
.status-badge.new {
background: #22c55e;
color: white;
}
.status-badge.hit {
background: #3b82f6;
color: white;
}
.status-badge.expiring {
background: #f59e0b;
color: white;
}
.status-badge.evicting {
background: #ef4444;
color: white;
}
.entry-ttl {
margin-bottom: 0.5rem;
}
.ttl-bar {
height: 6px;
background: var(--vp-c-bg);
border-radius: 999px;
overflow: hidden;
margin-bottom: 0.25rem;
}
.ttl-fill {
height: 100%;
background: linear-gradient(90deg, #22c55e, #f59e0b, #ef4444);
transition: width 1s linear;
}
.ttl-text {
font-size: 0.75rem;
color: var(--vp-c-text-2);
}
.entry-meta {
display: flex;
justify-content: space-between;
font-size: 0.75rem;
color: var(--vp-c-text-2);
}
.controls {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
margin-bottom: 1.5rem;
}
.control-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.control-group label {
font-size: 0.85rem;
font-weight: 600;
color: var(--vp-c-text-2);
}
.action-btn {
padding: 0.75rem 1rem;
border: none;
border-radius: 8px;
cursor: pointer;
font-weight: 600;
transition: all 0.2s;
}
.action-btn.read {
background: #3b82f6;
color: white;
}
.action-btn.write {
background: #22c55e;
color: white;
}
.action-btn.auto {
background: var(--vp-c-bg);
color: var(--vp-c-text-1);
border: 1px solid var(--vp-c-divider);
}
.action-btn.auto.active {
background: #ef4444;
color: white;
border-color: #ef4444;
}
.action-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.timeline {
background: var(--vp-c-bg);
border-radius: 10px;
padding: 1.25rem;
margin-bottom: 1rem;
border: 1px solid var(--vp-c-divider);
}
.timeline-title {
font-weight: 600;
margin-bottom: 1rem;
font-size: 0.95rem;
}
.timeline-events {
display: flex;
flex-direction: column;
gap: 0.5rem;
max-height: 200px;
overflow-y: auto;
}
.event {
display: grid;
grid-template-columns: 70px 1fr;
gap: 0.75rem;
padding: 0.5rem;
border-radius: 6px;
font-size: 0.85rem;
}
.event.hit {
background: #eff6ff;
}
.event.miss {
background: #fef2f2;
}
.event.write {
background: #f0fdf4;
}
.event.eviction {
background: #fef2f2;
}
.event-time {
color: var(--vp-c-text-2);
font-size: 0.75rem;
}
.event-content {
display: flex;
align-items: center;
gap: 0.5rem;
}
.event-icon {
font-size: 1rem;
}
.legend {
display: flex;
flex-wrap: wrap;
gap: 1rem;
padding: 1rem;
background: var(--vp-c-bg);
border-radius: 8px;
border: 1px solid var(--vp-c-divider);
}
.legend-item {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.85rem;
}
.legend-color {
width: 16px;
height: 16px;
border-radius: 4px;
border: 2px solid;
}
.legend-color.new {
border-color: #22c55e;
background: #f0fdf4;
}
.legend-color.hit {
border-color: #3b82f6;
background: #eff6ff;
}
.legend-color.expiring {
border-color: #f59e0b;
background: #fef3c7;
}
.legend-color.evicting {
border-color: #ef4444;
background: #fef2f2;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes shake {
0%,
100% {
transform: translateX(0);
}
25% {
transform: translateX(-5px);
}
75% {
transform: translateX(5px);
}
}
</style>
@@ -0,0 +1,808 @@
<!--
CachePatternsDemo.vue
缓存模式演示 - Cache-Aside, Read-Through, Write-Behind
-->
<template>
<div class="cache-patterns-demo">
<div class="header">
<div class="title">缓存模式 (Caching Patterns)</div>
<div class="subtitle">理解不同缓存读写模式的工作原理</div>
</div>
<div class="pattern-selector">
<button
v-for="pattern in patterns"
:key="pattern.id"
class="pattern-btn"
:class="{ active: activePattern === pattern.id }"
@click="activePattern = pattern.id"
>
{{ pattern.name }}
</button>
</div>
<div class="pattern-content">
<!-- Cache-Aside -->
<div v-if="activePattern === 'cache-aside'" class="pattern-detail">
<div class="description">
<div class="pattern-title">Cache-Aside (旁路缓存)</div>
<div class="pattern-subtitle">最常用的模式由应用代码控制缓存</div>
<div class="pattern-points">
<div class="point">
<span class="icon">📖</span>
<div>
<strong>读取</strong>先查缓存没命中再查数据库然后写入缓存
</div>
</div>
<div class="point">
<span class="icon"></span>
<div>
<strong>更新</strong
>先更新数据库然后<strong>删除</strong>缓存不是更新
</div>
</div>
</div>
</div>
<div class="diagram">
<div class="diagram-title">读取流程</div>
<div class="flow-chart">
<div class="flow-step" :class="{ active: flowStep >= 1 }">
<div class="step-number">1</div>
<div class="step-text">查询缓存</div>
</div>
<div class="flow-arrow"></div>
<div class="flow-decision">
<div class="decision-label">命中?</div>
<div class="decision-branches">
<div
class="branch yes"
:class="{ active: flowStep >= 2 && cacheHit }"
>
<div class="branch-label"></div>
<div class="branch-result"> 返回数据</div>
</div>
<div
class="branch no"
:class="{ active: flowStep >= 2 && !cacheHit }"
>
<div class="branch-label"></div>
<div class="branch-steps">
<div class="flow-step" :class="{ active: flowStep >= 3 }">
<div class="step-number">2</div>
<div class="step-text">查询数据库</div>
</div>
<div class="flow-arrow"></div>
<div class="flow-step" :class="{ active: flowStep >= 4 }">
<div class="step-number">3</div>
<div class="step-text">写入缓存</div>
</div>
<div class="flow-arrow"></div>
<div class="flow-step" :class="{ active: flowStep >= 5 }">
<div class="step-number">4</div>
<div class="step-text">返回数据</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="demo-controls">
<button
class="demo-btn"
@click="simulateCacheAside"
:disabled="simulating"
>
{{ simulating ? '模拟中...' : '模拟读取' }}
</button>
<label class="checkbox">
<input type="checkbox" v-model="cacheHit" />
缓存命中
</label>
</div>
</div>
<div class="code-example">
<div class="code-title">代码示例</div>
<pre class="code-block"><code>// Cache-Aside 模式
def get_user(user_id):
# 1. 查缓存
user = cache.get(f'user:{user_id}')
if user:
return user # 命中直接返回
# 2. 查数据库
user = db.query(f'SELECT * FROM users WHERE id = {user_id}')
# 3. 写入缓存
cache.set(f'user:{user_id}', user, ttl=600)
return user
def update_user(user_id, data):
# 1. 更新数据库
db.update('users', data)
# 2. 删除缓存不是更新
cache.delete(f'user:{user_id}')</code></pre>
</div>
</div>
<!-- Read-Through -->
<div v-if="activePattern === 'read-through'" class="pattern-detail">
<div class="description">
<div class="pattern-title">Read-Through / Write-Through</div>
<div class="pattern-subtitle">
由缓存库负责与数据库交互应用只和缓存打交道
</div>
<div class="pattern-points">
<div class="point">
<span class="icon">📖</span>
<div>
<strong>Read-Through</strong>缓存库自动从数据库加载数据
</div>
</div>
<div class="point">
<span class="icon"></span>
<div>
<strong>Write-Through</strong>写入缓存时同步写入数据库
</div>
</div>
</div>
</div>
<div class="diagram">
<div class="diagram-title">架构对比</div>
<div class="architecture-comparison">
<div class="arch-block">
<div class="arch-title">Cache-Aside</div>
<div class="arch-flow">
<div class="flow-box app">应用</div>
<div class="flow-arrows">
<div> 缓存</div>
<div> 数据库</div>
</div>
</div>
</div>
<div class="arch-block">
<div class="arch-title">Read-Through</div>
<div class="arch-flow">
<div class="flow-box app">应用</div>
<div class="flow-arrows">
<div> 缓存库</div>
</div>
<div class="flow-box cache">缓存库 数据库</div>
</div>
</div>
</div>
</div>
<div class="code-example">
<div class="code-title">代码示例</div>
<pre class="code-block"><code>// Read-Through 模式(代码更简洁)
def get_user(user_id):
# 缓存库自动处理数据库查询
user = cache.get_or_load(user_id, lambda: db.get_user(user_id))
return user
// Write-Through 模式
def update_user(user_id, data):
# 缓存库自动同步到数据库
cache.set(user_id, data) # 自动写入数据库</code></pre>
</div>
</div>
<!-- Write-Behind -->
<div v-if="activePattern === 'write-behind'" class="pattern-detail">
<div class="description">
<div class="pattern-title">Write-Behind (异步写回)</div>
<div class="pattern-subtitle">写入时只写缓存异步批量写数据库</div>
<div class="pattern-points">
<div class="point">
<span class="icon"></span>
<div><strong>优点</strong>写入极快适合写多的场景</div>
</div>
<div class="point">
<span class="icon"></span>
<div>
<strong>缺点</strong>数据可能丢失缓存崩了数据就没了
</div>
</div>
<div class="point">
<span class="icon">🎯</span>
<div>
<strong>适用</strong
>秒杀系统点赞数浏览量可接受少量丢失
</div>
</div>
</div>
</div>
<div class="diagram">
<div class="diagram-title">写入流程</div>
<div class="flow-chart">
<div class="flow-step">
<div class="step-number">1</div>
<div class="step-text">写入缓存</div>
<div class="step-time"> ~1ms</div>
</div>
<div class="flow-arrow"></div>
<div class="flow-step">
<div class="step-number">2</div>
<div class="step-text">立即返回</div>
</div>
<div class="flow-arrow"></div>
<div class="flow-step pending">
<div class="step-number">3</div>
<div class="step-text">异步批量写数据库</div>
<div class="step-time">🕐 后台执行</div>
</div>
</div>
<div class="demo-controls">
<button class="demo-btn" @click="simulateWriteBehind">
模拟批量写入
</button>
</div>
<div class="write-queue" v-if="writeQueue.length > 0">
<div class="queue-title">待写入队列</div>
<div class="queue-items">
<div
v-for="(item, index) in writeQueue"
:key="index"
class="queue-item"
:class="{ writing: item.writing, written: item.written }"
>
<span class="item-key">{{ item.key }}</span>
<span class="item-status">{{ item.status }}</span>
</div>
</div>
</div>
</div>
<div class="code-example">
<div class="code-title">代码示例</div>
<pre class="code-block"><code>// Write-Behind 模式
def update_counter(post_id):
# 1. 立即更新缓存极快
cache.incr(f'views:{post_id}')
# 立即返回不等待数据库
# 2. 后台异步批量写入数据库
async def flush_to_db():
while True:
await asyncio.sleep(5) # 每5秒批量写入
batch = cache.get_many('views:*')
db.batch_update(batch)
asyncio.create_task(flush_to_db())</code></pre>
</div>
</div>
</div>
<div class="pattern-comparison">
<div class="comparison-title">模式对比</div>
<table class="comparison-table">
<thead>
<tr>
<th>模式</th>
<th>复杂度</th>
<th>性能</th>
<th>一致性</th>
<th>适用场景</th>
</tr>
</thead>
<tbody>
<tr :class="{ highlight: activePattern === 'cache-aside' }">
<td>Cache-Aside</td>
<td></td>
<td></td>
<td></td>
<td>大多数场景</td>
</tr>
<tr :class="{ highlight: activePattern === 'read-through' }">
<td>Read-Through</td>
<td></td>
<td></td>
<td></td>
<td>简单场景</td>
</tr>
<tr :class="{ highlight: activePattern === 'write-behind' }">
<td>Write-Behind</td>
<td></td>
<td>极高</td>
<td></td>
<td>写多可丢失</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const activePattern = ref('cache-aside')
const flowStep = ref(0)
const cacheHit = ref(false)
const simulating = ref(false)
const writeQueue = ref([])
const patterns = [
{ id: 'cache-aside', name: 'Cache-Aside' },
{ id: 'read-through', name: 'Read-Through' },
{ id: 'write-behind', name: 'Write-Behind' }
]
const simulateCacheAside = async () => {
simulating.value = true
flowStep.value = 0
const steps = cacheHit.value ? [1, 2] : [1, 2, 3, 4, 5]
for (let i = 0; i < steps.length; i++) {
await new Promise((resolve) => setTimeout(resolve, 600))
flowStep.value = steps[i]
}
setTimeout(() => {
flowStep.value = 0
simulating.value = false
}, 1000)
}
const simulateWriteBehind = async () => {
writeQueue.value = [
{
key: 'views:post:1',
value: 100,
status: '待写入',
writing: false,
written: false
},
{
key: 'views:post:2',
value: 200,
status: '待写入',
writing: false,
written: false
},
{
key: 'views:post:3',
value: 150,
status: '待写入',
writing: false,
written: false
}
]
for (let i = 0; i < writeQueue.value.length; i++) {
await new Promise((resolve) => setTimeout(resolve, 800))
writeQueue.value[i].writing = true
writeQueue.value[i].status = '写入中...'
await new Promise((resolve) => setTimeout(resolve, 700))
writeQueue.value[i].writing = false
writeQueue.value[i].written = true
writeQueue.value[i].status = '✅ 已写入'
}
}
</script>
<style scoped>
.cache-patterns-demo {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
border-radius: 12px;
padding: 1.5rem;
margin: 1.5rem 0;
font-family: var(--vp-font-family-base);
}
.header {
margin-bottom: 1.5rem;
}
.title {
font-weight: 700;
font-size: 1.1rem;
}
.subtitle {
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.pattern-selector {
display: flex;
gap: 0.5rem;
margin-bottom: 2rem;
flex-wrap: wrap;
}
.pattern-btn {
padding: 0.75rem 1.5rem;
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 8px;
cursor: pointer;
font-weight: 600;
transition: all 0.2s;
}
.pattern-btn.active {
background: var(--vp-c-brand);
color: white;
border-color: var(--vp-c-brand);
}
.pattern-content {
min-height: 400px;
}
.pattern-detail {
display: flex;
flex-direction: column;
gap: 2rem;
}
.description {
background: var(--vp-c-bg);
padding: 1.5rem;
border-radius: 10px;
border: 1px solid var(--vp-c-divider);
}
.pattern-title {
font-size: 1.2rem;
font-weight: 700;
margin-bottom: 0.5rem;
}
.pattern-subtitle {
color: var(--vp-c-text-2);
margin-bottom: 1rem;
}
.pattern-points {
display: flex;
flex-direction: column;
gap: 1rem;
}
.point {
display: flex;
gap: 1rem;
align-items: flex-start;
}
.icon {
font-size: 1.5rem;
flex-shrink: 0;
}
.diagram {
background: var(--vp-c-bg);
padding: 1.5rem;
border-radius: 10px;
border: 1px solid var(--vp-c-divider);
}
.diagram-title {
font-weight: 600;
margin-bottom: 1rem;
font-size: 0.95rem;
}
.flow-chart {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
margin-bottom: 1.5rem;
}
.flow-step {
display: flex;
align-items: center;
gap: 1rem;
padding: 0.75rem 1.5rem;
background: var(--vp-c-bg-soft);
border-radius: 8px;
border: 2px solid var(--vp-c-divider);
transition: all 0.3s;
}
.flow-step.active {
border-color: var(--vp-c-brand);
background: #eff6ff;
}
.flow-step.pending {
border-color: #f59e0b;
background: #fef3c7;
}
.step-number {
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
background: var(--vp-c-brand);
color: white;
border-radius: 50%;
font-weight: 700;
font-size: 0.85rem;
}
.step-text {
font-weight: 600;
font-size: 0.9rem;
}
.step-time {
font-size: 0.8rem;
color: var(--vp-c-text-2);
}
.flow-arrow {
font-size: 1.5rem;
color: var(--vp-c-text-2);
}
.flow-decision {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
}
.decision-label {
font-weight: 600;
padding: 0.5rem 1rem;
background: #fef3c7;
border-radius: 6px;
border: 1px solid #f59e0b;
}
.decision-branches {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
.branch {
padding: 1rem;
border-radius: 8px;
border: 2px solid var(--vp-c-divider);
transition: all 0.3s;
}
.branch.active {
border-color: var(--vp-c-brand);
background: #eff6ff;
}
.branch-label {
font-weight: 600;
margin-bottom: 0.5rem;
text-align: center;
}
.branch-result {
text-align: center;
padding: 0.5rem;
background: #f0fdf4;
border-radius: 6px;
color: #166534;
font-weight: 600;
}
.branch-steps {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
}
.demo-controls {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 1rem;
}
.demo-btn {
padding: 0.75rem 1.5rem;
background: var(--vp-c-brand);
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-weight: 600;
transition: all 0.2s;
}
.demo-btn:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.demo-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.checkbox {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.9rem;
cursor: pointer;
}
.architecture-comparison {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2rem;
}
@media (max-width: 640px) {
.architecture-comparison {
grid-template-columns: 1fr;
}
}
.arch-block {
padding: 1rem;
background: var(--vp-c-bg-soft);
border-radius: 8px;
}
.arch-title {
font-weight: 600;
margin-bottom: 1rem;
text-align: center;
}
.arch-flow {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.75rem;
}
.flow-box {
padding: 0.75rem 1.5rem;
background: white;
border-radius: 8px;
border: 2px solid var(--vp-c-divider);
font-weight: 600;
}
.flow-box.cache {
font-size: 0.85rem;
}
.flow-arrows {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
.write-queue {
margin-top: 1rem;
}
.queue-title {
font-weight: 600;
margin-bottom: 0.75rem;
font-size: 0.9rem;
}
.queue-items {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.queue-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem;
background: var(--vp-c-bg-soft);
border-radius: 6px;
border: 1px solid var(--vp-c-divider);
transition: all 0.3s;
}
.queue-item.writing {
border-color: #f59e0b;
background: #fef3c7;
}
.queue-item.written {
border-color: #22c55e;
background: #f0fdf4;
}
.item-key {
font-weight: 600;
font-size: 0.85rem;
}
.item-status {
font-size: 0.8rem;
}
.code-example {
background: var(--vp-c-bg);
padding: 1.5rem;
border-radius: 10px;
border: 1px solid var(--vp-c-divider);
}
.code-title {
font-weight: 600;
margin-bottom: 1rem;
font-size: 0.95rem;
}
.code-block {
background: #1e293b;
color: #e2e8f0;
padding: 1rem;
border-radius: 8px;
overflow-x: auto;
font-size: 0.85rem;
line-height: 1.6;
}
.pattern-comparison {
background: var(--vp-c-bg);
padding: 1.5rem;
border-radius: 10px;
border: 1px solid var(--vp-c-divider);
}
.comparison-title {
font-weight: 600;
margin-bottom: 1rem;
font-size: 0.95rem;
}
.comparison-table {
width: 100%;
border-collapse: collapse;
}
.comparison-table th,
.comparison-table td {
padding: 0.75rem;
text-align: center;
border: 1px solid var(--vp-c-divider);
}
.comparison-table th {
background: var(--vp-c-bg-soft);
font-weight: 600;
font-size: 0.85rem;
}
.comparison-table td {
font-size: 0.85rem;
}
.comparison-table tr.highlight {
background: #eff6ff;
border-left: 3px solid var(--vp-c-brand);
}
</style>
@@ -0,0 +1,999 @@
<!--
CacheProblemsDemo.vue
缓存三大问题演示 - 缓存穿透缓存击穿缓存雪崩
-->
<template>
<div class="cache-problems-demo">
<div class="header">
<div class="title">缓存的三大问题</div>
<div class="subtitle">穿透击穿雪崩的场景与解决方案</div>
</div>
<div class="problem-selector">
<button
v-for="problem in problems"
:key="problem.id"
class="problem-btn"
:class="{ active: activeProblem === problem.id }"
@click="activeProblem = problem.id"
>
<span class="problem-icon">{{ problem.icon }}</span>
<span class="problem-name">{{ problem.name }}</span>
</button>
</div>
<div class="problem-content">
<!-- 缓存穿透 -->
<div v-if="activeProblem === 'penetration'" class="problem-detail">
<div class="problem-intro">
<div class="intro-title">什么是缓存穿透</div>
<div class="intro-text">
查询一个<strong>不存在的数据</strong>如恶意请求
id=-1缓存没有数据库也没有 导致每次请求都直接打到数据库
</div>
</div>
<div class="problem-scenario">
<div class="scenario-title">场景模拟</div>
<div class="scenario-diagram">
<div class="flow-item request">
<div class="flow-icon">🔥</div>
<div class="flow-text">请求 id=-999</div>
</div>
<div class="flow-arrow"></div>
<div class="flow-item cache" :class="{ miss: true }">
<div class="flow-icon"></div>
<div class="flow-text">缓存未命中</div>
</div>
<div class="flow-arrow"></div>
<div
class="flow-item database"
:class="{ overloaded: dbPressure >= 80 }"
>
<div class="flow-icon">🗄</div>
<div class="flow-text">数据库查询不存在</div>
</div>
</div>
<div class="controls">
<button
class="attack-btn"
@click="simulatePenetration"
:disabled="simulating"
>
{{ simulating ? '攻击中...' : '模拟恶意攻击' }}
</button>
</div>
<div class="pressure-meter">
<div class="meter-label">数据库压力</div>
<div class="meter-bar">
<div
class="meter-fill"
:style="{ width: dbPressure + '%' }"
></div>
</div>
<div class="meter-value">{{ dbPressure }}%</div>
</div>
</div>
<div class="solutions">
<div class="solutions-title">解决方案</div>
<div class="solution-list">
<div class="solution-item">
<div class="solution-header">
<span class="solution-number">1</span>
<span class="solution-name">布隆过滤器 (Bloom Filter)</span>
</div>
<div class="solution-desc">
在缓存前加一层过滤器快速判断"这个 id 肯定不存在"
<br />
<span class="note">100% 判断不存在但可能有误判</span>
</div>
</div>
<div class="solution-item">
<div class="solution-header">
<span class="solution-number">2</span>
<span class="solution-name">缓存空对象</span>
</div>
<div class="solution-desc">
查询不存在时缓存一个 NULL TTL 设置短一点 5 分钟
</div>
</div>
</div>
</div>
</div>
<!-- 缓存击穿 -->
<div v-if="activeProblem === 'breakdown'" class="problem-detail">
<div class="problem-intro">
<div class="intro-title">什么是缓存击穿</div>
<div class="intro-text">
某个<strong>热点数据</strong>过期如微博热搜瞬间几百万请求同时打到数据库
</div>
</div>
<div class="problem-scenario">
<div class="scenario-title">场景模拟</div>
<div class="hotkey-scenario">
<div class="hotkey-badge">
🔥 热点数据
<br />
<span class="key">user:12345</span>
</div>
<div class="concurrent-requests">
<div class="requests-title">并发请求</div>
<div class="requests-container">
<div
v-for="(req, index) in concurrentRequests"
:key="index"
class="request-item"
:class="req.status"
>
<div class="request-id">请求 {{ req.id }}</div>
<div class="request-status">{{ req.statusText }}</div>
</div>
</div>
</div>
<div class="mutex-visual" v-if="showMutex">
<div class="mutex-badge">🔒 互斥锁</div>
<div class="mutex-text">只有一个线程能查数据库</div>
</div>
</div>
<div class="controls">
<button
class="attack-btn"
@click="simulateBreakdown"
:disabled="simulating"
>
{{ simulating ? '模拟中...' : '模拟热点过期' }}
</button>
</div>
</div>
<div class="solutions">
<div class="solutions-title">解决方案</div>
<div class="solution-list">
<div class="solution-item">
<div class="solution-header">
<span class="solution-number">1</span>
<span class="solution-name">互斥锁 (Mutex Lock)</span>
</div>
<div class="solution-desc">
只允许一个线程查数据库其他线程等待
<br />
<span class="note">优点简单缺点阻塞其他请求</span>
</div>
</div>
<div class="solution-item">
<div class="solution-header">
<span class="solution-number">2</span>
<span class="solution-name">逻辑过期 (Logical Expiration)</span>
</div>
<div class="solution-desc">
不设置 TTL而是在 value 里存一个过期时间字段
<br />
<span class="note"
>查询时发现"逻辑过期"异步更新缓存同时返回旧数据</span
>
</div>
</div>
</div>
</div>
</div>
<!-- 缓存雪崩 -->
<div v-if="activeProblem === 'avalanche'" class="problem-detail">
<div class="problem-intro">
<div class="intro-title">什么是缓存雪崩</div>
<div class="intro-text">
大量缓存<strong>同时过期</strong>如系统重启后所有缓存都在
00:00:00 过期 数据库瞬间被打爆
</div>
</div>
<div class="problem-scenario">
<div class="scenario-title">场景模拟</div>
<div class="avalanche-visual">
<div class="cache-items">
<div
v-for="(item, index) in cacheItems"
:key="index"
class="cache-item"
:class="{ expired: item.expired }"
>
<div class="item-key">{{ item.key }}</div>
<div class="item-ttl">TTL: {{ item.ttl }}s</div>
</div>
</div>
<div class="mass-explosion" v-if="massExplosion">
<div class="explosion-icon">💥</div>
<div class="explosion-text">同时过期</div>
</div>
<div class="db-overload" :class="{ critical: dbPressure >= 90 }">
<div class="db-icon">🗄</div>
<div class="db-status">数据库负载: {{ dbPressure }}%</div>
</div>
</div>
<div class="controls">
<button
class="attack-btn"
@click="simulateAvalanche"
:disabled="simulating"
>
{{ simulating ? '模拟中...' : '模拟缓存雪崩' }}
</button>
<button class="solution-btn" @click="applyRandomTTL">
应用解决方案随机 TTL
</button>
</div>
</div>
<div class="solutions">
<div class="solutions-title">解决方案</div>
<div class="solution-list">
<div class="solution-item">
<div class="solution-header">
<span class="solution-number">1</span>
<span class="solution-name">随机 TTL</span>
</div>
<div class="solution-desc">
避免同时过期TTL 加上随机值
<br />
<span class="code"
>ttl = 600 + random.randint(-60, 60) # 600 ± 60 </span
>
</div>
</div>
<div class="solution-item">
<div class="solution-header">
<span class="solution-number">2</span>
<span class="solution-name">缓存预热</span>
</div>
<div class="solution-desc">
系统启动时主动加载热点数据到缓存
<br />
<span class="note"
>使用定时任务提前刷新即将过期的热点数据</span
>
</div>
</div>
<div class="solution-item">
<div class="solution-header">
<span class="solution-number">3</span>
<span class="solution-name">熔断降级</span>
</div>
<div class="solution-desc">
当数据库压力过大时暂时停止更新缓存直接返回降级数据
<br />
<span class="note">"系统繁忙,请稍后再试"</span>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="comparison-table">
<div class="table-title">三大问题对比</div>
<table class="problems-table">
<thead>
<tr>
<th>问题</th>
<th>原因</th>
<th>影响</th>
<th>主要解决方案</th>
</tr>
</thead>
<tbody>
<tr :class="{ active: activeProblem === 'penetration' }">
<td>缓存穿透</td>
<td>查询不存在的数据</td>
<td>数据库压力增加</td>
<td>布隆过滤器缓存空对象</td>
</tr>
<tr :class="{ active: activeProblem === 'breakdown' }">
<td>缓存击穿</td>
<td>热点数据过期</td>
<td>数据库瞬间压力</td>
<td>互斥锁逻辑过期</td>
</tr>
<tr :class="{ active: activeProblem === 'avalanche' }">
<td>缓存雪崩</td>
<td>大量缓存同时过期</td>
<td>数据库被打爆</td>
<td>随机 TTL缓存预热</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const activeProblem = ref('penetration')
const simulating = ref(false)
const dbPressure = ref(0)
const concurrentRequests = ref([])
const showMutex = ref(false)
const cacheItems = ref([])
const massExplosion = ref(false)
const problems = [
{ id: 'penetration', name: '缓存穿透', icon: '🕳️' },
{ id: 'breakdown', name: '缓存击穿', icon: '🔥' },
{ id: 'avalanche', name: '缓存雪崩', icon: '❄️' }
]
const initializeCacheItems = () => {
cacheItems.value = Array.from({ length: 8 }, (_, i) => ({
key: `key:${i + 1}`,
ttl: 10,
expired: false
}))
}
const simulatePenetration = async () => {
simulating.value = true
dbPressure.value = 0
for (let i = 0; i < 20; i++) {
await new Promise((resolve) => setTimeout(resolve, 100))
dbPressure.value = Math.min(100, dbPressure.value + 5)
}
setTimeout(() => {
simulating.value = false
dbPressure.value = 0
}, 2000)
}
const simulateBreakdown = async () => {
simulating.value = true
concurrentRequests.value = Array.from({ length: 10 }, (_, i) => ({
id: i + 1,
status: 'waiting',
statusText: '等待中'
}))
showMutex.value = true
// First request gets the lock
await new Promise((resolve) => setTimeout(resolve, 300))
concurrentRequests.value[0].status = 'processing'
concurrentRequests.value[0].statusText = '查询数据库...'
await new Promise((resolve) => setTimeout(resolve, 1000))
concurrentRequests.value[0].status = 'done'
concurrentRequests.value[0].statusText = '✅ 完成'
// Other requests wait and get from cache
for (let i = 1; i < concurrentRequests.value.length; i++) {
await new Promise((resolve) => setTimeout(resolve, 200))
concurrentRequests.value[i].status = 'done'
concurrentRequests.value[i].statusText = '✅ 从缓存获取'
}
showMutex.value = false
setTimeout(() => {
simulating.value = false
}, 1500)
}
const simulateAvalanche = async () => {
simulating.value = true
dbPressure.value = 0
massExplosion.value = false
initializeCacheItems()
// Countdown to expiration
for (let i = 10; i > 0; i--) {
await new Promise((resolve) => setTimeout(resolve, 200))
cacheItems.value.forEach((item) => {
item.ttl = i
})
}
// Mass expiration
massExplosion.value = true
cacheItems.value.forEach((item) => {
item.expired = true
})
// Database pressure spike
for (let i = 0; i < 20; i++) {
await new Promise((resolve) => setTimeout(resolve, 100))
dbPressure.value = Math.min(100, dbPressure.value + 5)
}
setTimeout(() => {
massExplosion.value = false
simulating.value = false
}, 2000)
}
const applyRandomTTL = async () => {
simulating.value = true
dbPressure.value = 0
massExplosion.value = false
initializeCacheItems()
// Apply random TTL
cacheItems.value.forEach((item) => {
item.ttl = 10 + Math.floor(Math.random() * 10) - 5
})
// Gradual expiration
const maxTTL = Math.max(...cacheItems.value.map((item) => item.ttl))
for (let t = maxTTL; t > 0; t--) {
await new Promise((resolve) => setTimeout(resolve, 300))
cacheItems.value.forEach((item) => {
if (item.ttl > 0) {
item.ttl--
if (item.ttl === 0) {
item.expired = true
}
}
})
const expiredCount = cacheItems.value.filter((item) => item.expired).length
dbPressure.value = Math.min(50, expiredCount * 8)
}
setTimeout(() => {
simulating.value = false
}, 1500)
}
initializeCacheItems()
</script>
<style scoped>
.cache-problems-demo {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
border-radius: 12px;
padding: 1.5rem;
margin: 1.5rem 0;
font-family: var(--vp-font-family-base);
}
.header {
margin-bottom: 1.5rem;
}
.title {
font-weight: 700;
font-size: 1.1rem;
}
.subtitle {
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.problem-selector {
display: flex;
gap: 0.75rem;
margin-bottom: 2rem;
flex-wrap: wrap;
}
.problem-btn {
flex: 1;
min-width: 150px;
padding: 1rem;
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 10px;
cursor: pointer;
font-weight: 600;
transition: all 0.2s;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
}
.problem-btn.active {
background: var(--vp-c-brand);
color: white;
border-color: var(--vp-c-brand);
}
.problem-icon {
font-size: 2rem;
}
.problem-name {
font-size: 0.95rem;
}
.problem-content {
min-height: 500px;
}
.problem-detail {
display: flex;
flex-direction: column;
gap: 2rem;
}
.problem-intro {
background: var(--vp-c-bg);
padding: 1.5rem;
border-radius: 10px;
border: 1px solid var(--vp-c-divider);
}
.intro-title {
font-size: 1.2rem;
font-weight: 700;
margin-bottom: 0.75rem;
}
.intro-text {
font-size: 0.95rem;
line-height: 1.6;
color: var(--vp-c-text-1);
}
.problem-scenario {
background: var(--vp-c-bg);
padding: 1.5rem;
border-radius: 10px;
border: 1px solid var(--vp-c-divider);
}
.scenario-title {
font-weight: 600;
margin-bottom: 1rem;
font-size: 0.95rem;
}
.scenario-diagram {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
margin-bottom: 1.5rem;
}
.flow-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem 1.5rem;
background: var(--vp-c-bg-soft);
border-radius: 8px;
border: 2px solid var(--vp-c-divider);
min-width: 250px;
justify-content: center;
}
.flow-item.cache.miss {
border-color: #ef4444;
background: #fef2f2;
}
.flow-item.database.overloaded {
border-color: #ef4444;
background: #fef2f2;
animation: pulse 1s infinite;
}
.flow-icon {
font-size: 1.5rem;
}
.flow-text {
font-weight: 600;
font-size: 0.9rem;
}
.flow-arrow {
font-size: 1.5rem;
color: var(--vp-c-text-2);
}
.controls {
display: flex;
gap: 1rem;
justify-content: center;
margin: 1.5rem 0;
flex-wrap: wrap;
}
.attack-btn {
padding: 0.75rem 1.5rem;
background: #ef4444;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-weight: 600;
transition: all 0.2s;
}
.attack-btn:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.solution-btn {
padding: 0.75rem 1.5rem;
background: #22c55e;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-weight: 600;
transition: all 0.2s;
}
.pressure-meter {
margin-top: 1rem;
}
.meter-label {
font-size: 0.85rem;
margin-bottom: 0.5rem;
font-weight: 600;
}
.meter-bar {
height: 20px;
background: var(--vp-c-bg);
border-radius: 999px;
overflow: hidden;
border: 1px solid var(--vp-c-divider);
}
.meter-fill {
height: 100%;
background: linear-gradient(90deg, #22c55e, #f59e0b, #ef4444);
transition: width 0.3s;
}
.meter-value {
text-align: center;
margin-top: 0.5rem;
font-size: 1.2rem;
font-weight: 700;
}
.hotkey-scenario {
display: flex;
flex-direction: column;
gap: 1.5rem;
margin-bottom: 1.5rem;
}
.hotkey-badge {
text-align: center;
padding: 1rem;
background: #fef3c7;
border-radius: 8px;
border: 2px solid #f59e0b;
font-weight: 600;
}
.key {
display: block;
margin-top: 0.5rem;
font-size: 0.9rem;
color: #92400e;
}
.concurrent-requests {
background: var(--vp-c-bg-soft);
padding: 1rem;
border-radius: 8px;
}
.requests-title {
font-weight: 600;
margin-bottom: 0.75rem;
font-size: 0.9rem;
}
.requests-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 0.5rem;
}
.request-item {
padding: 0.5rem;
background: var(--vp-c-bg);
border-radius: 6px;
border: 1px solid var(--vp-c-divider);
text-align: center;
font-size: 0.75rem;
}
.request-item.waiting {
border-color: #94a3b8;
}
.request-item.processing {
border-color: #f59e0b;
background: #fef3c7;
animation: pulse 1s infinite;
}
.request-item.done {
border-color: #22c55e;
background: #f0fdf4;
}
.request-id {
font-weight: 600;
margin-bottom: 0.25rem;
}
.request-status {
color: var(--vp-c-text-2);
}
.mutex-visual {
text-align: center;
padding: 1rem;
background: #eff6ff;
border-radius: 8px;
border: 2px solid #3b82f6;
}
.mutex-badge {
font-size: 1.5rem;
font-weight: 700;
margin-bottom: 0.5rem;
}
.mutex-text {
font-size: 0.9rem;
color: #1e40af;
}
.avalanche-visual {
display: flex;
flex-direction: column;
gap: 1rem;
margin-bottom: 1.5rem;
}
.cache-items {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
gap: 0.5rem;
}
.cache-item {
padding: 0.5rem;
background: #f0fdf4;
border-radius: 6px;
border: 2px solid #22c55e;
text-align: center;
transition: all 0.3s;
}
.cache-item.expired {
background: #fef2f2;
border-color: #ef4444;
animation: shake 0.5s;
}
.item-key {
font-weight: 600;
font-size: 0.8rem;
margin-bottom: 0.25rem;
}
.item-ttl {
font-size: 0.75rem;
color: var(--vp-c-text-2);
}
.mass-explosion {
text-align: center;
padding: 1rem;
background: #fef2f2;
border-radius: 8px;
border: 2px solid #ef4444;
}
.explosion-icon {
font-size: 3rem;
margin-bottom: 0.5rem;
}
.explosion-text {
font-size: 1.2rem;
font-weight: 700;
color: #dc2626;
}
.db-overload {
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
padding: 1rem;
background: var(--vp-c-bg-soft);
border-radius: 8px;
border: 2px solid var(--vp-c-divider);
transition: all 0.3s;
}
.db-overload.critical {
border-color: #ef4444;
background: #fef2f2;
animation: pulse 1s infinite;
}
.db-icon {
font-size: 2rem;
}
.db-status {
font-weight: 600;
font-size: 1rem;
}
.solutions {
background: var(--vp-c-bg);
padding: 1.5rem;
border-radius: 10px;
border: 1px solid var(--vp-c-divider);
}
.solutions-title {
font-weight: 600;
margin-bottom: 1rem;
font-size: 0.95rem;
}
.solution-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
.solution-item {
padding: 1rem;
background: var(--vp-c-bg-soft);
border-radius: 8px;
border-left: 4px solid var(--vp-c-brand);
}
.solution-header {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 0.5rem;
}
.solution-number {
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
background: var(--vp-c-brand);
color: white;
border-radius: 50%;
font-weight: 700;
font-size: 0.9rem;
}
.solution-name {
font-weight: 600;
font-size: 0.95rem;
}
.solution-desc {
font-size: 0.85rem;
line-height: 1.6;
color: var(--vp-c-text-1);
padding-left: 2.5rem;
}
.note {
display: block;
margin-top: 0.5rem;
color: var(--vp-c-text-2);
font-size: 0.8rem;
}
.code {
display: block;
margin-top: 0.5rem;
padding: 0.5rem;
background: #1e293b;
color: #e2e8f0;
border-radius: 4px;
font-family: monospace;
font-size: 0.8rem;
}
.comparison-table {
background: var(--vp-c-bg);
padding: 1.5rem;
border-radius: 10px;
border: 1px solid var(--vp-c-divider);
}
.table-title {
font-weight: 600;
margin-bottom: 1rem;
font-size: 0.95rem;
}
.problems-table {
width: 100%;
border-collapse: collapse;
}
.problems-table th,
.problems-table td {
padding: 0.75rem;
text-align: left;
border: 1px solid var(--vp-c-divider);
}
.problems-table th {
background: var(--vp-c-bg-soft);
font-weight: 600;
font-size: 0.85rem;
}
.problems-table td {
font-size: 0.85rem;
}
.problems-table tr.active {
background: #eff6ff;
border-left: 3px solid var(--vp-c-brand);
}
.problems-table tr.active td {
border-top-color: var(--vp-c-brand);
border-bottom-color: var(--vp-c-brand);
}
@keyframes pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.6;
}
}
@keyframes shake {
0%,
100% {
transform: translateX(0);
}
25% {
transform: translateX(-5px);
}
75% {
transform: translateX(5px);
}
}
</style>
@@ -0,0 +1,603 @@
<!--
LocalVsDistributedCacheDemo.vue
本地缓存 vs 分布式缓存对比演示
-->
<template>
<div class="cache-comparison-demo">
<div class="header">
<div class="title">本地缓存 vs 分布式缓存</div>
<div class="subtitle">对比两种缓存架构的性能和特点</div>
</div>
<div class="comparison-view">
<!-- Local Cache -->
<div class="cache-side local">
<div class="side-header">
<div class="title">本地缓存 (Local Cache)</div>
<div class="tag">进程内</div>
</div>
<div class="architecture">
<div class="app-instance">
<div class="instance-label">应用实例 1</div>
<div class="cache-box">
<div class="cache-label">缓存</div>
<div class="cache-data">
<div v-for="item in localCache1" :key="item" class="data-item">
{{ item }}
</div>
</div>
</div>
</div>
<div class="app-instance">
<div class="instance-label">应用实例 2</div>
<div class="cache-box">
<div class="cache-label">缓存</div>
<div class="cache-data">
<div v-for="item in localCache2" :key="item" class="data-item">
{{ item }}
</div>
</div>
</div>
</div>
</div>
<div class="metrics">
<div class="metric">
<div class="metric-label">响应时间</div>
<div class="metric-value fast">~1 ms</div>
</div>
<div class="metric">
<div class="metric-label">容量</div>
<div class="metric-value">~1 GB</div>
</div>
<div class="metric">
<div class="metric-label">一致性</div>
<div class="metric-value warning"></div>
</div>
</div>
<div class="pros-cons">
<div class="pros">
<div class="list-title"> 优点</div>
<div class="list-item">极快无网络开销</div>
<div class="list-item">简单内存 Map</div>
</div>
<div class="cons">
<div class="list-title"> 缺点</div>
<div class="list-item">容量受限</div>
<div class="list-item">实例间不一致</div>
</div>
</div>
</div>
<!-- Distributed Cache -->
<div class="cache-side distributed">
<div class="side-header">
<div class="title">分布式缓存 (Distributed Cache)</div>
<div class="tag">独立服务</div>
</div>
<div class="architecture">
<div class="instances-row">
<div class="app-instance-small">
<div class="instance-label-small">实例 1</div>
</div>
<div class="app-instance-small">
<div class="instance-label-small">实例 2</div>
</div>
<div class="app-instance-small">
<div class="instance-label-small">实例 3</div>
</div>
</div>
<div class="network-layer">
<div class="network-label">网络</div>
<div class="network-arrows"> </div>
</div>
<div class="redis-cluster">
<div class="cluster-label">Redis 集群</div>
<div class="redis-nodes">
<div class="redis-node">
<div class="node-label">Node 1</div>
<div class="node-data">
<div
v-for="item in redisData1"
:key="item"
class="data-item small"
>
{{ item }}
</div>
</div>
</div>
<div class="redis-node">
<div class="node-label">Node 2</div>
<div class="node-data">
<div
v-for="item in redisData2"
:key="item"
class="data-item small"
>
{{ item }}
</div>
</div>
</div>
<div class="redis-node">
<div class="node-label">Node 3</div>
<div class="node-data">
<div
v-for="item in redisData3"
:key="item"
class="data-item small"
>
{{ item }}
</div>
</div>
</div>
</div>
</div>
</div>
<div class="metrics">
<div class="metric">
<div class="metric-label">响应时间</div>
<div class="metric-value medium">~5 ms</div>
</div>
<div class="metric">
<div class="metric-label">容量</div>
<div class="metric-value">~100 GB</div>
</div>
<div class="metric">
<div class="metric-label">一致性</div>
<div class="metric-value good"></div>
</div>
</div>
<div class="pros-cons">
<div class="pros">
<div class="list-title"> 优点</div>
<div class="list-item">容量可扩展</div>
<div class="list-item">全局共享</div>
</div>
<div class="cons">
<div class="list-title"> 缺点</div>
<div class="list-item">网络延迟</div>
<div class="list-item">需要维护</div>
</div>
</div>
</div>
</div>
<div class="interactive-demo">
<div class="demo-title">交互演示写入和读取数据</div>
<div class="demo-controls">
<button class="demo-btn" @click="simulateWrite">写入数据</button>
<button class="demo-btn secondary" @click="simulateRead">
读取数据
</button>
<button class="demo-btn reset" @click="reset">重置</button>
</div>
<div class="demo-result" v-if="lastOperation">
<div class="result-icon">{{ lastOperation.icon }}</div>
<div class="result-text">{{ lastOperation.text }}</div>
<div class="result-detail">{{ lastOperation.detail }}</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const localCache1 = ref(['user:1', 'user:2', 'config:A'])
const localCache2 = ref(['user:3', 'config:B'])
const redisData1 = ref(['user:1', 'user:2', 'user:3'])
const redisData2 = ref(['product:A', 'product:B', 'product:C'])
const redisData3 = ref(['config:A', 'config:B'])
const lastOperation = ref(null)
let dataCounter = 4
const simulateWrite = () => {
const key = `user:${dataCounter++}`
// Local cache: Write to instance 1 only
localCache1.value.push(key)
if (localCache1.value.length > 5) localCache1.value.shift()
// Distributed cache: Hash to a node
const nodeIndex = dataCounter % 3
if (nodeIndex === 0) redisData1.value.push(key)
else if (nodeIndex === 1) redisData2.value.push(key)
else redisData3.value.push(key)
lastOperation.value = {
icon: '✍️',
text: `写入 ${key}`,
detail: '本地缓存: 仅实例1有数据 | 分布式缓存: 所有实例共享'
}
}
const simulateRead = () => {
const key = 'user:1'
const inLocal1 = localCache1.value.includes(key)
const inLocal2 = localCache2.value.includes(key)
const inRedis =
redisData1.value.includes(key) ||
redisData2.value.includes(key) ||
redisData3.value.includes(key)
lastOperation.value = {
icon: '🔍',
text: `读取 ${key}`,
detail: `本地缓存: 实例1${inLocal1 ? '✅' : '❌'} 实例2${inLocal2 ? '✅' : '❌'} | 分布式缓存: ${inRedis ? '✅' : '❌'}`
}
}
const reset = () => {
localCache1.value = ['user:1', 'user:2', 'config:A']
localCache2.value = ['user:3', 'config:B']
redisData1.value = ['user:1', 'user:2', 'user:3']
redisData2.value = ['product:A', 'product:B', 'product:C']
redisData3.value = ['config:A', 'config:B']
dataCounter = 4
lastOperation.value = null
}
</script>
<style scoped>
.cache-comparison-demo {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
border-radius: 12px;
padding: 1.5rem;
margin: 1.5rem 0;
font-family: var(--vp-font-family-base);
}
.header {
margin-bottom: 1.5rem;
}
.title {
font-weight: 700;
font-size: 1.1rem;
}
.subtitle {
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.comparison-view {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2rem;
margin-bottom: 2rem;
}
@media (max-width: 960px) {
.comparison-view {
grid-template-columns: 1fr;
}
}
.cache-side {
background: var(--vp-c-bg);
border-radius: 10px;
padding: 1.25rem;
border: 2px solid var(--vp-c-divider);
}
.cache-side.local {
border-color: #3b82f6;
}
.cache-side.distributed {
border-color: #ef4444;
}
.side-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.side-header .title {
font-size: 1rem;
font-weight: 700;
}
.tag {
padding: 0.25rem 0.75rem;
border-radius: 999px;
font-size: 0.75rem;
font-weight: 600;
background: var(--vp-c-bg-soft);
}
.architecture {
margin-bottom: 1rem;
}
.app-instance {
background: #eff6ff;
padding: 0.75rem;
border-radius: 8px;
margin-bottom: 0.5rem;
border: 1px solid #bfdbfe;
}
.instance-label {
font-size: 0.8rem;
font-weight: 600;
margin-bottom: 0.5rem;
color: #1e40af;
}
.cache-box {
background: white;
padding: 0.5rem;
border-radius: 6px;
border: 1px dashed #93c5fd;
}
.cache-label {
font-size: 0.7rem;
font-weight: 600;
margin-bottom: 0.35rem;
color: var(--vp-c-text-2);
}
.cache-data {
display: flex;
flex-wrap: wrap;
gap: 0.35rem;
}
.data-item {
padding: 0.2rem 0.5rem;
background: #dbeafe;
border-radius: 4px;
font-size: 0.7rem;
font-weight: 600;
color: #1e40af;
}
.data-item.small {
padding: 0.15rem 0.35rem;
font-size: 0.65rem;
}
.instances-row {
display: flex;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.app-instance-small {
flex: 1;
background: #fef2f2;
padding: 0.5rem;
border-radius: 6px;
border: 1px solid #fecaca;
text-align: center;
}
.instance-label-small {
font-size: 0.75rem;
font-weight: 600;
color: #991b1b;
}
.network-layer {
text-align: center;
padding: 0.5rem;
background: #fef3c7;
border-radius: 6px;
margin-bottom: 0.75rem;
}
.network-label {
font-size: 0.75rem;
font-weight: 600;
margin-bottom: 0.25rem;
}
.network-arrows {
font-size: 1.2rem;
}
.redis-cluster {
background: #fef2f2;
padding: 0.75rem;
border-radius: 8px;
border: 1px solid #fecaca;
}
.cluster-label {
font-size: 0.8rem;
font-weight: 600;
margin-bottom: 0.5rem;
color: #991b1b;
text-align: center;
}
.redis-nodes {
display: flex;
gap: 0.5rem;
}
.redis-node {
flex: 1;
background: white;
padding: 0.5rem;
border-radius: 6px;
border: 1px dashed #fca5a5;
}
.node-label {
font-size: 0.7rem;
font-weight: 600;
margin-bottom: 0.35rem;
color: var(--vp-c-text-2);
text-align: center;
}
.node-data {
display: flex;
flex-wrap: wrap;
gap: 0.25rem;
}
.metrics {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.75rem;
margin-bottom: 1rem;
padding: 0.75rem;
background: var(--vp-c-bg-soft);
border-radius: 8px;
}
.metric {
text-align: center;
}
.metric-label {
font-size: 0.75rem;
color: var(--vp-c-text-2);
margin-bottom: 0.25rem;
}
.metric-value {
font-size: 0.9rem;
font-weight: 700;
}
.metric-value.fast {
color: #22c55e;
}
.metric-value.medium {
color: #f59e0b;
}
.metric-value.good {
color: #22c55e;
}
.metric-value.warning {
color: #ef4444;
}
.pros-cons {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
.pros,
.cons {
padding: 0.75rem;
border-radius: 8px;
}
.pros {
background: #f0fdf4;
border: 1px solid #bbf7d0;
}
.cons {
background: #fef2f2;
border: 1px solid #fecaca;
}
.list-title {
font-size: 0.8rem;
font-weight: 600;
margin-bottom: 0.5rem;
}
.list-item {
font-size: 0.75rem;
margin-bottom: 0.35rem;
line-height: 1.4;
}
.interactive-demo {
background: var(--vp-c-bg);
border-radius: 10px;
padding: 1.5rem;
border: 1px solid var(--vp-c-divider);
}
.demo-title {
font-weight: 600;
margin-bottom: 1rem;
font-size: 0.95rem;
}
.demo-controls {
display: flex;
gap: 0.75rem;
margin-bottom: 1rem;
flex-wrap: wrap;
}
.demo-btn {
padding: 0.75rem 1.5rem;
background: var(--vp-c-brand);
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-weight: 600;
font-size: 0.9rem;
transition: all 0.2s;
}
.demo-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.demo-btn.secondary {
background: #3b82f6;
}
.demo-btn.reset {
background: var(--vp-c-bg);
color: var(--vp-c-text-1);
border: 1px solid var(--vp-c-divider);
}
.demo-result {
display: flex;
align-items: center;
gap: 1rem;
padding: 1rem;
background: var(--vp-c-bg-soft);
border-radius: 8px;
border-left: 4px solid var(--vp-c-brand);
}
.result-icon {
font-size: 1.5rem;
}
.result-text {
font-weight: 600;
font-size: 0.9rem;
}
.result-detail {
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
</style>
@@ -0,0 +1,538 @@
<!--
LocalityPrincipleDemo.vue
局部性原理演示 - 展示时间局部性和空间局部性
-->
<template>
<div class="locality-demo">
<div class="header">
<div class="title">局部性原理演示</div>
<div class="subtitle">理解缓存为什么有效</div>
</div>
<div class="tabs">
<button
class="tab-btn"
:class="{ active: activeTab === 'temporal' }"
@click="activeTab = 'temporal'"
>
时间局部性
</button>
<button
class="tab-btn"
:class="{ active: activeTab === 'spatial' }"
@click="activeTab = 'spatial'"
>
空间局部性
</button>
</div>
<div class="tab-content">
<!-- Temporal Locality -->
<div v-if="activeTab === 'temporal'" class="temporal-demo">
<div class="description">
<strong>时间局部性</strong
>如果你访问了某个数据未来很可能再次访问它
<br />
<span class="example"
>例子用户登录后每次请求都需要查询用户信息</span
>
</div>
<div class="timeline">
<div class="timeline-title">访问时间线</div>
<div class="timeline-events">
<div
v-for="(event, index) in temporalEvents"
:key="index"
class="event"
:class="{ hit: event.hit, miss: !event.hit }"
>
<div class="event-time">{{ event.time }}</div>
<div class="event-action">
<span class="user-icon">👤</span>
<span>查询 user_{{ event.userId }}</span>
</div>
<div class="event-result">
{{ event.hit ? '✅ 缓存命中' : '❌ 缓存未命中' }}
</div>
</div>
</div>
</div>
<div class="cache-state">
<div class="cache-title">当前缓存状态</div>
<div class="cache-items">
<div
v-for="item in cacheItems"
:key="item.id"
class="cache-item"
:class="{ active: item.active }"
>
<div class="item-id">{{ item.id }}</div>
<div class="item-hits">命中 {{ item.hits }} </div>
</div>
</div>
</div>
</div>
<!-- Spatial Locality -->
<div v-if="activeTab === 'spatial'" class="spatial-demo">
<div class="description">
<strong>空间局部性</strong
>如果你访问了某个数据很可能访问它附近的数据
<br />
<span class="example">例子浏览商品列表时通常会翻到下一页</span>
</div>
<div class="product-grid">
<div class="grid-title">商品浏览序列</div>
<div class="products">
<div
v-for="product in products"
:key="product.id"
class="product"
:class="{
viewed: product.viewed,
cached: product.cached,
current: product.current
}"
>
<div class="product-id">{{ product.id }}</div>
<div class="product-status">
<span v-if="product.current">👁 当前</span>
<span v-else-if="product.cached"> 已缓存</span>
<span v-else-if="product.viewed"> 已浏览</span>
</div>
</div>
</div>
</div>
<div class="spatial-explanation">
<div class="explanation-item">
<div class="icon">📊</div>
<div class="text">
<strong>预取策略</strong>当你浏览第 5 个商品时系统自动将 6-8
预加载到缓存
</div>
</div>
<div class="explanation-item">
<div class="icon">🎯</div>
<div class="text">
<strong>命中率提升</strong>空间局部性让缓存命中率达到 70-90%
</div>
</div>
</div>
</div>
</div>
<div class="interactive-controls">
<button class="control-btn" @click="addEvent">添加访问事件</button>
<button class="control-btn secondary" @click="reset">重置</button>
</div>
<div class="stats">
<div class="stat-item">
<div class="stat-label">总访问次数</div>
<div class="stat-value">{{ totalAccess }}</div>
</div>
<div class="stat-item">
<div class="stat-label">缓存命中</div>
<div class="stat-value hit">{{ hitCount }}</div>
</div>
<div class="stat-item">
<div class="stat-label">命中率</div>
<div class="stat-value">{{ hitRate }}%</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const activeTab = ref('temporal')
const temporalEvents = ref([])
const cacheItems = ref([])
const products = ref([])
let eventCounter = 0
const totalAccess = computed(() => temporalEvents.value.length)
const hitCount = computed(
() => temporalEvents.value.filter((e) => e.hit).length
)
const hitRate = computed(() => {
if (totalAccess.value === 0) return 0
return Math.round((hitCount.value / totalAccess.value) * 100)
})
const initializeProducts = () => {
products.value = Array.from({ length: 10 }, (_, i) => ({
id: `P${i + 1}`,
viewed: false,
cached: false,
current: false
}))
}
const addEvent = () => {
const currentTime = new Date().toLocaleTimeString()
const userId = Math.floor(Math.random() * 3) + 1
const existingItem = cacheItems.value.find(
(item) => item.id === `user_${userId}`
)
const hit = existingItem !== undefined
if (existingItem) {
existingItem.hits++
existingItem.active = true
setTimeout(() => {
existingItem.active = false
}, 1000)
} else {
if (cacheItems.value.length >= 5) {
cacheItems.value.shift()
}
cacheItems.value.push({
id: `user_${userId}`,
hits: 1,
active: true
})
}
temporalEvents.value.push({
time: currentTime,
userId,
hit
})
if (temporalEvents.value.length > 8) {
temporalEvents.value.shift()
}
if (activeTab.value === 'spatial') {
const currentIndex = products.value.findIndex((p) => p.current)
if (currentIndex !== -1) {
products.value[currentIndex].current = false
products.value[currentIndex].viewed = true
}
const nextIndex = currentIndex + 1
if (nextIndex < products.value.length) {
products.value[nextIndex].current = true
products.value[nextIndex].viewed = true
// Prefetch next items
for (let i = 1; i <= 2; i++) {
const prefetchIndex = nextIndex + i
if (prefetchIndex < products.value.length) {
products.value[prefetchIndex].cached = true
}
}
}
}
}
const reset = () => {
temporalEvents.value = []
cacheItems.value = []
initializeProducts()
}
initializeProducts()
</script>
<style scoped>
.locality-demo {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
border-radius: 12px;
padding: 1.5rem;
margin: 1.5rem 0;
font-family: var(--vp-font-family-base);
}
.header {
margin-bottom: 1rem;
}
.title {
font-weight: 700;
font-size: 1.05rem;
}
.subtitle {
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.tabs {
display: flex;
gap: 0.5rem;
margin-bottom: 1.5rem;
}
.tab-btn {
padding: 0.5rem 1rem;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
font-size: 0.9rem;
}
.tab-btn.active {
background: var(--vp-c-brand);
color: white;
border-color: var(--vp-c-brand);
}
.tab-content {
min-height: 300px;
}
.description {
margin-bottom: 1.5rem;
padding: 1rem;
background: var(--vp-c-bg);
border-radius: 8px;
border-left: 4px solid var(--vp-c-brand);
}
.example {
display: block;
margin-top: 0.5rem;
color: var(--vp-c-text-2);
font-size: 0.85rem;
}
.timeline {
margin-bottom: 1.5rem;
}
.timeline-title {
font-weight: 600;
margin-bottom: 0.75rem;
font-size: 0.95rem;
}
.timeline-events {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.event {
display: grid;
grid-template-columns: 80px 1fr 120px;
align-items: center;
gap: 1rem;
padding: 0.75rem;
background: var(--vp-c-bg);
border-radius: 8px;
border-left: 3px solid #94a3b8;
}
.event.hit {
border-left-color: #22c55e;
}
.event.miss {
border-left-color: #ef4444;
}
.event-time {
font-size: 0.8rem;
color: var(--vp-c-text-2);
}
.event-action {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.9rem;
}
.user-icon {
font-size: 1.2rem;
}
.event-result {
font-size: 0.8rem;
font-weight: 600;
}
.cache-state {
background: var(--vp-c-bg);
padding: 1rem;
border-radius: 8px;
}
.cache-title {
font-weight: 600;
margin-bottom: 0.75rem;
font-size: 0.95rem;
}
.cache-items {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.cache-item {
background: var(--vp-c-bg-soft);
padding: 0.5rem 0.75rem;
border-radius: 6px;
border: 1px solid var(--vp-c-divider);
font-size: 0.85rem;
transition: all 0.3s;
}
.cache-item.active {
border-color: var(--vp-c-brand);
background: #eff6ff;
transform: scale(1.05);
}
.item-id {
font-weight: 600;
}
.item-hits {
font-size: 0.75rem;
color: var(--vp-c-text-2);
margin-top: 0.25rem;
}
.product-grid {
margin-bottom: 1.5rem;
}
.grid-title {
font-weight: 600;
margin-bottom: 0.75rem;
font-size: 0.95rem;
}
.products {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));
gap: 0.5rem;
}
.product {
background: var(--vp-c-bg);
padding: 0.75rem;
border-radius: 8px;
border: 2px solid var(--vp-c-divider);
text-align: center;
transition: all 0.3s;
}
.product.viewed {
border-color: #94a3b8;
}
.product.cached {
border-color: #f59e0b;
background: #fef3c7;
}
.product.current {
border-color: var(--vp-c-brand);
background: #dbeafe;
transform: scale(1.1);
}
.product-id {
font-weight: 600;
margin-bottom: 0.25rem;
}
.product-status {
font-size: 0.75rem;
}
.spatial-explanation {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.explanation-item {
display: flex;
gap: 0.75rem;
padding: 0.75rem;
background: var(--vp-c-bg);
border-radius: 8px;
}
.icon {
font-size: 1.5rem;
}
.text {
flex: 1;
font-size: 0.85rem;
line-height: 1.5;
}
.interactive-controls {
display: flex;
gap: 0.5rem;
justify-content: center;
margin: 1.5rem 0;
}
.control-btn {
padding: 0.75rem 1.5rem;
background: var(--vp-c-brand);
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-weight: 600;
transition: all 0.2s;
}
.control-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.control-btn.secondary {
background: var(--vp-c-bg);
color: var(--vp-c-text-1);
border: 1px solid var(--vp-c-divider);
}
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 1rem;
padding-top: 1.5rem;
border-top: 1px solid var(--vp-c-divider);
}
.stat-item {
text-align: center;
}
.stat-label {
font-size: 0.85rem;
color: var(--vp-c-text-2);
margin-bottom: 0.25rem;
}
.stat-value {
font-size: 1.5rem;
font-weight: 700;
color: var(--vp-c-text-1);
}
.stat-value.hit {
color: #22c55e;
}
</style>
@@ -0,0 +1,620 @@
<!--
MultiLevelCacheDemo.vue
多级缓存架构演示 - 展示浏览器缓存CDN本地缓存Redis数据库的多级架构
-->
<template>
<div class="multi-level-cache-demo">
<div class="header">
<div class="title">多级缓存架构</div>
<div class="subtitle">每一层都是上一层的"保护伞"</div>
</div>
<div class="cache-levels">
<div
v-for="(level, index) in cacheLevels"
:key="level.name"
class="cache-level"
:class="{
active: activeLevel === index,
hit: level.status === 'hit',
miss: level.status === 'miss'
}"
>
<div class="level-number">L{{ level.layer }}</div>
<div class="level-content">
<div class="level-header">
<div class="level-name">{{ level.name }}</div>
<div class="level-meta">
<span class="latency">{{ level.latency }}</span>
<span class="capacity">{{ level.capacity }}</span>
</div>
</div>
<div class="level-description">{{ level.description }}</div>
<div class="level-status" v-if="level.status">
<span v-if="level.status === 'hit'" class="status-badge hit"
> 命中</span
>
<span v-if="level.status === 'miss'" class="status-badge miss"
> 未命中</span
>
</div>
</div>
<div class="level-arrow" v-if="index < cacheLevels.length - 1"></div>
</div>
</div>
<div class="controls">
<div class="control-group">
<label>请求数据</label>
<button class="request-btn" @click="makeRequest" :disabled="processing">
{{ processing ? '处理中...' : '发起请求' }}
</button>
</div>
<div class="control-group">
<label>模拟场景</label>
<select
v-model="scenario"
@change="onScenarioChange"
class="scenario-select"
>
<option value="normal">正常访问 (70% 命中率)</option>
<option value="cold">冷启动 (0% 命中率)</option>
<option value="hot">热点数据 (95% 命中率)</option>
</select>
</div>
</div>
<div class="request-flow" v-if="requestHistory.length > 0">
<div class="flow-title">请求流程</div>
<div class="flow-timeline">
<div
v-for="(event, index) in requestHistory"
:key="index"
class="flow-event"
:class="event.type"
>
<div class="event-level">{{ event.level }}</div>
<div class="event-action">
<span class="event-icon">{{ event.icon }}</span>
<span class="event-text">{{ event.action }}</span>
</div>
<div class="event-time">{{ event.time }}ms</div>
</div>
</div>
</div>
<div class="statistics">
<div class="stat-card">
<div class="stat-label">总请求数</div>
<div class="stat-value">{{ stats.totalRequests }}</div>
</div>
<div class="stat-card">
<div class="stat-label">缓存命中</div>
<div class="stat-value hit">{{ stats.cacheHits }}</div>
</div>
<div class="stat-card">
<div class="stat-label">命中率</div>
<div class="stat-value">{{ stats.hitRate }}%</div>
</div>
<div class="stat-card">
<div class="stat-label">平均响应时间</div>
<div class="stat-value">{{ stats.avgLatency }}ms</div>
</div>
<div class="stat-card">
<div class="stat-label">数据库访问</div>
<div class="stat-value db">{{ stats.dbAccess }}</div>
</div>
</div>
<div class="explanation">
<div class="explanation-title">多级缓存的优势</div>
<div class="explanation-grid">
<div class="explanation-item">
<div class="item-icon">🛡</div>
<div class="item-text">
<strong>逐级过滤</strong>
<br />
<span class="item-detail"
>每层过滤掉大部分请求最终到达数据库的可能只有 1%</span
>
</div>
</div>
<div class="explanation-item">
<div class="item-icon"></div>
<div class="item-text">
<strong>极速响应</strong>
<br />
<span class="item-detail"
>上层缓存命中时响应时间从 50ms 降至 0-10ms</span
>
</div>
</div>
<div class="explanation-item">
<div class="item-icon">💰</div>
<div class="item-text">
<strong>降低成本</strong>
<br />
<span class="item-detail"
>减少昂贵的数据库查询节省服务器资源</span
>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const activeLevel = ref(-1)
const processing = ref(false)
const scenario = ref('normal')
const requestHistory = ref([])
const cacheLevels = ref([
{
layer: 1,
name: '浏览器缓存',
latency: '~0 ms',
capacity: '~100 MB',
description: '静态资源(图片、CSS、JS',
status: null
},
{
layer: 2,
name: 'CDN 缓存',
latency: '~10 ms',
capacity: 'TB 级',
description: '边缘节点静态文件',
status: null
},
{
layer: 3,
name: '本地缓存',
latency: '~1 ms',
capacity: '~1 GB',
description: '进程内极热点数据',
status: null
},
{
layer: 4,
name: 'Redis 缓存',
latency: '~5 ms',
capacity: '~100 GB',
description: '分布式热点数据',
status: null
},
{
layer: 5,
name: '数据库',
latency: '~50 ms',
capacity: 'TB ~ PB',
description: '持久化存储',
status: null
}
])
const stats = ref({
totalRequests: 0,
cacheHits: 0,
hitRate: 0,
avgLatency: 0,
dbAccess: 0
})
const scenarioConfigs = {
normal: { hitRate: 0.7 },
cold: { hitRate: 0 },
hot: { hitRate: 0.95 }
}
const onScenarioChange = () => {
requestHistory.value = []
stats.value = {
totalRequests: 0,
cacheHits: 0,
hitRate: 0,
avgLatency: 0,
dbAccess: 0
}
cacheLevels.value.forEach((level) => {
level.status = null
})
}
const makeRequest = async () => {
if (processing.value) return
processing.value = true
requestHistory.value = []
// Reset statuses
cacheLevels.value.forEach((level) => {
level.status = null
})
const config = scenarioConfigs[scenario.value]
let hit = Math.random() < config.hitRate
let totalLatency = 0
const delays = [100, 100, 100, 100, 100]
for (let i = 0; i < cacheLevels.value.length; i++) {
activeLevel.value = i
await new Promise((resolve) => setTimeout(resolve, delays[i]))
const level = cacheLevels.value[i]
let eventTime = 0
if (hit && i < cacheLevels.value.length - 1) {
level.status = 'hit'
eventTime = parseInt(level.latency.match(/\d+/)[0])
totalLatency += eventTime
requestHistory.value.push({
level: level.name,
icon: '✅',
action: '缓存命中',
time: eventTime,
type: 'hit'
})
stats.value.cacheHits++
break
} else if (i === cacheLevels.value.length - 1) {
level.status = 'miss'
eventTime = parseInt(level.latency.match(/\d+/)[0])
totalLatency += eventTime
requestHistory.value.push({
level: level.name,
icon: '🗄️',
action: '查询数据库',
time: eventTime,
type: 'miss'
})
stats.value.dbAccess++
} else {
level.status = 'miss'
eventTime = parseInt(level.latency.match(/\d+/)[0])
totalLatency += eventTime
requestHistory.value.push({
level: level.name,
icon: '❌',
action: '未命中,继续',
time: eventTime,
type: 'miss'
})
}
}
stats.value.totalRequests++
stats.value.hitRate = Math.round(
(stats.value.cacheHits / stats.value.totalRequests) * 100
)
stats.value.avgLatency = Math.round(totalLatency)
processing.value = false
activeLevel.value = -1
}
</script>
<style scoped>
.multi-level-cache-demo {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
border-radius: 12px;
padding: 1.5rem;
margin: 1.5rem 0;
font-family: var(--vp-font-family-base);
}
.header {
margin-bottom: 1.5rem;
}
.title {
font-weight: 700;
font-size: 1.1rem;
}
.subtitle {
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.cache-levels {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin-bottom: 2rem;
}
.cache-level {
display: flex;
align-items: center;
gap: 1rem;
padding: 1rem;
background: var(--vp-c-bg);
border-radius: 10px;
border: 2px solid var(--vp-c-divider);
transition: all 0.3s;
}
.cache-level.active {
border-color: var(--vp-c-brand);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.cache-level.hit {
border-color: #22c55e;
background: #f0fdf4;
}
.cache-level.miss {
border-color: #ef4444;
background: #fef2f2;
}
.level-number {
width: 50px;
height: 50px;
display: flex;
align-items: center;
justify-content: center;
background: var(--vp-c-brand);
color: white;
border-radius: 8px;
font-weight: 700;
font-size: 1.1rem;
flex-shrink: 0;
}
.level-content {
flex: 1;
}
.level-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
}
.level-name {
font-weight: 700;
font-size: 1rem;
}
.level-meta {
display: flex;
gap: 1rem;
font-size: 0.8rem;
color: var(--vp-c-text-2);
}
.level-description {
font-size: 0.85rem;
color: var(--vp-c-text-2);
margin-bottom: 0.5rem;
}
.level-status {
display: flex;
gap: 0.5rem;
}
.status-badge {
padding: 0.25rem 0.75rem;
border-radius: 999px;
font-size: 0.75rem;
font-weight: 600;
}
.status-badge.hit {
background: #22c55e;
color: white;
}
.status-badge.miss {
background: #ef4444;
color: white;
}
.level-arrow {
width: 40px;
text-align: center;
font-size: 1.5rem;
color: var(--vp-c-text-2);
flex-shrink: 0;
}
.controls {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
margin-bottom: 2rem;
}
.control-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.control-group label {
font-size: 0.85rem;
font-weight: 600;
color: var(--vp-c-text-2);
}
.request-btn {
padding: 0.75rem 1.5rem;
background: var(--vp-c-brand);
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-weight: 600;
transition: all 0.2s;
}
.request-btn:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.request-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.scenario-select {
padding: 0.75rem;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
cursor: pointer;
font-size: 0.9rem;
}
.request-flow {
background: var(--vp-c-bg);
border-radius: 10px;
padding: 1.25rem;
margin-bottom: 1.5rem;
border: 1px solid var(--vp-c-divider);
}
.flow-title {
font-weight: 600;
margin-bottom: 1rem;
font-size: 0.95rem;
}
.flow-timeline {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.flow-event {
display: grid;
grid-template-columns: 120px 1fr 80px;
align-items: center;
gap: 1rem;
padding: 0.75rem;
border-radius: 8px;
font-size: 0.85rem;
}
.flow-event.hit {
background: #f0fdf4;
}
.flow-event.miss {
background: #fef2f2;
}
.event-level {
font-weight: 600;
color: var(--vp-c-text-1);
}
.event-action {
display: flex;
align-items: center;
gap: 0.5rem;
}
.event-icon {
font-size: 1rem;
}
.event-time {
font-weight: 600;
color: var(--vp-c-text-2);
}
.statistics {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 1rem;
margin-bottom: 1.5rem;
}
.stat-card {
background: var(--vp-c-bg);
border-radius: 10px;
padding: 1rem;
text-align: center;
border: 1px solid var(--vp-c-divider);
}
.stat-label {
font-size: 0.8rem;
color: var(--vp-c-text-2);
margin-bottom: 0.5rem;
}
.stat-value {
font-size: 1.5rem;
font-weight: 700;
color: var(--vp-c-text-1);
}
.stat-value.hit {
color: #22c55e;
}
.stat-value.db {
color: #ef4444;
}
.explanation {
background: var(--vp-c-bg);
border-radius: 10px;
padding: 1.5rem;
border: 1px solid var(--vp-c-divider);
}
.explanation-title {
font-weight: 600;
margin-bottom: 1rem;
font-size: 0.95rem;
}
.explanation-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1rem;
}
.explanation-item {
display: flex;
gap: 1rem;
padding: 1rem;
background: var(--vp-c-bg-soft);
border-radius: 8px;
}
.item-icon {
font-size: 2rem;
flex-shrink: 0;
}
.item-text {
font-size: 0.85rem;
line-height: 1.5;
}
.item-detail {
color: var(--vp-c-text-2);
font-size: 0.8rem;
}
</style>
@@ -0,0 +1,914 @@
<!--
ProductCacheDemo.vue
商品详情页缓存实战演示 - 完整的三级缓存系统
-->
<template>
<div class="product-cache-demo">
<div class="header">
<div class="title">商品详情页缓存系统实战</div>
<div class="subtitle">完整的三级缓存架构 + 监控面板</div>
</div>
<div class="architecture-overview">
<div class="overview-title">系统架构</div>
<div class="architecture-diagram">
<div class="layer client">
<div class="layer-label">客户端</div>
<div class="layer-icon">📱</div>
</div>
<div class="arrow"></div>
<div class="layer local-cache" :class="{ hit: currentLevel === 1 }">
<div class="layer-label">L1: 本地缓存 (Caffeine)</div>
<div class="layer-stats">
<div>容量: 1000</div>
<div>TTL: 30s</div>
<div>命中: {{ localHits }}</div>
</div>
</div>
<div class="arrow"></div>
<div class="layer redis-cache" :class="{ hit: currentLevel === 2 }">
<div class="layer-label">L2: Redis 集群</div>
<div class="layer-stats">
<div>容量: 100</div>
<div>TTL: 5min</div>
<div>命中: {{ redisHits }}</div>
</div>
</div>
<div class="arrow"></div>
<div class="layer database" :class="{ hit: currentLevel === 3 }">
<div class="layer-label">L3: MySQL 数据库</div>
<div class="layer-stats">
<div>持久化存储</div>
<div>查询: {{ dbQueries }}</div>
</div>
</div>
</div>
</div>
<div class="demo-sections">
<div class="section query-demo">
<div class="section-title">查询商品</div>
<div class="query-controls">
<input
v-model="productId"
type="text"
placeholder="输入商品ID (如: P001)"
class="product-input"
/>
<button class="query-btn" @click="queryProduct" :disabled="querying">
{{ querying ? '查询中...' : '查询' }}
</button>
<button class="reset-btn" @click="resetDemo">重置</button>
</div>
<div class="query-result" v-if="queryResult">
<div class="result-header">
<span class="result-icon">{{ queryResult.icon }}</span>
<span class="result-title">{{ queryResult.title }}</span>
</div>
<div class="result-details">
<div class="detail-item">
<span class="detail-label">商品ID:</span>
<span class="detail-value">{{ queryResult.id }}</span>
</div>
<div class="detail-item">
<span class="detail-label">名称:</span>
<span class="detail-value">{{ queryResult.name }}</span>
</div>
<div class="detail-item">
<span class="detail-label">价格:</span>
<span class="detail-value">¥{{ queryResult.price }}</span>
</div>
<div class="detail-item">
<span class="detail-label">来源:</span>
<span
class="detail-value source"
:class="queryResult.sourceLevel"
>
{{ queryResult.source }}
</span>
</div>
<div class="detail-item">
<span class="detail-label">响应时间:</span>
<span class="detail-value">{{ queryResult.responseTime }}ms</span>
</div>
</div>
</div>
<div class="query-flow" v-if="queryFlow.length > 0">
<div class="flow-title">查询流程</div>
<div class="flow-steps">
<div
v-for="(step, index) in queryFlow"
:key="index"
class="flow-step"
:class="step.type"
>
<div class="step-level">{{ step.level }}</div>
<div class="step-result">{{ step.result }}</div>
<div class="step-time">{{ step.time }}ms</div>
</div>
</div>
</div>
</div>
<div class="section cache-monitor">
<div class="section-title">缓存监控</div>
<div class="metrics-grid">
<div class="metric-card">
<div class="metric-label">总请求数</div>
<div class="metric-value">{{ metrics.totalRequests }}</div>
</div>
<div class="metric-card">
<div class="metric-label">本地缓存命中</div>
<div class="metric-value local">{{ localHits }}</div>
</div>
<div class="metric-card">
<div class="metric-label">Redis命中</div>
<div class="metric-value redis">{{ redisHits }}</div>
</div>
<div class="metric-card">
<div class="metric-label">数据库查询</div>
<div class="metric-value db">{{ dbQueries }}</div>
</div>
</div>
<div class="hit-rate-display">
<div class="rate-label">整体命中率</div>
<div class="rate-value">{{ overallHitRate }}%</div>
<div class="rate-bar">
<div
class="rate-fill"
:style="{ width: overallHitRate + '%' }"
></div>
</div>
<div class="rate-target">目标: > 90%</div>
</div>
<div class="cache-stats-detail">
<div class="stats-title">详细统计</div>
<div class="stats-list">
<div class="stat-item">
<span class="stat-label">本地缓存命中率:</span>
<span class="stat-value">{{ localHitRate }}%</span>
</div>
<div class="stat-item">
<span class="stat-label">Redis缓存命中率:</span>
<span class="stat-value">{{ redisHitRate }}%</span>
</div>
<div class="stat-item">
<span class="stat-label">平均响应时间:</span>
<span class="stat-value">{{ avgResponseTime }}ms</span>
</div>
<div class="stat-item">
<span class="stat-label">数据库压力:</span>
<span class="stat-value">{{ dbPressure }}%</span>
</div>
</div>
</div>
</div>
</div>
<div class="features">
<div class="feature-title">核心特性</div>
<div class="feature-grid">
<div class="feature-item">
<div class="feature-icon">🛡</div>
<div class="feature-name">多级缓存</div>
<div class="feature-desc">
本地缓存 + Redis 双层防护减少 99% 数据库查询
</div>
</div>
<div class="feature-item">
<div class="feature-icon">🔒</div>
<div class="feature-name">防击穿</div>
<div class="feature-desc">互斥锁保护热点数据避免并发查询数据库</div>
</div>
<div class="feature-item">
<div class="feature-icon">🎯</div>
<div class="feature-name">防穿透</div>
<div class="feature-desc">缓存空对象防止查询不存在的商品</div>
</div>
<div class="feature-item">
<div class="feature-icon"></div>
<div class="feature-name">随机 TTL</div>
<div class="feature-desc">避免缓存雪崩过期时间加随机值</div>
</div>
</div>
</div>
<div class="code-preview">
<div class="code-title">核心代码片段</div>
<pre class="code-block"><code>// 三级缓存查询
public Product getProduct(String productId) {
// L1: 本地缓存
Product product = localCache.getIfPresent(productId);
if (product != null) {
metrics.localHits++;
return product;
}
// L2: Redis 缓存
product = redisTemplate.get("product:" + productId);
if (product != null) {
localCache.put(productId, product); // 回填
metrics.redisHits++;
return product;
}
// L3: 数据库(加锁防击穿)
synchronized(this) {
// 双重检查
product = redisTemplate.get("product:" + productId);
if (product != null) return product;
// 查数据库
product = productMapper.selectById(productId);
if (product == null) {
// 缓存空对象(防穿透)
redisTemplate.set("product:" + productId,
NULL_PRODUCT, 5, TimeUnit.MINUTES);
return null;
}
// 写入缓存(随机 TTL 防雪崩)
int ttl = 300 + ThreadLocalRandom.current().nextInt(-30, 30);
redisTemplate.set("product:" + productId, product,
ttl, TimeUnit.SECONDS);
localCache.put(productId, product);
metrics.dbQueries++;
return product;
}
}</code></pre>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const productId = ref('P001')
const querying = ref(false)
const queryResult = ref(null)
const queryFlow = ref([])
const currentLevel = ref(0)
const localHits = ref(0)
const redisHits = ref(0)
const dbQueries = ref(0)
const metrics = ref({
totalRequests: 0
})
const products = {
P001: { id: 'P001', name: 'iPhone 15 Pro', price: 7999 },
P002: { id: 'P002', name: 'MacBook Pro 14"', price: 14999 },
P003: { id: 'P003', name: 'AirPods Pro', price: 1999 },
P004: { id: 'P004', name: 'iPad Air', price: 4799 }
}
const overallHitRate = computed(() => {
const total = metrics.value.totalRequests
if (total === 0) return 0
const hits = localHits.value + redisHits.value
return Math.round((hits / total) * 100)
})
const localHitRate = computed(() => {
const total = metrics.value.totalRequests
if (total === 0) return 0
return Math.round((localHits.value / total) * 100)
})
const redisHitRate = computed(() => {
const total = metrics.value.totalRequests
if (total === 0) return 0
return Math.round((redisHits.value / total) * 100)
})
const avgResponseTime = computed(() => {
const total = metrics.value.totalRequests
if (total === 0) return 0
const totalTime =
localHits.value * 1 + redisHits.value * 5 + dbQueries.value * 50
return Math.round(totalTime / total)
})
const dbPressure = computed(() => {
const total = metrics.value.totalRequests
if (total === 0) return 0
return Math.round((dbQueries.value / total) * 100)
})
const queryProduct = async () => {
if (!productId.value || querying.value) return
querying.value = true
queryFlow.value = []
queryResult.value = null
currentLevel.value = 0
const id = productId.value.toUpperCase()
const exists = products[id]
// Simulate cache levels
const flow = []
// Level 1: Local Cache (30% hit rate for demo)
await new Promise((resolve) => setTimeout(resolve, 300))
currentLevel.value = 1
const localHit = Math.random() < 0.3
flow.push({
level: 'L1: 本地缓存',
result: localHit ? '✅ 命中' : '❌ 未命中',
time: 1,
type: localHit ? 'hit' : 'miss'
})
if (localHit && exists) {
localHits.value++
metrics.value.totalRequests++
queryFlow.value = flow
queryResult.value = {
icon: '⚡',
title: '本地缓存命中',
id: products[id].id,
name: products[id].name,
price: products[id].price,
source: '本地缓存',
sourceLevel: 'local',
responseTime: 1
}
querying.value = false
currentLevel.value = 0
return
}
// Level 2: Redis (50% hit rate for demo)
await new Promise((resolve) => setTimeout(resolve, 300))
currentLevel.value = 2
const redisHit = !localHit && Math.random() < 0.5 && exists
flow.push({
level: 'L2: Redis',
result: redisHit ? '✅ 命中' : '❌ 未命中',
time: 5,
type: redisHit ? 'hit' : 'miss'
})
if (redisHit && exists) {
redisHits.value++
metrics.value.totalRequests++
queryFlow.value = flow
queryResult.value = {
icon: '🚀',
title: 'Redis缓存命中',
id: products[id].id,
name: products[id].name,
price: products[id].price,
source: 'Redis缓存',
sourceLevel: 'redis',
responseTime: 6
}
querying.value = false
currentLevel.value = 0
return
}
// Level 3: Database
await new Promise((resolve) => setTimeout(resolve, 500))
currentLevel.value = 3
flow.push({
level: 'L3: 数据库',
result: exists ? '✅ 查询成功' : '❌ 商品不存在',
time: 50,
type: exists ? 'hit' : 'miss'
})
dbQueries.value++
metrics.value.totalRequests++
queryFlow.value = flow
if (exists) {
queryResult.value = {
icon: '🗄️',
title: '数据库查询',
id: products[id].id,
name: products[id].name,
price: products[id].price,
source: 'MySQL 数据库',
sourceLevel: 'database',
responseTime: 56
}
} else {
queryResult.value = {
icon: '❌',
title: '商品不存在',
id: id,
name: '-',
price: '-',
source: '缓存空对象',
sourceLevel: 'notfound',
responseTime: 56
}
}
querying.value = false
currentLevel.value = 0
}
const resetDemo = () => {
productId.value = 'P001'
queryResult.value = null
queryFlow.value = []
localHits.value = 0
redisHits.value = 0
dbQueries.value = 0
metrics.value.totalRequests = 0
currentLevel.value = 0
}
</script>
<style scoped>
.product-cache-demo {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
border-radius: 12px;
padding: 1.5rem;
margin: 1.5rem 0;
font-family: var(--vp-font-family-base);
}
.header {
margin-bottom: 1.5rem;
}
.title {
font-weight: 700;
font-size: 1.1rem;
}
.subtitle {
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.architecture-overview {
background: var(--vp-c-bg);
padding: 1.5rem;
border-radius: 10px;
margin-bottom: 2rem;
border: 1px solid var(--vp-c-divider);
}
.overview-title {
font-weight: 600;
margin-bottom: 1rem;
font-size: 0.95rem;
}
.architecture-diagram {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
}
.layer {
width: 100%;
max-width: 400px;
padding: 1rem;
border-radius: 10px;
border: 2px solid var(--vp-c-divider);
transition: all 0.3s;
}
.layer.hit {
transform: scale(1.02);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);
}
.layer.client {
background: #f3e8ff;
border-color: #a855f7;
text-align: center;
}
.layer.local-cache {
background: #dbeafe;
border-color: #3b82f6;
}
.layer.local-cache.hit {
background: #eff6ff;
border-color: #3b82f6;
}
.layer.redis-cache {
background: #fef3c7;
border-color: #f59e0b;
}
.layer.redis-cache.hit {
background: #fef9c3;
border-color: #f59e0b;
}
.layer.database {
background: #fee2e2;
border-color: #ef4444;
}
.layer.database.hit {
background: #fef2f2;
border-color: #ef4444;
}
.layer-label {
font-weight: 600;
margin-bottom: 0.5rem;
}
.layer-icon {
font-size: 2rem;
}
.layer-stats {
display: flex;
justify-content: space-around;
font-size: 0.8rem;
color: var(--vp-c-text-2);
}
.arrow {
font-size: 1.5rem;
color: var(--vp-c-text-2);
}
.demo-sections {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1.5rem;
margin-bottom: 2rem;
}
@media (max-width: 960px) {
.demo-sections {
grid-template-columns: 1fr;
}
}
.section {
background: var(--vp-c-bg);
padding: 1.5rem;
border-radius: 10px;
border: 1px solid var(--vp-c-divider);
}
.section-title {
font-weight: 600;
margin-bottom: 1rem;
font-size: 0.95rem;
}
.query-controls {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
flex-wrap: wrap;
}
.product-input {
flex: 1;
min-width: 150px;
padding: 0.75rem;
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
font-size: 0.9rem;
}
.query-btn {
padding: 0.75rem 1.5rem;
background: var(--vp-c-brand);
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-weight: 600;
transition: all 0.2s;
}
.query-btn:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.query-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.reset-btn {
padding: 0.75rem 1.5rem;
background: var(--vp-c-bg);
color: var(--vp-c-text-1);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
cursor: pointer;
font-weight: 600;
transition: all 0.2s;
}
.query-result {
padding: 1rem;
background: var(--vp-c-bg-soft);
border-radius: 8px;
margin-bottom: 1rem;
border-left: 4px solid var(--vp-c-brand);
}
.result-header {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 1rem;
}
.result-icon {
font-size: 1.5rem;
}
.result-title {
font-weight: 700;
font-size: 1rem;
}
.result-details {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.detail-item {
display: flex;
justify-content: space-between;
padding: 0.5rem;
background: var(--vp-c-bg);
border-radius: 6px;
}
.detail-label {
font-weight: 600;
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
.detail-value {
font-size: 0.9rem;
font-weight: 600;
}
.detail-value.source.local {
color: #22c55e;
}
.detail-value.source.redis {
color: #f59e0b;
}
.detail-value.source.database {
color: #ef4444;
}
.detail-value.source.notfound {
color: #94a3b8;
}
.query-flow {
margin-top: 1rem;
}
.flow-title {
font-weight: 600;
margin-bottom: 0.75rem;
font-size: 0.9rem;
}
.flow-steps {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.flow-step {
display: grid;
grid-template-columns: 1fr 1fr 60px;
gap: 0.5rem;
padding: 0.5rem;
border-radius: 6px;
font-size: 0.85rem;
}
.flow-step.hit {
background: #f0fdf4;
}
.flow-step.miss {
background: #fef2f2;
}
.step-level {
font-weight: 600;
}
.step-result {
text-align: center;
}
.step-time {
text-align: right;
font-weight: 600;
color: var(--vp-c-text-2);
}
.metrics-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.75rem;
margin-bottom: 1.5rem;
}
.metric-card {
background: var(--vp-c-bg-soft);
padding: 1rem;
border-radius: 8px;
text-align: center;
}
.metric-label {
font-size: 0.75rem;
color: var(--vp-c-text-2);
margin-bottom: 0.25rem;
}
.metric-value {
font-size: 1.5rem;
font-weight: 700;
}
.metric-value.local {
color: #22c55e;
}
.metric-value.redis {
color: #f59e0b;
}
.metric-value.db {
color: #ef4444;
}
.hit-rate-display {
background: var(--vp-c-bg-soft);
padding: 1rem;
border-radius: 8px;
margin-bottom: 1rem;
}
.rate-label {
font-size: 0.8rem;
color: var(--vp-c-text-2);
margin-bottom: 0.5rem;
}
.rate-value {
font-size: 2rem;
font-weight: 700;
text-align: center;
margin-bottom: 0.5rem;
}
.rate-bar {
height: 12px;
background: var(--vp-c-bg);
border-radius: 999px;
overflow: hidden;
margin-bottom: 0.5rem;
}
.rate-fill {
height: 100%;
background: linear-gradient(90deg, #22c55e, #16a34a);
transition: width 0.5s;
}
.rate-target {
text-align: center;
font-size: 0.75rem;
color: var(--vp-c-text-2);
}
.cache-stats-detail {
background: var(--vp-c-bg-soft);
padding: 1rem;
border-radius: 8px;
}
.stats-title {
font-weight: 600;
margin-bottom: 0.75rem;
font-size: 0.85rem;
}
.stats-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.stat-item {
display: flex;
justify-content: space-between;
font-size: 0.8rem;
}
.stat-label {
color: var(--vp-c-text-2);
}
.stat-value {
font-weight: 600;
}
.features {
background: var(--vp-c-bg);
padding: 1.5rem;
border-radius: 10px;
margin-bottom: 2rem;
border: 1px solid var(--vp-c-divider);
}
.feature-title {
font-weight: 600;
margin-bottom: 1rem;
font-size: 0.95rem;
}
.feature-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
}
.feature-item {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
padding: 1rem;
background: var(--vp-c-bg-soft);
border-radius: 8px;
}
.feature-icon {
font-size: 2rem;
margin-bottom: 0.5rem;
}
.feature-name {
font-weight: 600;
margin-bottom: 0.5rem;
font-size: 0.9rem;
}
.feature-desc {
font-size: 0.8rem;
color: var(--vp-c-text-2);
line-height: 1.5;
}
.code-preview {
background: var(--vp-c-bg);
padding: 1.5rem;
border-radius: 10px;
border: 1px solid var(--vp-c-divider);
}
.code-title {
font-weight: 600;
margin-bottom: 1rem;
font-size: 0.95rem;
}
.code-block {
background: #1e293b;
color: #e2e8f0;
padding: 1rem;
border-radius: 8px;
overflow-x: auto;
font-size: 0.8rem;
line-height: 1.6;
}
</style>