Files
test-repo/docs/.vitepress/theme/components/appendix/queue-design/SeckillSystemDemo.vue
T

878 lines
18 KiB
Vue
Raw Normal View History

<!--
SeckillSystemDemo.vue
秒杀系统架构演示 - 完整的 MQ 应用场景
-->
<template>
<div class="seckill-demo">
<div class="header">
<div class="title">
秒杀系统消息队列的典型应用
</div>
<div class="subtitle">
处理 10 /秒的并发请求保证不超卖
</div>
</div>
<div class="scenario-settings">
<div class="setting">
<label>
商品库存
<strong>{{ stock }}</strong>
</label>
<input
v-model="stock"
type="range"
min="10"
max="1000"
step="10"
>
</div>
<div class="setting">
<label>
请求速率
<strong>{{ requestRate }}</strong>
请求/
</label>
<input
v-model="requestRate"
type="range"
min="100"
max="10000"
step="100"
>
</div>
<div class="setting">
<label>
订单处理
<strong>{{ processRate }}</strong>
订单/
</label>
<input
v-model="processRate"
type="range"
min="50"
max="500"
step="10"
>
</div>
</div>
<div class="action-bar">
<button
class="start-btn"
:disabled="running"
@click="startSeckill"
>
🚀 开始秒杀
</button>
<button
class="reset-btn"
@click="reset"
>
🔄 重置
</button>
</div>
<div class="architecture">
<div class="arch-layer gateway">
<div class="layer-title">
🌐 网关层 - 限流
</div>
<div class="layer-content">
<div class="stat-box">
<div class="stat-label">
总请求数
</div>
<div class="stat-value">
{{ totalRequests.toLocaleString() }}
</div>
</div>
<div class="stat-box">
<div class="stat-label">
限流通过
</div>
<div class="stat-value success">
{{ passedRequests.toLocaleString() }}
</div>
</div>
<div class="stat-box">
<div class="stat-label">
被拒绝
</div>
<div class="stat-value error">
{{ rejectedRequests.toLocaleString() }}
</div>
</div>
</div>
</div>
<div class="arch-arrow">
</div>
<div class="arch-layer redis">
<div class="layer-title">
Redis 预扣库存
</div>
<div class="layer-content">
<div class="stock-display">
<div class="stock-bar">
<div
class="stock-fill"
:style="{ width: stockPercent + '%' }"
/>
</div>
<div class="stock-text">
剩余: {{ remainingStock }} / {{ stock }}
</div>
</div>
<div
class="redis-status"
:class="redisStatus.class"
>
{{ redisStatus.text }}
</div>
</div>
</div>
<div class="arch-arrow">
</div>
<div class="arch-layer queue">
<div class="layer-title">
📦 消息队列缓冲
</div>
<div class="layer-content">
<div class="queue-visual">
<div class="queue-box">
<div class="queue-header">
<span>秒杀订单队列</span>
<span class="queue-count">{{ queueLength }}</span>
</div>
<div class="queue-bar-container">
<div
class="queue-bar"
:class="queueStatus"
:style="{ width: queuePercent + '%' }"
/>
</div>
</div>
</div>
</div>
</div>
<div class="arch-arrow">
</div>
<div class="arch-layer consumer">
<div class="layer-title">
订单服务处理
</div>
<div class="layer-content">
<div class="stat-box">
<div class="stat-label">
处理中
</div>
<div class="stat-value">
{{ processing }}
</div>
</div>
<div class="stat-box">
<div class="stat-label">
成功订单
</div>
<div class="stat-value success">
{{ successOrders }}
</div>
</div>
<div class="stat-box">
<div class="stat-label">
失败订单
</div>
<div class="stat-value error">
{{ failedOrders }}
</div>
</div>
</div>
</div>
</div>
<div class="real-time-stats">
<div class="stats-title">
📊 实时监控
</div>
<div class="stats-grid">
<div class="stat-item">
<div class="stat-label">
平均响应时间
</div>
<div class="stat-value">
{{ avgLatency }}ms
</div>
</div>
<div class="stat-item">
<div class="stat-label">
订单成功率
</div>
<div class="stat-value">
{{ orderSuccessRate }}%
</div>
</div>
<div class="stat-item">
<div class="stat-label">
队列积压
</div>
<div class="stat-value">
{{ queueLength }}
</div>
</div>
<div class="stat-item">
<div class="stat-label">
预计清空时间
</div>
<div class="stat-value">
{{ estimatedTime }}
</div>
</div>
</div>
</div>
<div class="log-section">
<div class="log-header">
<div class="log-title">
📋 事件日志
</div>
<button
class="clear-log"
@click="clearLogs"
>
清空
</button>
</div>
<div class="log-content">
<div
v-if="logs.length === 0"
class="log-empty"
>
暂无日志
</div>
<div
v-for="(log, index) in logs.slice(0, 15)"
:key="index"
class="log-entry"
:class="log.type"
>
<span class="log-time">{{ log.time }}</span>
<span class="log-msg">{{ log.message }}</span>
</div>
</div>
</div>
<div class="key-points">
<div class="point-title">
🎯 核心设计要点
</div>
<div class="point-list">
<div class="point-item">
<span class="point-icon">1</span>
<div>
<strong>网关限流</strong>只放行系统能处理的请求数 1
/避免打爆后端
</div>
</div>
<div class="point-item">
<span class="point-icon">2</span>
<div>
<strong>Redis 预扣</strong>原子操作扣减库存快速判断是否有货避免无效请求
</div>
</div>
<div class="point-item">
<span class="point-icon">3</span>
<div>
<strong>消息队列</strong>将成功的扣库存请求放入队列异步处理削峰填谷
</div>
</div>
<div class="point-item">
<span class="point-icon">4</span>
<div>
<strong>异步处理</strong>订单服务慢慢消费队列创建订单保证不超卖
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onUnmounted } from 'vue'
const stock = ref(100)
const requestRate = ref(5000)
const processRate = ref(200)
const running = ref(false)
const totalRequests = ref(0)
const passedRequests = ref(0)
const rejectedRequests = ref(0)
const remainingStock = ref(100)
const queueLength = ref(0)
const processing = ref(0)
const successOrders = ref(0)
const failedOrders = ref(0)
const logs = ref([])
let simulationInterval = null
let processInterval = null
const stockPercent = computed(() => {
if (stock.value === 0) return 0
return Math.round((remainingStock.value / stock.value) * 100)
})
const redisStatus = computed(() => {
if (remainingStock.value === 0) {
return { text: '🔴 已售罄', class: 'soldout' }
}
if (stockPercent.value < 20) {
return { text: '⚠️ 库存紧张', class: 'low' }
}
return { text: '✅ 库存充足', class: 'normal' }
})
const queuePercent = computed(() => {
const maxQueue = 5000
return Math.min(100, Math.round((queueLength.value / maxQueue) * 100))
})
const queueStatus = computed(() => {
if (queuePercent.value >= 80) return 'critical'
if (queuePercent.value >= 50) return 'warning'
return 'normal'
})
const avgLatency = computed(() => {
return 15 + Math.floor(queueLength.value / 100)
})
const orderSuccessRate = computed(() => {
const total = successOrders.value + failedOrders.value
if (total === 0) return 0
return Math.round((successOrders.value / total) * 100)
})
const estimatedTime = computed(() => {
if (queueLength.value === 0 || processRate.value === 0) return '0s'
const seconds = Math.ceil(queueLength.value / (processRate.value / 10))
if (seconds < 60) return `${seconds}s`
return `${Math.ceil(seconds / 60)}m`
})
const startSeckill = () => {
if (running.value) return
running.value = true
remainingStock.value = stock.value
successOrders.value = 0
failedOrders.value = 0
addLog(
'info',
`🚀 秒杀开始!库存: ${stock.value}, 请求速率: ${requestRate.value}/s`
)
simulationInterval = setInterval(() => {
const requests = Math.floor(requestRate.value / 10)
totalRequests.value += requests
// 网关限流:只放行 80%
const passed = Math.floor(requests * 0.8)
const rejected = requests - passed
passedRequests.value += passed
rejectedRequests.value += rejected
// Redis 预扣库存
let successfulPreDeduct = 0
for (let i = 0; i < passed; i++) {
if (remainingStock.value > 0) {
remainingStock.value--
queueLength.value++
successfulPreDeduct++
}
}
if (remainingStock.value === 0 && stock.value > 0) {
addLog('error', '🔴 商品已售罄!')
}
if (successfulPreDeduct > 0 && Math.random() < 0.1) {
addLog('info', `${successfulPreDeduct} 个请求预扣成功,进入队列`)
}
}, 100)
processInterval = setInterval(() => {
if (queueLength.value > 0) {
const toProcess = Math.min(
Math.floor(processRate.value / 10),
queueLength.value
)
queueLength.value -= toProcess
processing.value = toProcess
setTimeout(() => {
processing.value = 0
// 90% 成功率
const success = Math.floor(toProcess * 0.9)
const failed = toProcess - success
successOrders.value += success
failedOrders.value += failed
if (success > 0) {
addLog('success', `✅ 创建 ${success} 个订单`)
}
if (failed > 0) {
addLog('warning', `⚠️ ${failed} 个订单创建失败(库存不足)`)
}
}, 200)
}
}, 100)
}
const reset = () => {
stopSimulation()
totalRequests.value = 0
passedRequests.value = 0
rejectedRequests.value = 0
remainingStock.value = stock.value
queueLength.value = 0
processing.value = 0
successOrders.value = 0
failedOrders.value = 0
logs.value = []
}
const stopSimulation = () => {
running.value = false
if (simulationInterval) {
clearInterval(simulationInterval)
simulationInterval = null
}
if (processInterval) {
clearInterval(processInterval)
processInterval = null
}
}
const addLog = (type, message) => {
const now = new Date()
const time = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}:${now.getSeconds().toString().padStart(2, '0')}`
logs.value.unshift({ type, time, message })
if (logs.value.length > 50) {
logs.value = logs.value.slice(0, 50)
}
}
const clearLogs = () => {
logs.value = []
}
onUnmounted(() => {
stopSimulation()
})
</script>
<style scoped>
.seckill-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;
margin-top: 0.25rem;
}
.scenario-settings {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
margin-bottom: 1rem;
}
.setting label {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.85rem;
margin-bottom: 0.4rem;
}
.setting input[type='range'] {
width: 100%;
}
.action-bar {
display: flex;
gap: 0.75rem;
justify-content: center;
margin: 0.5rem 0;
}
.start-btn,
.reset-btn {
padding: 0.75rem 2rem;
border: none;
border-radius: 6px;
cursor: pointer;
font-weight: 600;
transition: all 0.2s;
}
.start-btn {
background: var(--vp-c-brand);
color: white;
}
.start-btn:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
}
.start-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.reset-btn {
background: var(--vp-c-text-2);
color: white;
}
.architecture {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
margin: 1.5rem 0;
}
.arch-layer {
width: 100%;
max-width: 600px;
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 10px;
padding: 0.75rem;
}
.layer-title {
font-weight: 600;
font-size: 0.9rem;
margin-bottom: 0.75rem;
text-align: center;
}
.layer-content {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
gap: 0.75rem;
}
.stat-box {
background: var(--vp-c-bg-soft);
border-radius: 6px;
padding: 0.75rem;
text-align: center;
}
.stat-label {
font-size: 0.75rem;
color: var(--vp-c-text-2);
margin-bottom: 0.35rem;
}
.stat-value {
font-size: 1.1rem;
font-weight: 700;
}
.stat-value.success {
color: #22c55e;
}
.stat-value.error {
color: #ef4444;
}
.arch-arrow {
font-size: 1.5rem;
color: var(--vp-c-text-3);
}
.stock-display {
grid-column: 1 / -1;
padding: 0.75rem;
background: var(--vp-c-bg-soft);
border-radius: 6px;
}
.stock-bar {
height: 20px;
background: var(--vp-c-bg);
border-radius: 10px;
overflow: hidden;
margin-bottom: 0.5rem;
}
.stock-fill {
height: 100%;
background: linear-gradient(90deg, #22c55e, #16a34a);
transition: width 0.3s ease;
}
.stock-text {
text-align: center;
font-size: 0.85rem;
font-weight: 600;
}
.redis-status {
grid-column: 1 / -1;
padding: 0.5rem;
border-radius: 6px;
text-align: center;
font-size: 0.85rem;
font-weight: 600;
margin-top: 0.5rem;
}
.redis-status.normal {
background: rgba(34, 197, 94, 0.1);
color: #16a34a;
}
.redis-status.low {
background: rgba(245, 158, 11, 0.1);
color: #d97706;
}
.redis-status.soldout {
background: rgba(239, 68, 68, 0.1);
color: #dc2626;
}
.queue-visual {
grid-column: 1 / -1;
}
.queue-box {
background: var(--vp-c-bg-soft);
border-radius: 6px;
padding: 0.75rem;
}
.queue-header {
display: flex;
justify-content: space-between;
font-size: 0.8rem;
font-weight: 600;
margin-bottom: 0.5rem;
}
.queue-bar-container {
height: 24px;
background: var(--vp-c-bg);
border-radius: 12px;
overflow: hidden;
}
.queue-bar {
height: 100%;
transition:
width 0.3s ease,
background 0.3s ease;
}
.queue-bar.normal {
background: linear-gradient(90deg, #22c55e, #16a34a);
}
.queue-bar.warning {
background: linear-gradient(90deg, #f59e0b, #d97706);
}
.queue-bar.critical {
background: linear-gradient(90deg, #ef4444, #dc2626);
}
.real-time-stats {
background: var(--vp-c-bg);
border-radius: 10px;
padding: 0.75rem;
margin: 0.5rem 0;
border: 1px solid var(--vp-c-divider);
}
.stats-title {
font-weight: 600;
margin-bottom: 0.75rem;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 0.75rem;
}
.stat-item {
text-align: center;
padding: 0.75rem;
background: var(--vp-c-bg-soft);
border-radius: 6px;
}
.stat-item .stat-label {
font-size: 0.75rem;
color: var(--vp-c-text-2);
margin-bottom: 0.35rem;
}
.stat-item .stat-value {
font-size: 1.1rem;
font-weight: 700;
color: var(--vp-c-text-1);
}
.log-section {
background: var(--vp-c-bg);
border-radius: 10px;
padding: 0.75rem;
margin: 0.5rem 0;
border: 1px solid var(--vp-c-divider);
}
.log-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.75rem;
}
.log-title {
font-weight: 600;
font-size: 0.9rem;
}
.clear-log {
padding: 0.35rem 0.75rem;
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
cursor: pointer;
font-size: 0.8rem;
}
.log-content {
max-height: 300px;
background: var(--vp-c-bg-soft);
border-radius: 6px;
padding: 0.75rem;
font-size: 0.8rem;
}
.log-empty {
text-align: center;
color: var(--vp-c-text-3);
padding: 0.75rem;
}
.log-entry {
display: flex;
gap: 0.5rem;
padding: 0.35rem 0;
border-bottom: 1px solid var(--vp-c-divider);
}
.log-entry:last-child {
border-bottom: none;
}
.log-time {
color: var(--vp-c-text-3);
font-family: monospace;
}
.log-msg {
flex: 1;
}
.log-entry.info .log-msg {
color: var(--vp-c-text-1);
}
.log-entry.success .log-msg {
color: #16a34a;
}
.log-entry.warning .log-msg {
color: #d97706;
}
.log-entry.error .log-msg {
color: #dc2626;
}
.key-points {
background: rgba(59, 130, 246, 0.1);
border-radius: 10px;
padding: 0.75rem;
border: 1px solid rgba(59, 130, 246, 0.3);
}
.point-title {
font-weight: 600;
margin-bottom: 0.75rem;
}
.point-list {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.point-item {
display: flex;
gap: 0.75rem;
align-items: flex-start;
}
.point-icon {
font-size: 1.2rem;
flex-shrink: 0;
}
.point-item div {
font-size: 0.9rem;
line-height: 1.5;
}
</style>