<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Watermill项目Redis消息队列支持深度调研</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,700;1,400&family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<script src="https://cdn.jsdelivr.net/npm/mermaid@10.6.1/dist/mermaid.min.js"></script>
<style>
:root {
--sage: #9CA986;
--clay: #B5A082;
--stone: #8B7D6B;
--cream: #F7F3E9;
--charcoal: #2C2C2C;
--mist: #E8E6E1;
}
body {
font-family: 'Inter', sans-serif;
background: linear-gradient(135deg, var(--cream) 0%, var(--mist) 100%);
color: var(--charcoal);
line-height: 1.7;
}
.serif {
font-family: 'Playfair Display', serif;
}
.hero-gradient {
background: linear-gradient(135deg, var(--sage) 0%, var(--clay) 50%, var(--stone) 100%);
}
.toc-fixed {
position: fixed;
top: 0;
left: 0;
width: 180px;
height: 100vh;
background: rgba(247, 243, 233, 0.95);
backdrop-filter: blur(10px);
border-right: 1px solid var(--sage);
z-index: 1000;
overflow-y: auto;
padding: 2rem 1.5rem;
}
.main-content {
margin-left: 180px;
min-height: 100vh;
}
.section-divider {
height: 1px;
background: linear-gradient(90deg, transparent 0%, var(--sage) 50%, transparent 100%);
margin: 4rem 0;
}
.highlight-box {
background: linear-gradient(135deg, rgba(156, 169, 134, 0.1) 0%, rgba(181, 160, 130, 0.1) 100%);
border-left: 4px solid var(--sage);
}
.citation-link {
color: var(--stone);
text-decoration: none;
border-bottom: 1px dotted var(--stone);
transition: all 0.2s ease;
}
.citation-link:hover {
color: var(--clay);
border-bottom-color: var(--clay);
}
.bento-grid {
display: grid;
grid-template-columns: 2fr 1fr;
grid-template-rows: auto auto;
gap: 2rem;
height: 60vh;
}
.bento-main {
grid-row: 1 / -1;
position: relative;
overflow: hidden;
border-radius: 1rem;
}
.bento-side {
display: flex;
flex-direction: column;
gap: 1rem;
}
.bento-card {
background: rgba(247, 243, 233, 0.8);
backdrop-filter: blur(10px);
border: 1px solid rgba(156, 169, 134, 0.3);
border-radius: 0.75rem;
padding: 1.5rem;
flex: 1;
}
.code-block {
background: var(--charcoal);
color: #E8E6E1;
border-radius: 0.75rem;
padding: 1.5rem;
overflow-x: auto;
font-family: 'Fira Code', 'Courier New', monospace;
font-size: 0.875rem;
line-height: 1.6;
}
.toc-link {
display: block;
padding: 0.5rem 0;
color: var(--stone);
text-decoration: none;
border-bottom: 1px solid transparent;
transition: all 0.2s ease;
}
.toc-link:hover, .toc-link.active {
color: var(--sage);
border-bottom-color: var(--sage);
}
.toc-link.sub {
padding-left: 1rem;
font-size: 0.875rem;
}
.comparison-table {
background: white;
border-radius: 1rem;
overflow: hidden;
box-shadow: 0 4px 20px rgba(0,0,0,0.05);
}
.comparison-table th {
background: var(--sage);
color: white;
padding: 1rem;
font-weight: 600;
}
.comparison-table td {
padding: 1rem;
border-bottom: 1px solid var(--mist);
}
.comparison-table tr:nth-child(even) {
background: rgba(156, 169, 134, 0.05);
}
.mermaid-container {
display: flex;
justify-content: center;
min-height: 300px;
max-height: 800px;
background: #ffffff;
border: 2px solid #e5e7eb;
border-radius: 12px;
padding: 30px;
margin: 30px 0;
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.08);
position: relative;
overflow: hidden;
}
.mermaid-container .mermaid {
width: 100%;
max-width: 100%;
height: 100%;
cursor: grab;
transition: transform 0.3s ease;
transform-origin: center center;
display: flex;
justify-content: center;
align-items: center;
touch-action: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.mermaid-container .mermaid svg {
max-width: 100%;
height: 100%;
display: block;
margin: 0 auto;
}
.mermaid-container .mermaid:active {
cursor: grabbing;
}
.mermaid-container.zoomed .mermaid {
height: 100%;
width: 100%;
cursor: grab;
}
.mermaid-controls {
position: absolute;
top: 15px;
right: 15px;
display: flex;
gap: 10px;
z-index: 20;
background: rgba(255, 255, 255, 0.95);
padding: 8px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.mermaid-control-btn {
background: #ffffff;
border: 1px solid #d1d5db;
border-radius: 6px;
padding: 10px;
cursor: pointer;
transition: all 0.2s ease;
color: #374151;
font-size: 14px;
min-width: 36px;
height: 36px;
text-align: center;
display: flex;
align-items: center;
justify-content: center;
}
.mermaid-control-btn:hover {
background: #f8fafc;
border-color: #3b82f6;
color: #3b82f6;
transform: translateY(-1px);
}
.mermaid-control-btn:active {
transform: scale(0.95);
}
@media (max-width: 1024px) {
.toc-fixed {
display: none;
}
.main-content {
margin-left: 0;
}
.bento-grid {
grid-template-columns: 1fr;
height: auto;
}
.bento-main {
grid-row: auto;
height: 40vh;
}
}
@media (max-width: 768px) {
.hero-gradient h1 {
font-size: 2.5rem;
}
.hero-gradient p {
font-size: 1rem;
}
.bento-card {
padding: 1rem;
}
.bento-card h3 {
font-size: 1.125rem;
}
.bento-card p {
font-size: 0.875rem;
}
.bento-main {
height: auto;
}
.hero-gradient h1 {
word-wrap: break-word;
}
.mermaid-container {
padding: 15px;
}
}
</style>
<base target="_blank">
</head>
<body>
<!-- Fixed Table of Contents -->
<nav class="toc-fixed">
<div class="mb-8">
<h3 class="serif text-xl font-bold text-charcoal mb-4">目录导航</h3>
<div class="w-12 h-0.5 bg-sage"></div>
</div>
<div class="space-y-1">
<a href="#hero" class="toc-link">概览</a>
<a href="#redis-stream" class="toc-link">1. Redis Stream 支持</a>
<a href="#stream-overview" class="toc-link sub">1.1 官方支持与实现库</a>
<a href="#core-features" class="toc-link sub">1.2 核心特性与机制</a>
<a href="#configuration" class="toc-link sub">1.3 配置与使用</a>
<a href="#redis-pubsub" class="toc-link">2. Redis Pub/Sub 支持</a>
<a href="#limitations" class="toc-link sub">2.1 原生 Pub/Sub 的局限性</a>
<a href="#implementation-strategy" class="toc-link sub">2.2 Watermill 的实现策略</a>
<a href="#differences" class="toc-link sub">2.3 与原生 Pub/Sub 的区别</a>
<a href="#redis-list" class="toc-link">3. Redis List 支持</a>
<a href="#official-support" class="toc-link sub">3.1 官方支持情况</a>
<a href="#custom-implementation" class="toc-link sub">3.2 自定义实现探讨</a>
<a href="#implementation-approach" class="toc-link sub">3.3 实现方式分析</a>
</div>
<div class="mt-8 pt-6 border-t border-sage">
<p class="text-xs text-stone">基于 Watermill 官方文档的深度调研报告</p>
</div>
</nav>
<!-- Main Content -->
<main class="main-content">
<!-- Redis Stream Support Section -->
<section id="redis-stream" class="py-16">
<div class="container mx-auto px-6">
<header class="text-center mb-16">
<h2 class="serif text-4xl font-bold text-charcoal mb-6">1. Redis Stream 支持</h2>
<p class="text-xl text-stone max-w-3xl mx-auto leading-relaxed">
Watermill项目对Redis消息队列的支持主要集中在其对Redis Stream的全面实现上,通过专门的库
<code class="bg-mist px-2 py-1 rounded">watermill-redisstream</code>提供强大、持久化的事件驱动能力。
</p>
</header>
<!-- Stream Overview -->
<div id="stream-overview" class="mb-16">
<h3 class="serif text-3xl font-semibold mb-8 text-charcoal">1.1 官方支持与实现库</h3>
<div class="grid lg:grid-cols-2 gap-12 items-start">
<div>
<h4 class="serif text-2xl font-semibold mb-6 text-sage">支持的Pub/Sub类型</h4>
<p class="mb-6">
根据Watermill官方文档和GitHub仓库的说明,Watermill支持多种消息队列系统,其中明确包括了
<strong>Redis Stream</strong>
<a href="https://github.com/ThreeDotsLabs/watermill" class="citation-link">[195]</a>。
在官方列出的支持列表中,Redis Stream与Kafka、RabbitMQ、NATS Jetstream等主流消息队列并列,表明其在Watermill生态系统中的重要地位
<a href="https://blog.csdn.net/gitblog_01199/article/details/152186719" class="citation-link">[192]</a>。
</p>
<div class="bg-white rounded-lg p-6 border border-sage/20 mb-6">
<h5 class="font-semibold mb-3 flex items-center">
<i class="fas fa-info-circle text-sage mr-2"></i>
官方实现特点
</h5>
<ul class="space-y-2 text-sm">
<li class="flex items-start">
<i class="fas fa-check text-green-500 mr-2 mt-1"></i>
<span>基于
<code class="bg-gray-100 px-1 rounded">redis/go-redis</code>库实现
</span>
</li>
<li class="flex items-start">
<i class="fas fa-check text-green-500 mr-2 mt-1"></i>
<span>支持完整的Pub/Sub语义</span>
</li>
<li class="flex items-start">
<i class="fas fa-check text-green-500 mr-2 mt-1"></i>
<span>集成Redis Stream高级特性</span>
</li>
</ul>
</div>
</div>
<div>
<h4 class="serif text-2xl font-semibold mb-6 text-sage">实现库:watermill-redisstream</h4>
<p class="mb-4">
Watermill对Redis Stream的支持是通过一个名为
<strong>watermill-redisstream</strong>的独立Go包实现的
<a href="https://github.com/ThreeDotsLabs/watermill-redisstream" class="citation-link">[194]</a>。
这个包提供了Publisher和Subscriber两个核心组件。
</p>
<div class="code-block mb-4">
<div class="text-green-400 mb-2">// 安装命令</div>
<div>go get github.com/ThreeDotsLabs/watermill-redisstream</div>
</div>
<p class="text-sm text-stone">
该库设计遵循Watermill统一接口规范,可与其他Pub/Sub实现无缝互换,并深度集成Redis Stream特性
<a href="https://pkg.go.dev/github.com/ThreeDotsLabs/watermill-redisstream/pkg/redisstream" class="citation-link">[196]</a>。
</p>
</div>
</div>
</div>
<!-- Core Features -->
<div id="core-features" class="mb-16">
<h3 class="serif text-3xl font-semibold mb-8 text-charcoal">1.2 核心特性与机制</h3>
<!-- Message Persistence -->
<div class="mb-12">
<h4 class="serif text-2xl font-semibold mb-6 flex items-center">
<i class="fas fa-database text-sage mr-3"></i>
消息持久化
</h4>
<div class="grid lg:grid-cols-3 gap-6">
<div class="lg:col-span-2">
<p class="mb-4">
Redis Stream的核心优势是其原生支持消息持久化。与Redis传统的Pub/Sub机制不同,Stream中的消息会被持久化存储在内存中,即使所有消费者都断开连接,消息也不会丢失
<a href="https://blog.csdn.net/Mrxiao_bo/article/details/134262366" class="citation-link">[209]</a>。
</p>
<p class="mb-4">
Watermill的
<code class="bg-mist px-1 rounded">watermill-redisstream</code>实现完全继承了这一特性。当发布者通过
<code class="bg-mist px-1 rounded">Publisher.Publish</code>方法发送消息时,这些消息会被追加到Stream末尾,并分配唯一ID
<a href="https://watermill.io/pubsubs/redisstream/" class="citation-link">[193]</a>。
</p>
</div>
<div class="bg-white rounded-lg p-6 border border-sage/20">
<h5 class="font-semibold mb-3 text-sage">持久化特性</h5>
<div class="space-y-3">
<div class="flex items-center">
<i class="fas fa-check-circle text-green-500 mr-2"></i>
<span class="text-sm">消息持久存储</span>
</div>
<div class="flex items-center">
<i class="fas fa-check-circle text-green-500 mr-2"></i>
<span class="text-sm">支持RDB/AOF持久化</span>
</div>
<div class="flex items-center">
<i class="fas fa-check-circle text-green-500 mr-2"></i>
<span class="text-sm">可配置Stream长度限制</span>
</div>
</div>
</div>
</div>
</div>
<!-- Consumer Groups -->
<div class="mb-12">
<h4 class="serif text-2xl font-semibold mb-6 flex items-center">
<i class="fas fa-users text-sage mr-3"></i>
消费者组 (Consumer Groups)
</h4>
<p class="mb-6">
消费者组是Redis Stream的强大功能,允许多个消费者协同处理同一个Stream中的消息,确保每条消息只被组内一个消费者处理。Watermill的
<code class="bg-mist px-1 rounded">watermill-redisstream</code>实现完全支持消费者组机制
<a href="https://watermill.io/pubsubs/redisstream/" class="citation-link">[3]</a>。
</p>
<div class="bg-white rounded-lg p-6 border border-sage/20">
<h5 class="font-semibold mb-4 text-sage">消费者组优势</h5>
<div class="grid md:grid-cols-2 gap-4">
<div>
<h6 class="font-medium mb-2">负载均衡</h6>
<p class="text-sm text-stone">多个消费者实例自动分配消息处理任务</p>
</div>
<div>
<h6 class="font-medium mb-2">故障转移</h6>
<p class="text-sm text-stone">消费者崩溃时,其他实例可接管未处理消息</p>
</div>
<div>
<h6 class="font-medium mb-2">水平扩展</h6>
<p class="text-sm text-stone">通过增加消费者实例提升处理能力</p>
</div>
<div>
<h6 class="font-medium mb-2">消息唯一性</h6>
<p class="text-sm text-stone">确保每条消息只被处理一次</p>
</div>
</div>
</div>
</div>
<!-- ACK Mechanism -->
<div class="mb-12">
<h4 class="serif text-2xl font-semibold mb-6 flex items-center">
<i class="fas fa-check-square text-sage mr-3"></i>
消息确认 (ACK) 机制
</h4>
<p class="mb-6">
消息确认机制是确保消息被成功处理的关键。在Redis Stream中,当消费者读取消息时,该消息被标记为待处理(pending)。消费者处理完消息后,需要向Redis发送ACK命令确认
<a href="https://watermill.io/pubsubs/redisstream/" class="citation-link">[193]</a>。
</p>
<div class="grid lg:grid-cols-2 gap-8">
<div>
<h5 class="font-semibold mb-4">Watermill中的ACK流程</h5>
<div class="space-y-4">
<div class="flex items-start">
<div class="w-8 h-8 bg-sage rounded-full flex items-center justify-center text-white text-sm font-bold mr-3 mt-1">1</div>
<div>
<p class="font-medium">接收消息</p>
<p class="text-sm text-stone">Subscriber从Stream接收消息并封装为message.Message对象</p>
</div>
</div>
<div class="flex items-start">
<div class="w-8 h-8 bg-sage rounded-full flex items-center justify-center text-white text-sm font-bold mr-3 mt-1">2</div>
<div>
<p class="font-medium">处理消息</p>
<p class="text-sm text-stone">应用逻辑处理消息内容</p>
</div>
</div>
<div class="flex items-start">
<div class="w-8 h-8 bg-sage rounded-full flex items-center justify-center text-white text-sm font-bold mr-3 mt-1">3</div>
<div>
<p class="font-medium">确认消息</p>
<p class="text-sm text-stone">调用
<code class="bg-gray-100 px-1 rounded">msg.Ack()</code>发送确认
</p>
</div>
</div>
<div class="flex items-start">
<div class="w-8 h-8 bg-sage rounded-full flex items-center justify-center text-white text-sm font-bold mr-3 mt-1">4</div>
<div>
<p class="font-medium">完成处理</p>
<p class="text-sm text-stone">Subscriber自动向Redis发送XACK命令</p>
</div>
</div>
</div>
</div>
<div class="bg-white rounded-lg p-6 border border-sage/20">
<h5 class="font-semibold mb-4 text-sage">投递语义</h5>
<div class="text-center">
<div class="w-20 h-20 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-4">
<i class="fas fa-shield-alt text-3xl text-green-600"></i>
</div>
<p class="font-bold text-lg mb-2">至少一次 (At-least-once)</p>
<p class="text-sm text-stone">确保消息不会丢失,但可能被重复投递</p>
<p class="text-xs text-stone mt-2">消费者逻辑需要具备幂等性</p>
</div>
</div>
</div>
</div>
<!-- Fan-out Pattern -->
<div class="mb-12">
<h4 class="serif text-2xl font-semibold mb-6 flex items-center">
<i class="fas fa-broadcast-tower text-sage mr-3"></i>
消息分发与扇出 (Fan-out)
</h4>
<p class="mb-6">
Watermill的Redis Stream实现支持两种主要的消息分发模式:扇出(Fan-out)和通过消费者组进行负载均衡。扇出模式将同一条消息广播给所有订阅了该topic的消费者
<a href="https://watermill.io/pubsubs/redisstream/" class="citation-link">[3]</a>。
</p>
<div class="grid md:grid-cols-2 gap-6">
<div class="bg-white rounded-lg p-6 border border-sage/20">
<h5 class="font-semibold mb-3 text-sage flex items-center">
<i class="fas fa-broadcast-tower mr-2"></i>
扇出模式 (Fan-out)
</h5>
<ul class="space-y-2 text-sm">
<li class="flex items-start">
<i class="fas fa-arrow-right text-sage mr-2 mt-1"></i>
<span>不配置消费者组</span>
</li>
<li class="flex items-start">
<i class="fas fa-arrow-right text-sage mr-2 mt-1"></i>
<span>使用XREAD命令读取消息</span>
</li>
<li class="flex items-start">
<i class="fas fa-arrow-right text-sage mr-2 mt-1"></i>
<span>所有订阅者接收所有消息</span>
</li>
<li class="flex items-start">
<i class="fas fa-arrow-right text-sage mr-2 mt-1"></i>
<span>适用于事件通知场景</span>
</li>
</ul>
</div>
<div class="bg-white rounded-lg p-6 border border-sage/20">
<h5 class="font-semibold mb-3 text-sage flex items-center">
<i class="fas fa-users mr-2"></i>
负载均衡模式
</h5>
<ul class="space-y-2 text-sm">
<li class="flex items-start">
<i class="fas fa-arrow-right text-sage mr-2 mt-1"></i>
<span>配置消费者组</span>
</li>
<li class="flex items-start">
<i class="fas fa-arrow-right text-sage mr-2 mt-1"></i>
<span>使用XREADGROUP命令</span>
</li>
<li class="flex items-start">
<i class="fas fa-arrow-right text-sage mr-2 mt-1"></i>
<span>消息在消费者间分配</span>
</li>
<li class="flex items-start">
<i class="fas fa-arrow-right text-sage mr-2 mt-1"></i>
<span>适用于任务处理场景</span>
</li>
</ul>
</div>
</div>
</div>
</div>
<!-- Configuration -->
<div id="configuration" class="mb-16">
<h3 class="serif text-3xl font-semibold mb-8 text-charcoal">1.3 配置与使用</h3>
<div class="grid lg:grid-cols-2 gap-12">
<div>
<h4 class="serif text-2xl font-semibold mb-6 text-sage">发布者 (Publisher) 配置</h4>
<p class="mb-4">
发布者负责将消息发送到Redis Stream。通过
<code class="bg-mist px-1 rounded">NewPublisher</code>函数创建,需要
<code class="bg-mist px-1 rounded">PublisherConfig</code>配置对象。
</p>
<div class="code-block mb-4">
<div class="text-green-400 mb-2">// 创建Redis客户端</div>
<div>pubClient := redis.NewClient(&redis.Options{</div>
<div> Addr: "localhost:6379",</div>
<div> DB: 0,</div>
<div>})</div>
<br>
<div class="text-green-400 mb-2">// 创建发布者</div>
<div>publisher, err := redisstream.NewPublisher(</div>
<div> redisstream.PublisherConfig{</div>
<div> Client: pubClient,</div>
<div> Marshaller: redisstream.DefaultMarshallerUnmarshaller{},</div>
<div> },</div>
<div> watermill.NewStdLogger(false, false),</div>
<div>)</div>
</div>
<p class="text-sm text-stone">
Publish方法是阻塞的,等待Redis响应,确保发布操作的可靠性
<a href="https://watermill.io/pubsubs/redisstream/" class="citation-link">[193]</a>。
</p>
</div>
<div>
<h4 class="serif text-2xl font-semibold mb-6 text-sage">订阅者 (Subscriber) 配置</h4>
<p class="mb-4">
订阅者负责从Redis Stream接收消息。通过
<code class="bg-mist px-1 rounded">NewSubscriber</code>函数创建,需要
<code class="bg-mist px-1 rounded">SubscriberConfig</code>配置对象。
</p>
<div class="code-block mb-4">
<div class="text-green-400 mb-2">// 创建Redis客户端</div>
<div>subClient := redis.NewClient(&redis.Options{</div>
<div> Addr: "localhost:6379",</div>
<div> DB: 0,</div>
<div>})</div>
<br>
<div class="text-green-400 mb-2">// 创建订阅者</div>
<div>subscriber, err := redisstream.NewSubscriber(</div>
<div> redisstream.SubscriberConfig{</div>
<div> Client: subClient,</div>
<div> Unmarshaller: redisstream.DefaultMarshallerUnmarshaller{},</div>
<div> ConsumerGroup: "my-consumer-group", // 启用消费者组</div>
<div> },</div>
<div> watermill.NewStdLogger(false, false),</div>
<div>)</div>
</div>
<p class="text-sm text-stone">
通过Subscribe方法开始监听指定topic,处理完消息后必须调用
<code class="bg-gray-100 px-1 rounded">msg.Ack()</code>确认。
</p>
</div>
</div>
<div class="mt-8">
<h4 class="serif text-2xl font-semibold mb-6 text-sage">消息编组器 (Marshaler)</h4>
<div class="bg-white rounded-lg p-6 border border-sage/20">
<p class="mb-4">
由于Watermill的
<code class="bg-mist px-1 rounded">message.Message</code>结构体与Redis Stream底层数据格式不直接兼容,需要编组器进行转换。
<code class="bg-mist px-1 rounded">watermill-redisstream</code>库提供了默认的
<code class="bg-mist px-1 rounded">DefaultMarshallerUnmarshaller</code>实现
<a href="https://watermill.io/pubsubs/redisstream/" class="citation-link">[193]</a>。
</p>
<div class="bg-gray-50 rounded p-4">
<h5 class="font-semibold mb-2">默认实现特点:</h5>
<ul class="text-sm space-y-1">
<li>• 使用MessagePack进行高效二进制序列化</li>
<li>• 支持UUID、元数据(Metadata)和载荷(Payload)的完整转换</li>
<li>• 可自定义实现Marshaller和Unmarshaller接口</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</section>
<div class="section-divider"></div>
<!-- Redis Pub/Sub Support Section -->
<section id="redis-pubsub" class="py-16">
<div class="container mx-auto px-6">
<header class="text-center mb-16">
<h2 class="serif text-4xl font-bold text-charcoal mb-6">2. Redis Pub/Sub 支持</h2>
<p class="text-xl text-stone max-w-3xl mx-auto leading-relaxed">
Watermill如何处理Redis原生发布/订阅(Pub/Sub)模式,以及其基于Redis Stream的巧妙实现策略。
</p>
</header>
<!-- Limitations -->
<div id="limitations" class="mb-16">
<h3 class="serif text-3xl font-semibold mb-8 text-charcoal">2.1 原生 Pub/Sub 的局限性</h3>
<div class="grid lg:grid-cols-2 gap-12">
<div>
<h4 class="serif text-2xl font-semibold mb-6 text-red-600">消息无法持久化</h4>
<p class="mb-4">
Redis的原生发布/订阅(Pub/Sub)机制是完全无状态的,本质上是一个消息代理,负责将消息从发布者实时转发给所有订阅者
<a href="https://zhuanlan.zhihu.com/p/1903515066168514323" class="citation-link">[208]</a>。
消息并不会被存储到Redis数据库中。
</p>
<div class="bg-red-50 border-l-4 border-red-400 p-4 mb-4">
<p class="text-red-700 text-sm">
<strong>"即发即弃"模式:</strong>一旦消息被发布,如果此时没有订阅者或订阅者未能及时接收,消息就会永久丢失
<a href="https://blog.csdn.net/Mrxiao_bo/article/details/134262366" class="citation-link">[209]</a>。
</p>
</div>
<p class="text-sm text-stone">
这种设计使得原生Pub/Sub非常适合实时聊天、在线游戏状态广播等场景,但不适用于需要保证消息必达的业务。
</p>
</div>
<div>
<h4 class="serif text-2xl font-semibold mb-6 text-red-600">消息丢失风险</h4>
<div class="space-y-4">
<div class="bg-white rounded-lg p-4 border border-red-200">
<h5 class="font-semibold text-red-700 mb-2 flex items-center">
<i class="fas fa-user-times mr-2"></i>
消费者离线
</h5>
<p class="text-sm text-stone">订阅者在消息发布时不在线,将完全错过消息,重新上线后无法获取离线期间的消息
<a href="https://www.cnblogs.com/goldsunshine/p/17410148.html" class="citation-link">[216]</a>。
</p>
</div>
<div class="bg-white rounded-lg p-4 border border-red-200">
<h5 class="font-semibold text-red-700 mb-2 flex items-center">
<i class="fas fa-network-wired mr-2"></i>
网络问题
</h5>
<p class="text-sm text-stone">消息传输过程中网络连接中断可能导致消息丢失,Redis在消息发送给客户端后就将其丢弃。</p>
</div>
<div class="bg-white rounded-lg p-4 border border-red-200">
<h5 class="font-semibold text-red-700 mb-2 flex items-center">
<i class="fas fa-server mr-2"></i>
服务器故障
</h5>
<p class="text-sm text-stone">Redis服务器崩溃或重启,所有在内存中等待转发的消息都会丢失。</p>
</div>
</div>
</div>
</div>
</div>
<!-- Implementation Strategy -->
<div id="implementation-strategy" class="mb-16">
<h3 class="serif text-3xl font-semibold mb-8 text-charcoal">2.2 Watermill 的实现策略</h3>
<div class="bg-gradient-to-r from-sage/10 to-clay/10 rounded-xl p-8 mb-8">
<h4 class="serif text-2xl font-semibold mb-6 text-center text-sage">基于Redis Stream实现Pub/Sub模式</h4>
<p class="text-center mb-6 text-lg">
Watermill选择基于Redis Stream来实现Pub/Sub模式,巧妙利用Stream的持久化特性,在提供发布/订阅语义的同时保证消息可靠性。
</p>
</div>
<div class="grid lg:grid-cols-2 gap-8">
<div>
<h4 class="serif text-2xl font-semibold mb-6 text-sage">实现机制</h4>
<div class="space-y-4">
<div class="flex items-start">
<div class="w-10 h-10 bg-sage rounded-full flex items-center justify-center text-white font-bold mr-4 mt-1">1</div>
<div>
<h5 class="font-semibold mb-1">发布消息</h5>
<p class="text-sm text-stone">Publish方法内部执行XADD命令,将消息追加到Stream</p>
</div>
</div>
<div class="flex items-start">
<div class="w-10 h-10 bg-clay rounded-full flex items-center justify-center text-white font-bold mr-4 mt-1">2</div>
<div>
<h5 class="font-semibold mb-1">订阅消息</h5>
<p class="text-sm text-stone">根据是否配置消费者组,选择XREAD或XREADGROUP命令</p>
</div>
</div>
<div class="flex items-start">
<div class="w-10 h-10 bg-stone rounded-full flex items-center justify-center text-white font-bold mr-4 mt-1">3</div>
<div>
<h5 class="font-semibold mb-1">消息处理</h5>
<p class="text-sm text-stone">基于持久化日志,支持消息重放和回溯</p>
</div>
</div>
</div>
</div>
<div>
<h4 class="serif text-2xl font-semibold mb-6 text-sage">解决的问题</h4>
<div class="space-y-4">
<div class="bg-white rounded-lg p-4 border border-green-200">
<h5 class="font-semibold text-green-700 mb-2 flex items-center">
<i class="fas fa-database mr-2"></i>
消息持久化
</h5>
<p class="text-sm text-stone">所有消息存储在Redis Stream中,具备持久性,消费者崩溃或离线后消息依然安全
<a href="https://blog.csdn.net/Mrxiao_bo/article/details/134262366" class="citation-link">[209]</a>。
</p>
</div>
<div class="bg-white rounded-lg p-4 border border-green-200">
<h5 class="font-semibold text-green-700 mb-2 flex items-center">
<i class="fas fa-history mr-2"></i>
消息回溯
</h5>
<p class="text-sm text-stone">消费者可指定消息ID,从Stream任意位置重新读取消息,支持数据恢复和审计。</p>
</div>
<div class="bg-white rounded-lg p-4 border border-green-200">
<h5 class="font-semibold text-green-700 mb-2 flex items-center">
<i class="fas fa-shield-alt mr-2"></i>
可靠性保证
</h5>
<p class="text-sm text-stone">通过XACK命令实现可靠消息处理语义,确保只有成功处理的消息才被最终确认
<a href="https://watermill.io/pubsubs/redisstream/" class="citation-link">[3]</a>。
</p>
</div>
</div>
</div>
</div>
</div>
<!-- Differences -->
<div id="differences" class="mb-16">
<h3 class="serif text-3xl font-semibold mb-8 text-charcoal">2.3 与原生 Pub/Sub 的区别</h3>
<div class="comparison-table">
<table class="w-full">
<thead>
<tr>
<th class="text-left">特性</th>
<th class="text-left">Watermill (基于Redis Stream)</th>
<th class="text-left">Redis原生Pub/Sub</th>
</tr>
</thead>
<tbody>
<tr>
<td class="font-medium">消息持久化</td>
<td class="text-green-600"><strong>支持</strong>。消息存储在Stream中,可配置持久化到磁盘。</td>
<td class="text-red-600"><strong>不支持</strong>。消息是瞬时的,不存储。</td>
</tr>
<tr>
<td class="font-medium">消息可靠性</td>
<td class="text-green-600"><strong>高</strong>。通过消费者组和ACK机制,保证"至少一次"投递。</td>
<td class="text-red-600"><strong>低</strong>。消息可能因消费者离线或网络问题而丢失。</td>
</tr>
<tr>
<td class="font-medium">消费者组</td>
<td class="text-green-600"><strong>支持</strong>。可实现负载均衡和故障转移。</td>
<td class="text-red-600"><strong>不支持</strong>。所有订阅者都会收到所有消息的副本。</td>
</tr>
<tr>
<td class="font-medium">消息回溯</td>
<td class="text-green-600"><strong>支持</strong>。消费者可以从任意历史位置开始读取。</td>
<td class="text-red-600"><strong>不支持</strong>。只能接收订阅后新发布的消息。</td>
</tr>
<tr>
<td class="font-medium">性能</td>
<td class="text-yellow-600"><strong>中等</strong>。受持久化操作影响,吞吐量较低。</td>
<td class="text-green-600"><strong>极高</strong>。无状态代理,吞吐量非常高。</td>
</tr>
<tr>
<td class="font-medium">适用场景</td>
<td>需要高可靠性、持久性、负载均衡的事件驱动、任务队列。</td>
<td>实时性要求高、可容忍消息丢失的广播、通知。</td>
</tr>
</tbody>
</table>
</div>
<div class="mt-8 bg-yellow-50 border-l-4 border-yellow-400 p-6">
<h5 class="font-semibold text-yellow-800 mb-2 flex items-center">
<i class="fas fa-lightbulb mr-2"></i>
设计权衡
</h5>
<p class="text-yellow-700 text-sm">
Watermill的Redis支持通过牺牲一部分性能(约54,000条/秒 vs 原生Pub/Sub的100万条/秒以上)
<a href="https://zhuanlan.zhihu.com/p/1903515066168514323" class="citation-link">[208]</a>,
换取了强大的持久化和可靠性保证。开发者应根据业务对可靠性和实时性的具体要求做出选择。
</p>
</div>
</div>
</div>
</section>
<div class="section-divider"></div>
<!-- Redis List Support Section -->
<section id="redis-list" class="py-16">
<div class="container mx-auto px-6">
<header class="text-center mb-16">
<h2 class="serif text-4xl font-bold text-charcoal mb-6">3. Redis List 支持</h2>
<p class="text-xl text-stone max-w-3xl mx-auto leading-relaxed">
Watermill对Redis List数据结构的支持情况分析,以及自定义实现的可能性和挑战。
</p>
</header>
<!-- Official Support -->
<div id="official-support" class="mb-16">
<h3 class="serif text-3xl font-semibold mb-8 text-charcoal">3.1 官方支持情况</h3>
<div class="bg-red-50 border-l-4 border-red-400 p-8 mb-8">
<h4 class="serif text-2xl font-semibold mb-4 text-red-700 flex items-center">
<i class="fas fa-times-circle mr-3"></i>
无官方直接支持
</h4>
<p class="text-red-700 mb-4">
根据对Watermill官方文档、GitHub仓库以及相关社区资源的调研,Watermill<strong>没有</strong>提供对基于Redis List的消息队列的官方直接支持。
</p>
<p class="text-red-600 text-sm">
在Watermill官方列出的所有支持的Pub/Sub实现中,明确包含了Redis Stream,但并未提及任何基于Redis List的实现
<a href="https://github.com/ThreeDotsLabs/watermill" class="citation-link">[195]</a>
<a href="https://trendshift.io/admin/repository/ask-ai/10995" class="citation-link">[205]</a>。
</p>
</div>
<div class="grid lg:grid-cols-2 gap-8">
<div>
<h4 class="serif text-2xl font-semibold mb-6 text-sage">原因分析</h4>
<div class="space-y-4">
<div class="bg-white rounded-lg p-4 border border-sage/20">
<h5 class="font-semibold mb-2 flex items-center">
<i class="fas fa-target text-sage mr-2"></i>
设计理念冲突
</h5>
<p class="text-sm text-stone">
Watermill核心设计理念是提供统一、可靠且功能丰富的消息处理框架,而Redis List作为相对原始的消息队列实现,功能集与Watermill设计目标不完全匹配。
</p>
</div>
<div class="bg-white rounded-lg p-4 border border-sage/20">
<h5 class="font-semibold mb-2 flex items-center">
<i class="fas fa-puzzle-piece text-sage mr-2"></i>
功能局限性
</h5>
<p class="text-sm text-stone">
Redis List缺乏原生消费者组、消息确认(ACK)机制以及消息持久化保证,这些都是Watermill强调的核心特性。
</p>
</div>
<div class="bg-white rounded-lg p-4 border border-sage/20">
<h5 class="font-semibold mb-2 flex items-center">
<i class="fas fa-arrow-right text-sage mr-2"></i>
战略选择
</h5>
<p class="text-sm text-stone">
Watermill团队选择将精力集中在功能更强大、更现代的Redis Stream上,而非维护功能受限的List实现。
</p>
</div>
</div>
</div>
<div>
<h4 class="serif text-2xl font-semibold mb-6 text-sage">官方支持列表</h4>
<div class="bg-white rounded-lg p-6 border border-sage/20">
<h5 class="font-semibold mb-4">Watermill官方支持的Pub/Sub实现:</h5>
<div class="grid grid-cols-2 gap-3 text-sm">
<div class="flex items-center">
<i class="fas fa-check text-green-500 mr-2"></i>
<span>AMQP (RabbitMQ)</span>
</div>
<div class="flex items-center">
<i class="fas fa-check text-green-500 mr-2"></i>
<span>Kafka</span>
</div>
<div class="flex items-center">
<i class="fas fa-check text-green-500 mr-2"></i>
<span>NATS</span>
</div>
<div class="flex items-center">
<i class="fas fa-check text-green-500 mr-2"></i>
<span>Google Cloud Pub/Sub</span>
</div>
<div class="flex items-center">
<i class="fas fa-check text-green-500 mr-2"></i>
<span>SQL/SQLite</span>
</div>
<div class="flex items-center bg-green-50 p-2 rounded">
<i class="fas fa-check text-green-500 mr-2"></i>
<span><strong>Redis Stream</strong></span>
</div>
<div class="flex items-center bg-red-50 p-2 rounded col-span-2">
<i class="fas fa-times text-red-500 mr-2"></i>
<span><strong>Redis List (不支持)</strong></span>
</div>
</div>
<p class="text-xs text-stone mt-4">
社区中可能存在非官方实现,但其稳定性和兼容性无法保证。推荐使用官方支持的Redis Stream。
</p>
</div>
</div>
</div>
</div>
<!-- Custom Implementation -->
<div id="custom-implementation" class="mb-16">
<h3 class="serif text-3xl font-semibold mb-8 text-charcoal">3.2 社区实践与自定义实现</h3>
<div class="bg-yellow-50 border-l-4 border-yellow-400 p-6 mb-8">
<h4 class="font-semibold text-yellow-800 mb-2 flex items-center">
<i class="fas fa-exclamation-triangle mr-2"></i>
理论可行性
</h4>
<p class="text-yellow-700 text-sm">
尽管Watermill官方没有直接支持Redis List,但理论上开发者可以通过自定义实现Watermill的Publisher和Subscriber接口,创建基于Redis List的Pub/Sub适配器。
</p>
</div>
<div class="grid lg:grid-cols-2 gap-8">
<div>
<h4 class="serif text-2xl font-semibold mb-6 text-sage">基于Redis List命令的实现</h4>
<div class="bg-white rounded-lg p-6 border border-sage/20 mb-6">
<h5 class="font-semibold mb-4 text-sage">核心命令</h5>
<div class="space-y-3">
<div>
<h6 class="font-medium text-sm mb-1">发布消息(入队)</h6>
<code class="bg-gray-100 px-2 py-1 rounded text-sm">LPUSH</code>
<span class="text-stone text-sm ml-2">从列表左侧插入消息</span>
</div>
<div>
<h6 class="font-medium text-sm mb-1">消费消息(出队)</h6>
<div class="flex items-center space-x-4">
<div>
<code class="bg-gray-100 px-2 py-1 rounded text-sm">BRPOP</code>
<span class="text-stone text-sm ml-2">阻塞式弹出</span>
</div>
<div>
<code class="bg-gray-100 px-2 py-1 rounded text-sm">RPOP</code>
<span class="text-stone text-sm ml-2">非阻塞式弹出</span>
</div>
</div>
</div>
</div>
</div>
<h5 class="font-semibold mb-4">自定义实现要点</h5>
<div class="space-y-3 text-sm">
<div class="flex items-start">
<i class="fas fa-code text-sage mr-2 mt-1"></i>
<span>Publisher: 序列化message.Message并使用LPUSH推送到Redis List</span>
</div>
<div class="flex items-start">
<i class="fas fa-sync text-sage mr-2 mt-1"></i>
<span>Subscriber: 循环使用BRPOP从Redis List拉取消息</span>
</div>
<div class="flex items-start">
<i class="fas fa-exchange-alt text-sage mr-2 mt-1"></i>
<span>反序列化消息并通过channel发送给Watermill路由器</span>
</div>
</div>
</div>
<div>
<h4 class="serif text-2xl font-semibold mb-6 text-sage">集成挑战</h4>
<div class="space-y-4">
<div class="bg-white rounded-lg p-4 border border-orange-200">
<h5 class="font-semibold text-orange-700 mb-2 flex items-center">
<i class="fas fa-exclamation-circle mr-2"></i>
ACK机制缺失
</h5>
<p class="text-sm text-stone">
消息一旦被RPOP或BRPOP出来就从List中移除,如果消费者处理时崩溃,消息永久丢失。
</p>
</div>
<div class="bg-white rounded-lg p-4 border border-orange-200">
<h5 class="font-semibold text-orange-700 mb-2 flex items-center">
<i class="fas fa-redo mr-2"></i>
重试机制复杂
</h5>
<p class="text-sm text-stone">
需要引入额外机制(如"处理中"List或Sorted Set)来跟踪正在处理的消息。
</p>
</div>
<div class="bg-white rounded-lg p-4 border border-orange-200">
<h5 class="font-semibold text-orange-700 mb-2 flex items-center">
<i class="fas fa-shield-alt mr-2"></i>
幂等性要求
</h5>
<p class="text-sm text-stone">
由于网络问题或消费者崩溃可能导致消息重复处理,消费者逻辑需要具备幂等性。
</p>
</div>
</div>
</div>
</div>
</div>
<!-- Implementation Approach -->
<div id="implementation-approach" class="mb-16">
<h3 class="serif text-3xl font-semibold mb-8 text-charcoal">3.3 实现方式探讨</h3>
<div class="grid lg:grid-cols-2 gap-8">
<div>
<h4 class="serif text-2xl font-semibold mb-6 text-sage">经典实现方式</h4>
<div class="bg-white rounded-lg p-6 border border-sage/20 mb-6">
<h5 class="font-semibold mb-4 text-sage">LPUSH + BRPOP/RPOP</h5>
<p class="text-sm text-stone mb-4">构建Redis List队列最经典的方式,实现简单FIFO队列。</p>
<div class="space-y-3">
<div>
<h6 class="font-medium text-sm mb-1 text-green-600">发布者(Publisher)</h6>
<ul class="text-xs text-stone space-y-1 ml-4">
<li>• 接收*message.Message</li>
<li>• 序列化为JSON字符串</li>
<li>• 执行LPUSH <topic> <serialized_message></li>
</ul>
</div>
<div>
<h6 class="font-medium text-sm mb-1 text-blue-600">订阅者(Subscriber)</h6>
<ul class="text-xs text-stone space-y-1 ml-4">
<li>• 后台goroutine循环执行BRPOP</li>
<li>• 反序列化为*message.Message</li>
<li>• 通过channel发送给Watermill路由器</li>
</ul>
</div>
</div>
</div>
<h5 class="font-semibold mb-3">优点</h5>
<ul class="text-sm text-stone space-y-1">
<li class="flex items-start">
<i class="fas fa-check text-green-500 mr-2 mt-1"></i>
<span>实现简单,性能高</span>
</li>
<li class="flex items-start">
<i class="fas fa-check text-green-500 mr-2 mt-1"></i>
<span>保证消息顺序性(FIFO)</span>
</li>
<li class="flex items-start">
<i class="fas fa-check text-green-500 mr-2 mt-1"></i>
<span>适用于轻量级任务处理</span>
</li>
</ul>
</div>
<div>
<h4 class="serif text-2xl font-semibold mb-6 text-sage">可靠性增强方案</h4>
<div class="space-y-4">
<div class="bg-white rounded-lg p-4 border border-sage/20">
<h5 class="font-semibold mb-2 text-sage flex items-center">
<i class="fas fa-list mr-2"></i>
"处理中"List方案
</h5>
<p class="text-xs text-stone mb-2">
消费者从主队列RPOP消息后,LPUSH到临时"处理中"List,处理成功后再移除。
</p>
<div class="bg-gray-50 rounded p-2 text-xs">
<div>主队列 → RPOP → "处理中"List → 处理 → 移除</div>
<div class="text-orange-600 mt-1">崩溃后检查"处理中"List重新处理</div>
</div>
</div>
<div class="bg-white rounded-lg p-4 border border-sage/20">
<h5 class="font-semibold mb-2 text-sage flex items-center">
<i class="fas fa-sort-amount-up mr-2"></i>
Sorted Set方案
</h5>
<p class="text-xs text-stone mb-2">
使用Sorted Set存储消息,时间戳作为score,通过ZRANGEBYSCORE获取待处理消息。
</p>
<div class="bg-gray-50 rounded p-2 text-xs">
<div>ZADD <topic> <timestamp> <message></div>
<div class="text-orange-600 mt-1">处理成功后ZREM,失败可延迟重试</div>
</div>
</div>
<div class="bg-red-50 border border-red-200 rounded-lg p-4">
<h5 class="font-semibold text-red-700 mb-2 flex items-center">
<i class="fas fa-exclamation-triangle mr-2"></i>
实现复杂性
</h5>
<p class="text-xs text-red-600">
这些增强方案大大增加了实现复杂性,并且其可靠性和性能无法与Redis Stream原生支持相比。
</p>
</div>
</div>
</div>
</div>
<div class="mt-12 bg-gradient-to-r from-sage/10 to-clay/10 rounded-xl p-8">
<h4 class="serif text-2xl font-semibold mb-6 text-center text-charcoal">结论与建议</h4>
<div class="grid md:grid-cols-2 gap-8">
<div>
<h5 class="font-semibold mb-3 flex items-center text-sage">
<i class="fas fa-lightbulb mr-2"></i>
技术可行性
</h5>
<p class="text-sm text-stone mb-3">
虽然理论上可以在Watermill中自定义实现基于Redis List的Pub/Sub适配器,但考虑到其固有的功能缺陷和实现复杂性,通常不是推荐的选择。
</p>
</div>
<div>
<h5 class="font-semibold mb-3 flex items-center text-sage">
<i class="fas fa-thumbs-up mr-2"></i>
最佳实践
</h5>
<p class="text-sm text-stone mb-3">
对于大多数应用而言,直接使用Watermill官方提供的
<code class="bg-white px-1 rounded">watermill-redisstream</code>是更明智、更高效的方案。
</p>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Footer -->
<footer class="bg-charcoal text-white py-12">
<div class="container mx-auto px-6">
<div class="text-center">
<h3 class="serif text-2xl font-bold mb-4">Watermill Redis支持调研总结</h3>
<div class="w-24 h-0.5 bg-sage mx-auto mb-6"></div>
<p class="text-gray-300 max-w-3xl mx-auto mb-8">
Watermill通过官方
<code class="bg-gray-700 px-2 py-1 rounded">watermill-redisstream</code>库提供了对Redis Stream的全面支持,
基于Redis Stream实现了可靠的Pub/Sub模式,但不支持Redis List。这种设计选择体现了对可靠性和持久性的重视,
为构建现代事件驱动应用提供了坚实基础。
</p>
<div class="grid md:grid-cols-3 gap-6 text-sm">
<div class="bg-gray-800 rounded-lg p-4">
<i class="fas fa-star text-sage text-2xl mb-2"></i>
<h4 class="font-semibold mb-2">Redis Stream</h4>
<p class="text-gray-400">官方全面支持,推荐选择</p>
</div>
<div class="bg-gray-800 rounded-lg p-4">
<i class="fas fa-check-circle text-clay text-2xl mb-2"></i>
<h4 class="font-semibold mb-2">Pub/Sub模式</h4>
<p class="text-gray-400">基于Stream实现,可靠性高</p>
</div>
<div class="bg-gray-800 rounded-lg p-4">
<i class="fas fa-times-circle text-stone text-2xl mb-2"></i>
<h4 class="font-semibold mb-2">Redis List</h4>
<p class="text-gray-400">无官方支持,不推荐</p>
</div>
</div>
</div>
</div>
</footer>
</main>
<script>
// Initialize Mermaid with enhanced styling
mermaid.initialize({
startOnLoad: true,
theme: 'base',
themeVariables: {
primaryColor: '#9CA986',
primaryTextColor: '#2C2C2C',
primaryBorderColor: '#8B7D6B',
lineColor: '#8B7D6B',
secondaryColor: '#B5A082',
tertiaryColor: '#F7F3E9',
background: '#FFFFFF',
mainBkg: '#FFFFFF',
secondBkg: '#F7F3E9',
tertiaryBkg: '#E8E6E1',
nodeBkg: '#FFFFFF',
clusterBkg: '#F7F3E9',
edgeLabelBackground: 'rgba(247, 243, 233, 0.95)',
nodeTextColor: '#2C2C2C',
textColor: '#2C2C2C',
labelTextColor: '#2C2C2C',
loopTextColor: '#2C2C2C',
noteTextColor: '#2C2C2C',
noteBorderColor: '#B5A082',
noteBkgColor: '#F7F3E9',
activationBorderColor: '#8B7D6B',
activationBkgColor: '#E8E6E1',
sectionBkgColor: '#F7F3E9',
altSectionBkgColor: '#E8E6E1',
gridColor: '#E8E6E1',
c0: '#FFFFFF',
c1: '#F7F3E9',
c2: '#E8E6E1',
c3: '#D6D4CF',
// Enhanced contrast colors for different node types
activeTaskBkgColor: '#9CA986',
activeTaskBorderColor: '#8B7D6B',
gridColor: '#8B7D6B',
section0: '#F7F3E9',
section1: '#E8E6E1',
section2: '#D6D4CF',
section3: '#C4C2BD'
},
flowchart: {
useMaxWidth: true,
htmlLabels: true,
curve: 'basis',
padding: 30,
nodeSpacing: 60,
rankSpacing: 100,
diagramPadding: 20
},
sequence: {
useMaxWidth: true,
diagramMarginX: 50,
diagramMarginY: 10,
actorMargin: 50,
width: 150,
height: 65,
boxMargin: 10,
boxTextMargin: 5,
noteMargin: 10,
messageMargin: 35,
mirrorActors: true,
bottomMarginAdj: 1,
useMaxWidth: true,
rightAngles: false,
showSequenceNumbers: false
},
gantt: {
useMaxWidth: true,
leftPadding: 75,
gridLineStartPadding: 35,
fontSize: 11,
sectionFontSize: 24,
numberSectionStyles: 4
}
});
// Initialize Mermaid Controls for zoom and pan
function initializeMermaidControls() {
const containers = document.querySelectorAll('.mermaid-container');
containers.forEach(container => {
const mermaidElement = container.querySelector('.mermaid');
let scale = 1;
let isDragging = false;
let startX, startY, translateX = 0, translateY = 0;
// 触摸相关状态
let isTouch = false;
let touchStartTime = 0;
let initialDistance = 0;
let initialScale = 1;
let isPinching = false;
// Zoom controls
const zoomInBtn = container.querySelector('.zoom-in');
const zoomOutBtn = container.querySelector('.zoom-out');
const resetBtn = container.querySelector('.reset-zoom');
const fullscreenBtn = container.querySelector('.fullscreen');
function updateTransform() {
mermaidElement.style.transform = `translate(${translateX}px, ${translateY}px) scale(${scale})`;
if (scale > 1) {
container.classList.add('zoomed');
} else {
container.classList.remove('zoomed');
}
mermaidElement.style.cursor = isDragging ? 'grabbing' : 'grab';
}
if (zoomInBtn) {
zoomInBtn.addEventListener('click', () => {
scale = Math.min(scale * 1.25, 4);
updateTransform();
});
}
if (zoomOutBtn) {
zoomOutBtn.addEventListener('click', () => {
scale = Math.max(scale / 1.25, 0.3);
if (scale <= 1) {
translateX = 0;
translateY = 0;
}
updateTransform();
});
}
if (resetBtn) {
resetBtn.addEventListener('click', () => {
scale = 1;
translateX = 0;
translateY = 0;
updateTransform();
});
}
if (fullscreenBtn) {
fullscreenBtn.addEventListener('click', () => {
if (container.requestFullscreen) {
container.requestFullscreen();
} else if (container.webkitRequestFullscreen) {
container.webkitRequestFullscreen();
} else if (container.msRequestFullscreen) {
container.msRequestFullscreen();
}
});
}
// Mouse Events
mermaidElement.addEventListener('mousedown', (e) => {
if (isTouch) return; // 如果是触摸设备,忽略鼠标事件
isDragging = true;
startX = e.clientX - translateX;
startY = e.clientY - translateY;
mermaidElement.style.cursor = 'grabbing';
updateTransform();
e.preventDefault();
});
document.addEventListener('mousemove', (e) => {
if (isDragging && !isTouch) {
translateX = e.clientX - startX;
translateY = e.clientY - startY;
updateTransform();
}
});
document.addEventListener('mouseup', () => {
if (isDragging && !isTouch) {
isDragging = false;
mermaidElement.style.cursor = 'grab';
updateTransform();
}
});
document.addEventListener('mouseleave', () => {
if (isDragging && !isTouch) {
isDragging = false;
mermaidElement.style.cursor = 'grab';
updateTransform();
}
});
// 获取两点之间的距离
function getTouchDistance(touch1, touch2) {
return Math.hypot(
touch2.clientX - touch1.clientX,
touch2.clientY - touch1.clientY
);
}
// Touch Events - 触摸事件处理
mermaidElement.addEventListener('touchstart', (e) => {
isTouch = true;
touchStartTime = Date.now();
if (e.touches.length === 1) {
// 单指拖动
isPinching = false;
isDragging = true;
const touch = e.touches[0];
startX = touch.clientX - translateX;
startY = touch.clientY - translateY;
} else if (e.touches.length === 2) {
// 双指缩放
isPinching = true;
isDragging = false;
const touch1 = e.touches[0];
const touch2 = e.touches[1];
initialDistance = getTouchDistance(touch1, touch2);
initialScale = scale;
}
e.preventDefault();
}, { passive: false });
mermaidElement.addEventListener('touchmove', (e) => {
if (e.touches.length === 1 && isDragging && !isPinching) {
// 单指拖动
const touch = e.touches[0];
translateX = touch.clientX - startX;
translateY = touch.clientY - startY;
updateTransform();
} else if (e.touches.length === 2 && isPinching) {
// 双指缩放
const touch1 = e.touches[0];
const touch2 = e.touches[1];
const currentDistance = getTouchDistance(touch1, touch2);
if (initialDistance > 0) {
const newScale = Math.min(Math.max(
initialScale * (currentDistance / initialDistance),
0.3
), 4);
scale = newScale;
updateTransform();
}
}
e.preventDefault();
}, { passive: false });
mermaidElement.addEventListener('touchend', (e) => {
// 重置状态
if (e.touches.length === 0) {
isDragging = false;
isPinching = false;
initialDistance = 0;
// 延迟重置isTouch,避免鼠标事件立即触发
setTimeout(() => {
isTouch = false;
}, 100);
} else if (e.touches.length === 1 && isPinching) {
// 从双指变为单指,切换为拖动模式
isPinching = false;
isDragging = true;
const touch = e.touches[0];
startX = touch.clientX - translateX;
startY = touch.clientY - translateY;
}
updateTransform();
});
mermaidElement.addEventListener('touchcancel', (e) => {
isDragging = false;
isPinching = false;
initialDistance = 0;
setTimeout(() => {
isTouch = false;
}, 100);
updateTransform();
});
// Enhanced wheel zoom with better center point handling
container.addEventListener('wheel', (e) => {
e.preventDefault();
const rect = container.getBoundingClientRect();
const centerX = rect.width / 2;
const centerY = rect.height / 2;
const delta = e.deltaY > 0 ? 0.9 : 1.1;
const newScale = Math.min(Math.max(scale * delta, 0.3), 4);
// Adjust translation to zoom towards center
if (newScale !== scale) {
const scaleDiff = newScale / scale;
translateX = translateX * scaleDiff;
translateY = translateY * scaleDiff;
scale = newScale;
if (scale <= 1) {
translateX = 0;
translateY = 0;
}
updateTransform();
}
});
// Initialize display
updateTransform();
});
}
// Initialize Mermaid controls when DOM is loaded
document.addEventListener('DOMContentLoaded', initializeMermaidControls);
// Smooth scrolling for anchor links
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function (e) {
e.preventDefault();
const target = document.querySelector(this.getAttribute('href'));
if (target) {
target.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
}
});
});
// Active TOC link highlighting
const observerOptions = {
rootMargin: '-20% 0px -70% 0px',
threshold: 0
};
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
const id = entry.target.getAttribute('id');
const tocLink = document.querySelector(`a[href="#${id}"]`);
if (entry.isIntersecting) {
// Remove active class from all links
document.querySelectorAll('.toc-link').forEach(link => {
link.classList.remove('active');
});
// Add active class to current link
if (tocLink) {
tocLink.classList.add('active');
}
}
});
}, observerOptions);
// Observe all sections
document.querySelectorAll('section[id], div[id]').forEach(section => {
observer.observe(section);
});
// Add loading animation for images
document.querySelectorAll('img').forEach(img => {
img.addEventListener('load', function() {
this.style.opacity = '1';
});
img.style.opacity = '0';
img.style.transition = 'opacity 0.3s ease';
});
</script>
</body>
</html>
登录后可参与表态
讨论回复
1 条回复
S-9 (steper9)
#1
10-03 03:49
登录后可参与表态