2026-01-18 12:21:49 +08:00
|
|
|
|
<!--
|
|
|
|
|
|
CacheProblemsDemo.vue
|
|
|
|
|
|
缓存三大问题演示 - 缓存穿透、缓存击穿、缓存雪崩
|
|
|
|
|
|
-->
|
|
|
|
|
|
<template>
|
|
|
|
|
|
<div class="cache-problems-demo">
|
|
|
|
|
|
<div class="header">
|
2026-02-18 17:38:10 +08:00
|
|
|
|
<div class="title">
|
|
|
|
|
|
缓存的三大问题
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="subtitle">
|
|
|
|
|
|
穿透、击穿、雪崩的场景与解决方案
|
|
|
|
|
|
</div>
|
2026-01-18 12:21:49 +08:00
|
|
|
|
</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">
|
|
|
|
|
|
<!-- 缓存穿透 -->
|
2026-02-18 17:38:10 +08:00
|
|
|
|
<div
|
|
|
|
|
|
v-if="activeProblem === 'penetration'"
|
|
|
|
|
|
class="problem-detail"
|
|
|
|
|
|
>
|
2026-01-18 12:21:49 +08:00
|
|
|
|
<div class="problem-intro">
|
2026-02-18 17:38:10 +08:00
|
|
|
|
<div class="intro-title">
|
|
|
|
|
|
什么是缓存穿透?
|
|
|
|
|
|
</div>
|
2026-01-18 12:21:49 +08:00
|
|
|
|
<div class="intro-text">
|
|
|
|
|
|
查询一个<strong>不存在的数据</strong>(如恶意请求
|
|
|
|
|
|
id=-1),缓存没有,数据库也没有。 导致每次请求都直接打到数据库。
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="problem-scenario">
|
2026-02-18 17:38:10 +08:00
|
|
|
|
<div class="scenario-title">
|
|
|
|
|
|
场景模拟
|
|
|
|
|
|
</div>
|
2026-01-18 12:21:49 +08:00
|
|
|
|
<div class="scenario-diagram">
|
|
|
|
|
|
<div class="flow-item request">
|
2026-02-18 17:38:10 +08:00
|
|
|
|
<div class="flow-icon">
|
|
|
|
|
|
🔥
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="flow-text">
|
|
|
|
|
|
请求 id=-999
|
|
|
|
|
|
</div>
|
2026-01-18 12:21:49 +08:00
|
|
|
|
</div>
|
2026-02-18 17:38:10 +08:00
|
|
|
|
<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">
|
|
|
|
|
|
↓
|
2026-01-18 12:21:49 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div
|
|
|
|
|
|
class="flow-item database"
|
|
|
|
|
|
:class="{ overloaded: dbPressure >= 80 }"
|
|
|
|
|
|
>
|
2026-02-18 17:38:10 +08:00
|
|
|
|
<div class="flow-icon">
|
|
|
|
|
|
🗄️
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="flow-text">
|
|
|
|
|
|
数据库查询(不存在)
|
|
|
|
|
|
</div>
|
2026-01-18 12:21:49 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="controls">
|
|
|
|
|
|
<button
|
|
|
|
|
|
class="attack-btn"
|
|
|
|
|
|
:disabled="simulating"
|
2026-02-18 17:38:10 +08:00
|
|
|
|
@click="simulatePenetration"
|
2026-01-18 12:21:49 +08:00
|
|
|
|
>
|
|
|
|
|
|
{{ simulating ? '攻击中...' : '模拟恶意攻击' }}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="pressure-meter">
|
2026-02-18 17:38:10 +08:00
|
|
|
|
<div class="meter-label">
|
|
|
|
|
|
数据库压力
|
|
|
|
|
|
</div>
|
2026-01-18 12:21:49 +08:00
|
|
|
|
<div class="meter-bar">
|
|
|
|
|
|
<div
|
|
|
|
|
|
class="meter-fill"
|
|
|
|
|
|
:style="{ width: dbPressure + '%' }"
|
2026-02-18 17:38:10 +08:00
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="meter-value">
|
|
|
|
|
|
{{ dbPressure }}%
|
2026-01-18 12:21:49 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="solutions">
|
2026-02-18 17:38:10 +08:00
|
|
|
|
<div class="solutions-title">
|
|
|
|
|
|
解决方案
|
|
|
|
|
|
</div>
|
2026-01-18 12:21:49 +08:00
|
|
|
|
<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 肯定不存在"。
|
2026-02-18 17:38:10 +08:00
|
|
|
|
<br>
|
2026-01-18 12:21:49 +08:00
|
|
|
|
<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>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 缓存击穿 -->
|
2026-02-18 17:38:10 +08:00
|
|
|
|
<div
|
|
|
|
|
|
v-if="activeProblem === 'breakdown'"
|
|
|
|
|
|
class="problem-detail"
|
|
|
|
|
|
>
|
2026-01-18 12:21:49 +08:00
|
|
|
|
<div class="problem-intro">
|
2026-02-18 17:38:10 +08:00
|
|
|
|
<div class="intro-title">
|
|
|
|
|
|
什么是缓存击穿?
|
|
|
|
|
|
</div>
|
2026-01-18 12:21:49 +08:00
|
|
|
|
<div class="intro-text">
|
|
|
|
|
|
某个<strong>热点数据</strong>过期(如微博热搜),瞬间几百万请求同时打到数据库。
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="problem-scenario">
|
2026-02-18 17:38:10 +08:00
|
|
|
|
<div class="scenario-title">
|
|
|
|
|
|
场景模拟
|
|
|
|
|
|
</div>
|
2026-01-18 12:21:49 +08:00
|
|
|
|
<div class="hotkey-scenario">
|
|
|
|
|
|
<div class="hotkey-badge">
|
|
|
|
|
|
🔥 热点数据
|
2026-02-18 17:38:10 +08:00
|
|
|
|
<br>
|
2026-01-18 12:21:49 +08:00
|
|
|
|
<span class="key">user:12345</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="concurrent-requests">
|
2026-02-18 17:38:10 +08:00
|
|
|
|
<div class="requests-title">
|
|
|
|
|
|
并发请求
|
|
|
|
|
|
</div>
|
2026-01-18 12:21:49 +08:00
|
|
|
|
<div class="requests-container">
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-for="(req, index) in concurrentRequests"
|
|
|
|
|
|
:key="index"
|
|
|
|
|
|
class="request-item"
|
|
|
|
|
|
:class="req.status"
|
|
|
|
|
|
>
|
2026-02-18 17:38:10 +08:00
|
|
|
|
<div class="request-id">
|
|
|
|
|
|
请求 {{ req.id }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="request-status">
|
|
|
|
|
|
{{ req.statusText }}
|
|
|
|
|
|
</div>
|
2026-01-18 12:21:49 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-02-18 17:38:10 +08:00
|
|
|
|
<div
|
|
|
|
|
|
v-if="showMutex"
|
|
|
|
|
|
class="mutex-visual"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="mutex-badge">
|
|
|
|
|
|
🔒 互斥锁
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="mutex-text">
|
|
|
|
|
|
只有一个线程能查数据库
|
|
|
|
|
|
</div>
|
2026-01-18 12:21:49 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="controls">
|
|
|
|
|
|
<button
|
|
|
|
|
|
class="attack-btn"
|
|
|
|
|
|
:disabled="simulating"
|
2026-02-18 17:38:10 +08:00
|
|
|
|
@click="simulateBreakdown"
|
2026-01-18 12:21:49 +08:00
|
|
|
|
>
|
|
|
|
|
|
{{ simulating ? '模拟中...' : '模拟热点过期' }}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="solutions">
|
2026-02-18 17:38:10 +08:00
|
|
|
|
<div class="solutions-title">
|
|
|
|
|
|
解决方案
|
|
|
|
|
|
</div>
|
2026-01-18 12:21:49 +08:00
|
|
|
|
<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">
|
|
|
|
|
|
只允许一个线程查数据库,其他线程等待。
|
2026-02-18 17:38:10 +08:00
|
|
|
|
<br>
|
2026-01-18 12:21:49 +08:00
|
|
|
|
<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 里存一个过期时间字段。
|
2026-02-18 17:38:10 +08:00
|
|
|
|
<br>
|
|
|
|
|
|
<span class="note">查询时发现"逻辑过期",异步更新缓存,同时返回旧数据</span>
|
2026-01-18 12:21:49 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 缓存雪崩 -->
|
2026-02-18 17:38:10 +08:00
|
|
|
|
<div
|
|
|
|
|
|
v-if="activeProblem === 'avalanche'"
|
|
|
|
|
|
class="problem-detail"
|
|
|
|
|
|
>
|
2026-01-18 12:21:49 +08:00
|
|
|
|
<div class="problem-intro">
|
2026-02-18 17:38:10 +08:00
|
|
|
|
<div class="intro-title">
|
|
|
|
|
|
什么是缓存雪崩?
|
|
|
|
|
|
</div>
|
2026-01-18 12:21:49 +08:00
|
|
|
|
<div class="intro-text">
|
|
|
|
|
|
大量缓存<strong>同时过期</strong>(如系统重启后,所有缓存都在
|
|
|
|
|
|
00:00:00 过期), 数据库瞬间被打爆。
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="problem-scenario">
|
2026-02-18 17:38:10 +08:00
|
|
|
|
<div class="scenario-title">
|
|
|
|
|
|
场景模拟
|
|
|
|
|
|
</div>
|
2026-01-18 12:21:49 +08:00
|
|
|
|
<div class="avalanche-visual">
|
|
|
|
|
|
<div class="cache-items">
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-for="(item, index) in cacheItems"
|
|
|
|
|
|
:key="index"
|
|
|
|
|
|
class="cache-item"
|
|
|
|
|
|
:class="{ expired: item.expired }"
|
|
|
|
|
|
>
|
2026-02-18 17:38:10 +08:00
|
|
|
|
<div class="item-key">
|
|
|
|
|
|
{{ item.key }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="item-ttl">
|
|
|
|
|
|
TTL: {{ item.ttl }}s
|
|
|
|
|
|
</div>
|
2026-01-18 12:21:49 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-02-18 17:38:10 +08:00
|
|
|
|
<div
|
|
|
|
|
|
v-if="massExplosion"
|
|
|
|
|
|
class="mass-explosion"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="explosion-icon">
|
|
|
|
|
|
💥
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="explosion-text">
|
|
|
|
|
|
同时过期!
|
|
|
|
|
|
</div>
|
2026-01-18 12:21:49 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-02-18 17:38:10 +08:00
|
|
|
|
<div
|
|
|
|
|
|
class="db-overload"
|
|
|
|
|
|
:class="{ critical: dbPressure >= 90 }"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="db-icon">
|
|
|
|
|
|
🗄️
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="db-status">
|
|
|
|
|
|
数据库负载: {{ dbPressure }}%
|
|
|
|
|
|
</div>
|
2026-01-18 12:21:49 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="controls">
|
|
|
|
|
|
<button
|
|
|
|
|
|
class="attack-btn"
|
|
|
|
|
|
:disabled="simulating"
|
2026-02-18 17:38:10 +08:00
|
|
|
|
@click="simulateAvalanche"
|
2026-01-18 12:21:49 +08:00
|
|
|
|
>
|
|
|
|
|
|
{{ simulating ? '模拟中...' : '模拟缓存雪崩' }}
|
|
|
|
|
|
</button>
|
2026-02-18 17:38:10 +08:00
|
|
|
|
<button
|
|
|
|
|
|
class="solution-btn"
|
|
|
|
|
|
@click="applyRandomTTL"
|
|
|
|
|
|
>
|
2026-01-18 12:21:49 +08:00
|
|
|
|
应用解决方案(随机 TTL)
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="solutions">
|
2026-02-18 17:38:10 +08:00
|
|
|
|
<div class="solutions-title">
|
|
|
|
|
|
解决方案
|
|
|
|
|
|
</div>
|
2026-01-18 12:21:49 +08:00
|
|
|
|
<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 加上随机值。
|
2026-02-18 17:38:10 +08:00
|
|
|
|
<br>
|
|
|
|
|
|
<span class="code">ttl = 600 + random.randint(-60, 60) # 600 ± 60 秒</span>
|
2026-01-18 12:21:49 +08:00
|
|
|
|
</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">
|
|
|
|
|
|
系统启动时,主动加载热点数据到缓存。
|
2026-02-18 17:38:10 +08:00
|
|
|
|
<br>
|
|
|
|
|
|
<span class="note">使用定时任务,提前刷新即将过期的热点数据</span>
|
2026-01-18 12:21:49 +08:00
|
|
|
|
</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">
|
|
|
|
|
|
当数据库压力过大时,暂时停止更新缓存,直接返回降级数据。
|
2026-02-18 17:38:10 +08:00
|
|
|
|
<br>
|
2026-01-18 12:21:49 +08:00
|
|
|
|
<span class="note">如"系统繁忙,请稍后再试"</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="comparison-table">
|
2026-02-18 17:38:10 +08:00
|
|
|
|
<div class="table-title">
|
|
|
|
|
|
三大问题对比
|
|
|
|
|
|
</div>
|
2026-01-18 12:21:49 +08:00
|
|
|
|
<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;
|
2026-02-14 20:23:34 +08:00
|
|
|
|
padding: 0.75rem;
|
2026-01-18 12:21:49 +08:00
|
|
|
|
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);
|
2026-02-14 20:23:34 +08:00
|
|
|
|
border-radius: 6px;
|
2026-01-18 12:21:49 +08:00
|
|
|
|
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;
|
2026-02-14 20:23:34 +08:00
|
|
|
|
border-radius: 6px;
|
2026-01-18 12:21:49 +08:00
|
|
|
|
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;
|
2026-02-14 20:23:34 +08:00
|
|
|
|
border-radius: 6px;
|
2026-01-18 12:21:49 +08:00
|
|
|
|
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;
|
2026-02-14 20:23:34 +08:00
|
|
|
|
padding: 0.75rem;
|
2026-01-18 12:21:49 +08:00
|
|
|
|
background: #fef3c7;
|
2026-02-14 20:23:34 +08:00
|
|
|
|
border-radius: 6px;
|
2026-01-18 12:21:49 +08:00
|
|
|
|
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);
|
2026-02-14 20:23:34 +08:00
|
|
|
|
padding: 0.75rem;
|
|
|
|
|
|
border-radius: 6px;
|
2026-01-18 12:21:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.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;
|
2026-02-14 20:23:34 +08:00
|
|
|
|
padding: 0.75rem;
|
2026-01-18 12:21:49 +08:00
|
|
|
|
background: #eff6ff;
|
2026-02-14 20:23:34 +08:00
|
|
|
|
border-radius: 6px;
|
2026-01-18 12:21:49 +08:00
|
|
|
|
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;
|
2026-02-14 20:23:34 +08:00
|
|
|
|
padding: 0.75rem;
|
2026-01-18 12:21:49 +08:00
|
|
|
|
background: #fef2f2;
|
2026-02-14 20:23:34 +08:00
|
|
|
|
border-radius: 6px;
|
2026-01-18 12:21:49 +08:00
|
|
|
|
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;
|
2026-02-14 20:23:34 +08:00
|
|
|
|
padding: 0.75rem;
|
2026-01-18 12:21:49 +08:00
|
|
|
|
background: var(--vp-c-bg-soft);
|
2026-02-14 20:23:34 +08:00
|
|
|
|
border-radius: 6px;
|
2026-01-18 12:21:49 +08:00
|
|
|
|
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 {
|
2026-02-14 20:23:34 +08:00
|
|
|
|
padding: 0.75rem;
|
2026-01-18 12:21:49 +08:00
|
|
|
|
background: var(--vp-c-bg-soft);
|
2026-02-14 20:23:34 +08:00
|
|
|
|
border-radius: 6px;
|
2026-01-18 12:21:49 +08:00
|
|
|
|
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>
|