Loading...
正在加载...
请稍候

Watermill项目 Redis消息队列支持

S-9 (steper9) 2025年10月03日 03:36
<!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>&nbsp;&nbsp;Addr: "localhost:6379",</div> <div>&nbsp;&nbsp;DB: &nbsp;&nbsp;0,</div> <div>})</div> <br> <div class="text-green-400 mb-2">// 创建发布者</div> <div>publisher, err := redisstream.NewPublisher(</div> <div>&nbsp;&nbsp;redisstream.PublisherConfig{</div> <div>&nbsp;&nbsp;&nbsp;&nbsp;Client: &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;pubClient,</div> <div>&nbsp;&nbsp;&nbsp;&nbsp;Marshaller: redisstream.DefaultMarshallerUnmarshaller{},</div> <div>&nbsp;&nbsp;},</div> <div>&nbsp;&nbsp;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>&nbsp;&nbsp;Addr: "localhost:6379",</div> <div>&nbsp;&nbsp;DB: &nbsp;&nbsp;0,</div> <div>})</div> <br> <div class="text-green-400 mb-2">// 创建订阅者</div> <div>subscriber, err := redisstream.NewSubscriber(</div> <div>&nbsp;&nbsp;redisstream.SubscriberConfig{</div> <div>&nbsp;&nbsp;&nbsp;&nbsp;Client: &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;subClient,</div> <div>&nbsp;&nbsp;&nbsp;&nbsp;Unmarshaller: &nbsp;redisstream.DefaultMarshallerUnmarshaller{},</div> <div>&nbsp;&nbsp;&nbsp;&nbsp;ConsumerGroup: "my-consumer-group", // 启用消费者组</div> <div>&nbsp;&nbsp;},</div> <div>&nbsp;&nbsp;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
Watermill 快速入门 https://academy.threedots.tech/trainings/watermill-quickstart/exercise/