feat: update docs and components, fix DLQ demo bug
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user