<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Redis on 方糖猫Blog</title>
    <link>https://ftmao.com/blog/tags/redis/</link>
    <description>AI 视频、AI Agent 与工程实践笔记</description>
    <generator>Hugo</generator>
    <language>zh-CN</language>
    <lastBuildDate>Mon, 14 Sep 2026 14:00:00 +0800</lastBuildDate><atom:link href="https://ftmao.com/blog/tags/redis/index.xml" rel="self" type="application/rss+xml" /><item>
      <title>Ruby on Rails 高级篇：接入 Redis、Elasticsearch、消息队列与大数据组件的工程实践</title>
      <link>https://ftmao.com/blog/posts/ruby-on-rails-advanced-external-components/</link>
      <pubDate>Mon, 14 Sep 2026 14:00:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/ruby-on-rails-advanced-external-components/</guid>
      <description>&lt;h1 id=&#34;ruby-on-rails-高级篇接入-rediselasticsearch消息队列与大数据组件的工程实践&#34;&gt;Ruby on Rails 高级篇：接入 Redis、Elasticsearch、消息队列与大数据组件的工程实践&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;姊妹篇：《&lt;a href=&#34;https://ftmao.com/blog/blog/posts/ruby-on-rails-beginner-to-mastery/&#34;&gt;Ruby on Rails 从零基础到精通&lt;/a&gt;》——入门篇讲「把功能写出来」，这一篇讲「把系统接出去」。
目标读者：已经能独立写 Rails CRUD，现在产品要接搜索、接缓存、接消息队列、接大数据管道，想知道「正确姿势」而不是「能跑就行」的开发者。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;入门篇的应用是自包含的：一个 Rails 进程 + 一个数据库就是全世界。真实系统不是这样——搜索要 ES、缓存要 Redis、异步要 MQ、报表要大数据平台。每接一个组件，就多一种故障模式。这一篇的结构：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    A[&amp;#34;一 Redis&amp;lt;br/&amp;gt;缓存 队列 数据结构&amp;#34;] --&amp;gt; B[&amp;#34;二 Elasticsearch&amp;lt;br/&amp;gt;搜索与索引同步&amp;#34;]
    B --&amp;gt; C[&amp;#34;三 消息队列&amp;lt;br/&amp;gt;RabbitMQ / Kafka&amp;#34;]
    C --&amp;gt; D[&amp;#34;四 大数据组件&amp;lt;br/&amp;gt;Spark / Flink / ClickHouse&amp;#34;]
    D --&amp;gt; E[&amp;#34;五 通用纪律&amp;lt;br/&amp;gt;超时 重试 熔断 幂等&amp;#34;]
    style E fill:#e8f4e8&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;前四篇每个组件都回答三个问题：&lt;strong&gt;用什么 gem、数据怎么流动、生产上死在哪&lt;/strong&gt;。第五篇是把前四篇的教训抽象成通用清单——以后接任何新组件，照着过一遍就行。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;第一篇-redis一个组件四种角色&#34;&gt;第一篇 Redis：一个组件，四种角色&lt;/h2&gt;
&lt;p&gt;Rails 接 Redis 最大的误区是「装个 redis gem 存取字符串」。Redis 在 Rails 生态里实际承担四种完全不同的角色，配置和运维要求都不一样：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    R[(&amp;#34;Redis&amp;#34;)]
    R1[&amp;#34;角色1 缓存&amp;lt;br/&amp;gt;Rails.cache&amp;#34;] --- R
    R2[&amp;#34;角色2 任务队列&amp;lt;br/&amp;gt;Sidekiq&amp;#34;] --- R
    R3[&amp;#34;角色3 实时推送&amp;lt;br/&amp;gt;Action Cable&amp;#34;] --- R
    R4[&amp;#34;角色4 业务数据结构&amp;lt;br/&amp;gt;计数器/排行榜/锁&amp;#34;] --- R&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h3 id=&#34;11-角色一缓存最常用&#34;&gt;1.1 角色一：缓存（最常用）&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# config/environments/production.rb&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;config&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;cache_store &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;:redis_cache_store&lt;/span&gt;, {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#e6db74&#34;&gt;url&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;ENV&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;[&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;REDIS_CACHE_URL&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;]&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#e6db74&#34;&gt;connect_timeout&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;,     &lt;span style=&#34;color:#75715e&#34;&gt;# 连接超时 1s，别用默认的 5s&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#e6db74&#34;&gt;read_timeout&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;5&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#e6db74&#34;&gt;write_timeout&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;5&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#e6db74&#34;&gt;error_handler&lt;/span&gt;: &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt;(method:, &lt;span style=&#34;color:#e6db74&#34;&gt;returning&lt;/span&gt;:, &lt;span style=&#34;color:#e6db74&#34;&gt;exception&lt;/span&gt;:) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#75715e&#34;&gt;# 关键：Redis 挂了降级为「缓存 miss」，而不是整个请求 500&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;Sentry&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;capture_exception(exception)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;之后入门篇第六篇讲的 &lt;code&gt;Rails.cache.fetch&lt;/code&gt;、俄罗斯套娃缓存全部自动走 Redis。两个生产级要点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;error_handler 必须配&lt;/strong&gt;。缓存的定位是加速器，Redis 抖动不该打垮主站——miss 只是变慢，异常才是事故。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缓存和队列用不同的 Redis 实例/库&lt;/strong&gt;。缓存实例可以随便清、可以开 &lt;code&gt;allkeys-lru&lt;/code&gt; 淘汰；队列实例丢数据就是丢任务。混在一个实例上，运维策略互相打架。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;三大经典问题的 Rails 解法：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;问题&lt;/th&gt;
					&lt;th&gt;解法&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;穿透（查不存在的数据）&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;Rails.cache.fetch&lt;/code&gt; 缓存 nil（&lt;code&gt;skip_nil: false&lt;/code&gt; 是默认行为，nil 也会被缓存），恶意枚举 ID 时考虑布隆过滤器&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;击穿（热点 key 过期被并发回源）&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;Rails.cache.fetch(key, race_condition_ttl: 10)&lt;/code&gt;——过期后第一个进程回源，其余继续用旧值撑 10 秒&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;雪崩（大面积同时过期）&lt;/td&gt;
					&lt;td&gt;TTL 加随机抖动：&lt;code&gt;expires_in: 30.minutes + rand(300).seconds&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&#34;12-角色二sidekiq-任务队列&#34;&gt;1.2 角色二：Sidekiq 任务队列&lt;/h3&gt;
&lt;p&gt;入门篇讲过 ActiveJob 的 API，生产上吞吐量要求高的场景底层换成 Sidekiq：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# config/application.rb&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;config&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;active_job&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;queue_adapter &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;:sidekiq&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# config/sidekiq.yml —— 队列优先级：重要队列权重高&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:queues&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  - [&lt;span style=&#34;color:#ae81ff&#34;&gt;critical, 3]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  - [&lt;span style=&#34;color:#ae81ff&#34;&gt;default, 2]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  - [&lt;span style=&#34;color:#ae81ff&#34;&gt;low, 1]&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;Sidekiq 与 ActiveJob 的关系：ActiveJob 是 Rails 的标准接口（可换后端），Sidekiq 是高性能实现。建议业务代码只写 ActiveJob API，但&lt;strong&gt;有两件事只有原生 Sidekiq 做得好&lt;/strong&gt;：定时任务（sidekiq-cron / sidekiq-scheduler）和中间件链（统一埋点、限流）。队列 Redis 的内存要按「任务堆积三天不丢」规划，持久化开 AOF。&lt;/p&gt;
&lt;h3 id=&#34;13-角色三action-cable-实时推送&#34;&gt;1.3 角色三：Action Cable 实时推送&lt;/h3&gt;
&lt;p&gt;入门篇的 Turbo Streams 底层就是 Action Cable，默认用 Redis pub/sub 做进程间广播：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# config/cable.yml&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;production&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;adapter&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;redis&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;url&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;&amp;lt;%= ENV.fetch(&amp;#34;REDIS_CABLE_URL&amp;#34;) %&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;多实例部署时这个是必须的（甲请求打到实例 A，乙的浏览器连着实例 B，靠 Redis 广播互通）。流量大了可以换 &lt;code&gt;anycable&lt;/code&gt;（Go 实现，单机连接数高一个数量级），Rails 代码不用改。&lt;/p&gt;
&lt;h3 id=&#34;14-角色四业务数据结构&#34;&gt;1.4 角色四：业务数据结构&lt;/h3&gt;
&lt;p&gt;排行榜、计数器、分布式锁、去重集合——这些直接用 Redis 数据结构，比塞进 MySQL 合适：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# Kredis：Rails 官方出品的 Redis 数据结构封装&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Post&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;ApplicationRecord&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  kredis_list &lt;span style=&#34;color:#e6db74&#34;&gt;:recent_viewer_ids&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;limit&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;100&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  kredis_counter &lt;span style=&#34;color:#e6db74&#34;&gt;:views&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  kredis_unique_list &lt;span style=&#34;color:#e6db74&#34;&gt;:tag_cache&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;limit&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;50&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;post&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;views&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;increment                    &lt;span style=&#34;color:#75715e&#34;&gt;# INCR，原子计数&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;post&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;recent_viewer_ids&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;prepend(user&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;id) &lt;span style=&#34;color:#75715e&#34;&gt;# 自动截断到 100&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;分布式锁用 &lt;code&gt;redis-mutex&lt;/code&gt; 或 Sidekiq 生态的 &lt;code&gt;sidekiq-unique-jobs&lt;/code&gt;（防止同一任务并发执行，这是任务系统最常用的锁场景）。&lt;/p&gt;
&lt;h3 id=&#34;15-追问有没有-jetcache-那种本地--redis两级缓存&#34;&gt;1.5 追问：有没有 JetCache 那种「本地 + Redis」两级缓存？&lt;/h3&gt;
&lt;p&gt;Java 的 JetCache/多层缓存方案（Caffeine 进程内 + Redis 远端 + pub/sub 广播失效）在 Ruby 生态&lt;strong&gt;没有对等的成熟 gem&lt;/strong&gt;，原因在部署模型：Java 是单进程多线程，Caffeine 缓存被整个 JVM 共享，L1 收益巨大；Ruby 应用服务器是多进程模型（Puma 5 个 worker = 5 个独立进程，内存互不相通），进程内缓存每个 worker 各存一份，命中率被摊薄、失效还得跨进程广播——收益/复杂度比不划算，生态便没有收编出「JetCache.rb」。&lt;/p&gt;
&lt;p&gt;Rails 的现实替代分三层：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;请求级 L1（内建，你已经在用）&lt;/strong&gt;：所有 &lt;code&gt;ActiveSupport::Cache&lt;/code&gt; store 都内置 &lt;code&gt;LocalCache&lt;/code&gt; 策略——同一次请求内重复读同一个 key 走进程内存，不打 Redis，请求结束自动清空，无一致性问题。它解决的是「一次请求内 N+1 次读缓存」。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;跨请求 L1（手卷，二十来行）&lt;/strong&gt;：配置/字典类「变很少、读极多」的数据，用 &lt;code&gt;Concurrent::Map&lt;/code&gt; + 短 TTL + Redis pub/sub 主动失效：&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;ConfigCache&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;TTL&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;30&lt;/span&gt;  &lt;span style=&#34;color:#75715e&#34;&gt;# 短 TTL 兜底：pub/sub 丢消息最多脏 30s&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;fetch&lt;/span&gt;(key)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    entry &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; store&lt;span style=&#34;color:#f92672&#34;&gt;[&lt;/span&gt;key&lt;span style=&#34;color:#f92672&#34;&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; entry&lt;span style=&#34;color:#f92672&#34;&gt;[&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;:value&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;]&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; entry &lt;span style=&#34;color:#f92672&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; entry&lt;span style=&#34;color:#f92672&#34;&gt;[&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;:exp&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;]&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;Time&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;now
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    store&lt;span style=&#34;color:#f92672&#34;&gt;[&lt;/span&gt;key&lt;span style=&#34;color:#f92672&#34;&gt;]&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; { &lt;span style=&#34;color:#e6db74&#34;&gt;value&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;yield&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;exp&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;TTL&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;seconds&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;from_now }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    store&lt;span style=&#34;color:#f92672&#34;&gt;[&lt;/span&gt;key&lt;span style=&#34;color:#f92672&#34;&gt;][&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;:value&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;store&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; (@store &lt;span style=&#34;color:#f92672&#34;&gt;||=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;Concurrent&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;::&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;Map&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;new)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#75715e&#34;&gt;# 启动时订阅失效广播（多 worker 各自订阅）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;listen&lt;/span&gt;(redis)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;Thread&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;new { redis&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;subscribe(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;config:invalidate&amp;#34;&lt;/span&gt;) { &lt;span style=&#34;color:#f92672&#34;&gt;|&lt;/span&gt;m&lt;span style=&#34;color:#f92672&#34;&gt;|&lt;/span&gt; store&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;delete(m&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;message) } }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Rails 8 的官方答案：Solid Cache 用本地 NVMe 当「超大 L1」&lt;/strong&gt;：与其在内存里抠几千条 entry，不如把缓存放本机 SSD（SQLite 本地文件）——容量大两个数量级、无网络往返、持久化重启不丢。思路是不优化单次读取延迟，而是把「几乎什么都缓存」变便宜。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;第二篇-elasticsearch搜索是数据同步问题&#34;&gt;第二篇 Elasticsearch：搜索是数据同步问题&lt;/h2&gt;
&lt;p&gt;Rails 接 ES，写查询语法是简单的一半，&lt;strong&gt;难的一半是「MySQL 里的数据怎么进 ES、怎么保持一致」&lt;/strong&gt;。&lt;/p&gt;
&lt;h3 id=&#34;21-gem-选型&#34;&gt;2.1 Gem 选型&lt;/h3&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;Gem&lt;/th&gt;
					&lt;th&gt;特点&lt;/th&gt;
					&lt;th&gt;建议&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;Searchkick&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;封装好、中文分词/高亮/聚合开箱即用&lt;/td&gt;
					&lt;td&gt;大多数项目的首选&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;elasticsearch-model&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;官方 gem，贴近原生 DSL&lt;/td&gt;
					&lt;td&gt;查询复杂、需要完全控制力时&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;以 Searchkick 为例：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Post&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;ApplicationRecord&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  searchkick &lt;span style=&#34;color:#e6db74&#34;&gt;word_middle&lt;/span&gt;: &lt;span style=&#34;color:#f92672&#34;&gt;[&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;:title&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;]&lt;/span&gt;,   &lt;span style=&#34;color:#75715e&#34;&gt;# title 支持中间匹配（类 LIKE %x%）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;             &lt;span style=&#34;color:#e6db74&#34;&gt;settings&lt;/span&gt;: { &lt;span style=&#34;color:#e6db74&#34;&gt;number_of_shards&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#75715e&#34;&gt;# 控制哪些字段进 ES、以什么形态进&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;search_data&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#e6db74&#34;&gt;title&lt;/span&gt;: title,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#e6db74&#34;&gt;body&lt;/span&gt;: body,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#e6db74&#34;&gt;user_name&lt;/span&gt;: user&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;name,      &lt;span style=&#34;color:#75715e&#34;&gt;# 关联字段打平进索引&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#e6db74&#34;&gt;published_at&lt;/span&gt;: published_at,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#e6db74&#34;&gt;comment_count&lt;/span&gt;: comments&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;size
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# 中文分词：ES 集群装 IK 插件后&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# Post.search &amp;#34;视频&amp;#34;, fields: [:title], 自动走配置的分析器&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;Post&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;search &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;ruby 教程&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;where&lt;/span&gt;: { &lt;span style=&#34;color:#e6db74&#34;&gt;published_at&lt;/span&gt;: { &lt;span style=&#34;color:#e6db74&#34;&gt;lt&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;Time&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;current } },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;order&lt;/span&gt;: { &lt;span style=&#34;color:#e6db74&#34;&gt;comment_count&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;:desc&lt;/span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;page&lt;/span&gt;: params&lt;span style=&#34;color:#f92672&#34;&gt;[&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;:page&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;]&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;per_page&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;20&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;&lt;code&gt;search_data&lt;/code&gt; 是核心设计点：&lt;strong&gt;ES 文档是反范式的&lt;/strong&gt;。关联表的字段（作者名、评论数）要打平进文档，否则查询时要回库 join，ES 的意义就没了。代价是：用户名改了，所有相关文档都要重建索引——这就是下一节的问题。&lt;/p&gt;
&lt;h3 id=&#34;22-索引同步三种策略的取舍&#34;&gt;2.2 索引同步：三种策略的取舍&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    W[&amp;#34;业务写入 MySQL&amp;#34;] --&amp;gt; S1[&amp;#34;策略A Model 回调&amp;lt;br/&amp;gt;同步写 ES&amp;#34;]
    W --&amp;gt; S2[&amp;#34;策略B 后台 Job&amp;lt;br/&amp;gt;异步写 ES&amp;#34;]
    W --&amp;gt; S3[&amp;#34;策略C CDC&amp;lt;br/&amp;gt;binlog 监听管道&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;策略 A：回调同步写&lt;/strong&gt;（Searchkick 默认 &lt;code&gt;after_commit&lt;/code&gt;）。简单强一致，但 ES 抖动直接拖慢/打挂业务写入，只适合内部系统。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;策略 B：回调只入队，Job 异步写&lt;/strong&gt;（&lt;code&gt;Searchkick&lt;/code&gt; 配置 &lt;code&gt;Searchkick.callbacks = :async&lt;/code&gt;，或自写 &lt;code&gt;after_commit { ReindexJob.perform_later(self) }&lt;/code&gt;）。&lt;strong&gt;大多数项目的正确选择&lt;/strong&gt;：写入不被 ES 拖累，失败可重试。注意 Job 里要重新从 DB 读数据再写 ES（防止任务执行时数据已过期，写入陈旧版本）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;策略 C：CDC 管道&lt;/strong&gt;（MySQL binlog → Canal/Debezium → Kafka → 消费写 ES）。业务代码零侵入，适合写入量大、文档来源多的大系统，代价是多维护一条数据管道。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;23-零停机重建索引&#34;&gt;2.3 零停机重建索引&lt;/h3&gt;
&lt;p&gt;Mapping 变更（加字段、改分词）必须重建索引。标准流程靠&lt;strong&gt;别名&lt;/strong&gt;：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# 1. 建新索引 posts_v2，全量灌数据&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;Post&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;reindex  &lt;span style=&#34;color:#75715e&#34;&gt;# Searchkick 自动建带时间戳的新索引并切换别名&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# 底层原理：读永远走别名 posts → 指向 posts_v1 或 posts_v2&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# reindex 完成后别名原子切换，全程无读中断&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;Searchkick 的 &lt;code&gt;reindex&lt;/code&gt; 默认就是这个模式，手工玩原生 API 时记住同一原则：&lt;strong&gt;读别名，不写别名；切换原子操作&lt;/strong&gt;。&lt;/p&gt;
&lt;h3 id=&#34;24-生产上死在哪&#34;&gt;2.4 生产上死在哪&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;写放大&lt;/strong&gt;：热门对象高频更新（计数器、状态），每次都触发 reindex → 把高频字段拆出搜索文档，或改用 &lt;code&gt;update&lt;/code&gt; 部分更新。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;深分页&lt;/strong&gt;：&lt;code&gt;from + size&lt;/code&gt; 超过 1 万直接报错，列表导出场景用 &lt;code&gt;scroll&lt;/code&gt; 或 &lt;code&gt;search_after&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ES 当主库用&lt;/strong&gt;：ES 不是数据库——近实时（默认 1s 刷新）、无事务。写后立刻要读的场景，读 MySQL 兜底，别调 &lt;code&gt;refresh&lt;/code&gt; 硬扛。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2 id=&#34;第三篇-消息队列activejob-不是-mq&#34;&gt;第三篇 消息队列：ActiveJob 不是 MQ&lt;/h2&gt;
&lt;p&gt;先澄清一个普遍误解：&lt;strong&gt;ActiveJob/Sidekiq 是「任务队列」，不是「消息队列」&lt;/strong&gt;。两者的区别决定了选型：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;&lt;/th&gt;
					&lt;th&gt;任务队列（Sidekiq）&lt;/th&gt;
					&lt;th&gt;消息队列（RabbitMQ/Kafka）&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;语义&lt;/td&gt;
					&lt;td&gt;「替我执行这段代码」&lt;/td&gt;
					&lt;td&gt;「广播一个事实，谁关心谁消费」&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;消费方&lt;/td&gt;
					&lt;td&gt;只有自己系统的 worker&lt;/td&gt;
					&lt;td&gt;可以是任意多个异构系统&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;典型场景&lt;/td&gt;
					&lt;td&gt;发邮件、生成报表&lt;/td&gt;
					&lt;td&gt;订单已支付 → 通知库存/物流/积分/大数据&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Rails 内部异步用 ActiveJob 就够了；&lt;strong&gt;当事件的消费者不止你的 Rails 应用&lt;/strong&gt;（要对接 Java 服务、数据团队、第三方），才需要真 MQ。&lt;/p&gt;
&lt;h3 id=&#34;31-rabbitmq可靠投递优先&#34;&gt;3.1 RabbitMQ：可靠投递优先&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# bunny 是 Ruby 的 RabbitMQ 客户端&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;conn &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;Bunny&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;new(&lt;span style=&#34;color:#66d9ef&#34;&gt;ENV&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;[&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;RABBITMQ_URL&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;]&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;tap(&lt;span style=&#34;color:#f92672&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;:start&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;ch &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; conn&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;create_channel
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;exchange &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; ch&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;topic(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;orders&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;durable&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;true&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# 发布：持久化消息 + 发送方确认&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;exchange&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;publish(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  { &lt;span style=&#34;color:#e6db74&#34;&gt;order_id&lt;/span&gt;: order&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;id, &lt;span style=&#34;color:#e6db74&#34;&gt;event&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;paid&amp;#34;&lt;/span&gt; }&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;to_json,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#e6db74&#34;&gt;routing_key&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;order.paid&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#e6db74&#34;&gt;persistent&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;true&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#e6db74&#34;&gt;message_id&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;SecureRandom&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;uuid   &lt;span style=&#34;color:#75715e&#34;&gt;# 供消费方幂等去重&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;消费侧用 &lt;code&gt;sneakers&lt;/code&gt;（基于 bunny 的 worker 框架）。RabbitMQ 的关键概念是 exchange/routing key/queue 三层路由，灵活但概念多；适合「企业内部可靠事件通知」。&lt;/p&gt;
&lt;h3 id=&#34;32-kafka吞吐与回放优先&#34;&gt;3.2 Kafka：吞吐与回放优先&lt;/h3&gt;
&lt;p&gt;Rails 社区接 Kafka 的标准答案是 &lt;strong&gt;Karafka&lt;/strong&gt;（用法神似 Rails 路由）：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# app/consumers/order_events_consumer.rb&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;OrderEventsConsumer&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;ApplicationConsumer&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;consume&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    messages&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;each &lt;span style=&#34;color:#66d9ef&#34;&gt;do&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;|&lt;/span&gt;message&lt;span style=&#34;color:#f92672&#34;&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      event &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;JSON&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;parse(message&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;payload)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#66d9ef&#34;&gt;OrderEventHandler&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;call(event)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# karafka.rb —— 声明消费哪个 topic&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;routes&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;draw &lt;span style=&#34;color:#66d9ef&#34;&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  topic &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;order_events&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    consumer &lt;span style=&#34;color:#66d9ef&#34;&gt;OrderEventsConsumer&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;发布侧用 &lt;code&gt;WaterDrop&lt;/code&gt;（Karafka 配套的 producer）：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;WaterDrop&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;::&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;SyncProducer&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;call(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  { &lt;span style=&#34;color:#e6db74&#34;&gt;event&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;order.paid&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;order_id&lt;/span&gt;: order&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;id }&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;to_json,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#e6db74&#34;&gt;topic&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;order_events&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#e6db74&#34;&gt;key&lt;/span&gt;: order&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;id&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;to_s   &lt;span style=&#34;color:#75715e&#34;&gt;# 同 key 进同分区 → 同一订单的事件保序&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;Kafka 相比 RabbitMQ 的独特价值：&lt;strong&gt;消息可回放&lt;/strong&gt;（消费位点重置就能重跑历史事件）和&lt;strong&gt;分区保序&lt;/strong&gt;。大数据团队要全量订单事件时，只有 Kafka 这条路合适。&lt;/p&gt;
&lt;h3 id=&#34;33-消费侧的三个生死问题&#34;&gt;3.3 消费侧的三个生死问题&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;幂等&lt;/strong&gt;：MQ 的投递语义是「至少一次」，重复消费是常态不是异常。每个消费者都必须能安全重放：&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;call&lt;/span&gt;(event)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#75715e&#34;&gt;# 用 message_id 去重，或用业务唯一键 upsert&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;ProcessedEvent&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;exists?(&lt;span style=&#34;color:#e6db74&#34;&gt;message_id&lt;/span&gt;: event&lt;span style=&#34;color:#f92672&#34;&gt;[&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;message_id&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;]&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;ActiveRecord&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;::&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;Base&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;transaction &lt;span style=&#34;color:#66d9ef&#34;&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;ProcessedEvent&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;create!(&lt;span style=&#34;color:#e6db74&#34;&gt;message_id&lt;/span&gt;: event&lt;span style=&#34;color:#f92672&#34;&gt;[&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;message_id&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;]&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    handle(event)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;ol start=&#34;2&#34;&gt;
&lt;li&gt;&lt;strong&gt;死信&lt;/strong&gt;：处理失败不能无限重试（一条坏消息卡死整个分区）。重试 N 次后进死信队列，告警 + 人工/延时处理。Karafka 有 Dead Letter Queue 原生支持，RabbitMQ 配 DLX。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;发布可靠性&lt;/strong&gt;：「先写库后发消息」存在窗口期——库写了、进程挂了、消息没发。解法是 &lt;strong&gt;Outbox 模式&lt;/strong&gt;：业务数据和 outbox 事件表在同一个事务里写库，后台进程轮询 outbox 表投递 MQ，投递成功标记已发。数据一致性要求高的链路（钱、库存）必须上 outbox，不能裸发。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2 id=&#34;第四篇-大数据组件先想清楚-rails-的角色&#34;&gt;第四篇 大数据组件：先想清楚 Rails 的角色&lt;/h2&gt;
&lt;p&gt;标题里最常见的认知偏差是「Rails 怎么接 Spark」。正确的问题其实是：&lt;strong&gt;在大数据架构里，Rails 应用应该站在什么位置？&lt;/strong&gt; 答案是——Rails 几乎从不「接」Spark，它站在管道的两端：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    A[&amp;#34;Rails 应用&amp;lt;br/&amp;gt;业务数据库 MySQL/PG&amp;#34;] --&amp;gt;|&amp;#34;CDC / 事件 Kafka / 定时导出&amp;#34;| B[&amp;#34;数据管道&amp;lt;br/&amp;gt;Kafka&amp;#34;]
    B --&amp;gt; C[&amp;#34;计算引擎&amp;lt;br/&amp;gt;Spark / Flink&amp;#34;]
    C --&amp;gt; D[&amp;#34;分析存储&amp;lt;br/&amp;gt;ClickHouse / ES / Hive&amp;#34;]
    D --&amp;gt;|&amp;#34;API / 只读库 / 回写&amp;#34;| E[&amp;#34;Rails 应用&amp;lt;br/&amp;gt;展示分析结果&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h3 id=&#34;41-上游角色把数据交出去&#34;&gt;4.1 上游角色：把数据交出去&lt;/h3&gt;
&lt;p&gt;Rails 是业务数据的源头，交出去的方式按实时性递增：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;定时导出&lt;/strong&gt;（小时/天级）：&lt;code&gt;find_each&lt;/code&gt; 分批读库导 CSV/Parquet 上传对象存储。简单可靠，报表场景够用。注意 &lt;code&gt;find_each&lt;/code&gt; 默认按主键分批，千万别在导大表时用 &lt;code&gt;each&lt;/code&gt;（全量加载内存爆炸）或 &lt;code&gt;offset&lt;/code&gt; 分页（越深越慢）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Kafka 事件&lt;/strong&gt;（秒级）：重要业务事件按第三篇的方式发 Kafka，数据团队自己消费。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CDC&lt;/strong&gt;（准实时、全量变更）：Debezium/Canal 监听 binlog，业务代码零侵入。数据团队的最爱——不用求你在每个 Model 里埋事件。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;原则：同一份数据只选一条路&lt;/strong&gt;，既发 Kafka 又跑 CDC 会造成数据团队收到双份口径。&lt;/p&gt;
&lt;h3 id=&#34;42-下游角色把结果拿回来&#34;&gt;4.2 下游角色：把结果拿回来&lt;/h3&gt;
&lt;p&gt;Spark/Flink 算完的结果（用户画像、推荐列表、统计报表），Rails 消费的方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;读分析库&lt;/strong&gt;：ClickHouse/ES 直接开只读连接给 Rails（用 &lt;code&gt;establish_connection&lt;/code&gt; 配第二数据源），OLAP 查询走它，别打 MySQL。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;回写业务库&lt;/strong&gt;：推荐结果、用户分群这类要参与业务逻辑的，由数据管道回写 MySQL/Redis，Rails 无感知地读——&lt;strong&gt;首选方案，耦合最低&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内部 HTTP API&lt;/strong&gt;：数据团队提供服务接口，Rails 用 Faraday/HTTParty 调。适用跨团队边界，记得按第五篇配超时熔断。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;43-反模式警告&#34;&gt;4.3 反模式警告&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;不要在 Rails 进程里嵌 Spark 客户端跑计算——Ruby 不在 JVM 大数据生态里，硬接只有痛苦。&lt;/li&gt;
&lt;li&gt;不要让 Spark 作业直接写 Rails 的在线 MySQL 主库——批量回写打爆主库的事故年年有，回写写从库同步的旁表或走限速的 API。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;第五篇-通用纪律接入任何组件前的检查清单&#34;&gt;第五篇 通用纪律：接入任何组件前的检查清单&lt;/h2&gt;
&lt;p&gt;前四篇的教训抽象出来，是一套与具体组件无关的纪律。以后接任何外部服务（HTTP API、对象存储、AI 模型接口都一样），按这张清单过一遍：&lt;/p&gt;
&lt;h3 id=&#34;51-超时永远不依赖默认值&#34;&gt;5.1 超时：永远不依赖默认值&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# HTTP 客户端：连接/读/写三个超时分开设&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;Faraday&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;new(&lt;span style=&#34;color:#e6db74&#34;&gt;url&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;https://api.example.com&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#66d9ef&#34;&gt;do&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;|&lt;/span&gt;f&lt;span style=&#34;color:#f92672&#34;&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  f&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;options&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;timeout &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;3&lt;/span&gt;        &lt;span style=&#34;color:#75715e&#34;&gt;# 读超时&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  f&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;options&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;open_timeout &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;   &lt;span style=&#34;color:#75715e&#34;&gt;# 连接超时必须更短&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;默认超时几乎都是灾难（Ruby 的 Net::HTTP 读超时默认 60s——下游挂了，你的 Puma 线程全部吊死 60 秒，线程池耗尽，全站 500）。&lt;strong&gt;所有跨进程调用必须显式设超时，且要比上游网关的超时短&lt;/strong&gt;。&lt;/p&gt;
&lt;h3 id=&#34;52-重试只对幂等操作且带退避&#34;&gt;5.2 重试：只对幂等操作，且带退避&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;retry_on &lt;span style=&#34;color:#66d9ef&#34;&gt;Net&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;::&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;OpenTimeout&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;wait&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;:exponentially_longer&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;attempts&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;5&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;重试放大故障——下游已经过载，你的重试是在 DDoS 它。规则：GET/幂等操作才可重试；次数有上限；间隔指数退避加抖动。&lt;/p&gt;
&lt;h3 id=&#34;53-熔断下游持续失败就快速失败&#34;&gt;5.3 熔断：下游持续失败就快速失败&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# stoplight gem：失败率超阈值 → 熔断器打开，直接抛错不发起请求&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;light &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;Stoplight&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;es-search&amp;#34;&lt;/span&gt;) { &lt;span style=&#34;color:#66d9ef&#34;&gt;Post&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;search(query) }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;light&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;with_fallback { &lt;span style=&#34;color:#66d9ef&#34;&gt;Post&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;where(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;title LIKE ?&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;%&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;#{&lt;/span&gt;query&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;%&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;limit(&lt;span style=&#34;color:#ae81ff&#34;&gt;20&lt;/span&gt;) }  &lt;span style=&#34;color:#75715e&#34;&gt;# 降级：回退 DB 模糊查&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                       &lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;run&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;没有熔断的系统，下游慢 = 全站慢。熔断器的意义是&lt;strong&gt;保护自己的线程池&lt;/strong&gt;，顺带给下游喘息时间。配一个配置中心的降级开关（如 &lt;code&gt;features.es_search = false&lt;/code&gt; 时直接走 DB），是大流量系统的标准配置。&lt;/p&gt;
&lt;h3 id=&#34;54-幂等写操作带唯一键&#34;&gt;5.4 幂等：写操作带唯一键&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;对外发消息：带 &lt;code&gt;message_id&lt;/code&gt;（见第三篇）。&lt;/li&gt;
&lt;li&gt;对外调写 API：带 &lt;code&gt;Idempotency-Key&lt;/code&gt; header（Stripe 系 API 的标准）。&lt;/li&gt;
&lt;li&gt;自己提供写接口：用唯一索引兜底（&lt;code&gt;add_index :payments, :idempotency_key, unique: true&lt;/code&gt;），重复请求撞索引返回原结果。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;55-连接池与背压&#34;&gt;5.5 连接池与背压&lt;/h3&gt;
&lt;p&gt;每个组件的连接池都要和 Puma 并发模型对齐：Puma 5 workers × 16 threads = 80 并发 → DB/Redis 连接池至少 80 → 而下游服务/数据库能不能扛 80 个长连接，要算。Sidekiq 的并发（默认 5~25）单独算一份。连接数规划错位是生产事故的高发区。&lt;/p&gt;
&lt;h3 id=&#34;56-可观测性每次外部调用都埋点&#34;&gt;5.6 可观测性：每次外部调用都埋点&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# 用 instrument 包裹外部调用，自动记录耗时，可接入 StatsD/Prometheus&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;ActiveSupport&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;::&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;Notifications&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;instrument(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;external.es_search&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;index&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;posts&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#66d9ef&#34;&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;Post&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;search(query)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;加上 SkyWalking/OpenTelemetry 做分布式追踪（Rails 侧 &lt;code&gt;opentelemetry-instrumentation-rails&lt;/code&gt; 一行接入），每个外部组件的耗时、错误率、QPS 都要能在监控面板上单独看到——出问题时你才知道是哪个组件在拖。&lt;/p&gt;
&lt;h3 id=&#34;57-生态边界不是所有中间件都有-ruby-版&#34;&gt;5.7 生态边界：不是所有中间件都有 Ruby 版&lt;/h3&gt;
&lt;p&gt;接入任何组件前先问一句：&lt;strong&gt;这是本生态的活法吗？&lt;/strong&gt; 最典型的例子是 Nacos/Sentinel 这类 Spring Cloud 全家桶——Nacos 官方只维护 Java/Go/Python 客户端，Ruby 没有可用 SDK。这不是生态缺陷，而是因为它们解决的问题在 Rails 世界各有归宿：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;服务发现&lt;/strong&gt;：Nacos 是客户端发现模式（Feign 需要实例列表），Rails 文化是单体优先；即使微服务化，K8s Service DNS / 云 LB 这类&lt;strong&gt;服务端发现&lt;/strong&gt;已把注册中心吃掉了，调用方根本不需要知道实例列表。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;配置中心&lt;/strong&gt;：部署期配置用 ENV + ConfigMap，密钥用 credentials，动态开关用 &lt;code&gt;flipper&lt;/code&gt;，业务热配置一张 config 表加短 TTL 缓存就够。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;限流降级&lt;/strong&gt;：Sentinel 没有 Ruby 版，对应物是 Rack::Attack（限流）+ stoplight（熔断）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果公司基础设施已经是 Nacos（Rails 服务必须融入），原则是&lt;strong&gt;让平台层消化异构，不要在应用里养半成品客户端&lt;/strong&gt;：优先走 K8s 部署 + 网关适配（Rails 应用完全无感知）；必须注册进 Nacos 时，用 OpenAPI 在启动脚本里注册实例 + 定时心跳（十几行脚本的事），配置拉取同理。SDK 的连接管理、本地缓存、监听重连这些核心行为没有维护好的 Ruby 实现，自己扛就是给自己找事故。&lt;/p&gt;
&lt;h3 id=&#34;58-对象存储文件别过应用服务器&#34;&gt;5.8 对象存储：文件别过应用服务器&lt;/h3&gt;
&lt;p&gt;入门篇 §6.3 讲了 Active Storage 的用法，生产接 S3/OSS 时核心纪律只有一条：&lt;strong&gt;文件不经过 Rails 进程&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;默认链路是 浏览器 → Rails → S3：一个 100MB 上传占住一个 Puma worker 几十秒，几个并发上传就能吃光线程池（入门篇 §8.2 的并发模型在这里兑现成事故）。正确姿势是 &lt;strong&gt;Direct Upload&lt;/strong&gt;：浏览器先向 Rails 申请预签名 URL（Active Storage 自带 &lt;code&gt;/rails/active_storage/direct_uploads&lt;/code&gt; 端点），然后直传 S3，表单提交时只带一个 signed_id：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# 视图里一行开启&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;form&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;file_field &lt;span style=&#34;color:#e6db74&#34;&gt;:video&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;direct_upload&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;true&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;配套三件事：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;公开资源走 CDN 回源&lt;/strong&gt;：别让每个头像请求都打到 S3（流量费 + 延迟双输）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;私有文件用短时效签名 URL&lt;/strong&gt;：&lt;code&gt;rails_blob_url&lt;/code&gt; 的链接永久有效且经 Rails 代理；敏感文件用 &lt;code&gt;avatar.url(expires_in: 5.minutes)&lt;/code&gt; 直出 S3。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;图片变体改异步预热&lt;/strong&gt;：默认首次访问才懒生成缩略图（慢且占 worker），上传完成后用 Job 预生成（&lt;code&gt;avatar.variant(resize_to_limit: [200, 200]).processed&lt;/code&gt;）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;还有两个自动化问题必须回答，否则上线就是给运维埋雷：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;垃圾谁来清？不会自动发生，要两道工序。&lt;/strong&gt; 一是应用侧：浏览器直传成功但用户没提交表单，留下「传了但没关联」的孤儿 blob——Rails 不自动清，挂周期任务扫：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;ActiveStorage&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;::&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;Blob&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;unattached&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;where(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;created_at &amp;lt; ?&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;2&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;days&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;ago)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;find_each(&lt;span style=&#34;color:#f92672&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;:purge_later&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;二是存储侧：分片上传（multipart）中断会在 S3 留下残片，&lt;strong&gt;S3 对残片照常收费&lt;/strong&gt;——bucket 上必须配 Lifecycle 规则 &lt;code&gt;AbortIncompleteMultipartUpload&lt;/code&gt;（如 7 天后中止），不配就是漏水的账单。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;传完后怎么触发后续处理？有现成挂点。&lt;/strong&gt; blob 关联到记录时，Active Storage 自动排 &lt;code&gt;AnalyzeJob&lt;/code&gt; 提取元数据（宽高、时长、校验和），这是内置的「消息」；业务级处理（转码、扫毒、抽帧）挂在自己模型的回调上：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Video&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;ApplicationRecord&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  has_one_attached &lt;span style=&#34;color:#e6db74&#34;&gt;:file&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  after_save_commit &lt;span style=&#34;color:#e6db74&#34;&gt;:enqueue_transcode&lt;/span&gt;, &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt;: &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; { file&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;attached? }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;private&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;enqueue_transcode&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;TranscodeJob&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;perform_later(id)   &lt;span style=&#34;color:#75715e&#34;&gt;# 走 §6.1 的 Active Job 链路，别在请求里做&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;只有绕过 Active Storage 直接对接桶事件的大文件管道，才需要 S3 Event Notification → SQS → 消费者的路子——那是第三篇 MQ 的地盘，一般项目用不上。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;收尾集成的本质是边界管理&#34;&gt;收尾：集成的本质是边界管理&lt;/h2&gt;
&lt;p&gt;回头看这五篇，真正难的技术点一个都没有——gem 都现成，文档都齐全。难的是&lt;strong&gt;边界上的纪律&lt;/strong&gt;：数据跨进程之后，一致性、时序、失败语义全都变了。最后三条心法：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;每接一个组件，先写「它挂了怎么办」&lt;/strong&gt;。缓存挂了降级、ES 挂了回 DB、MQ 挂了走 outbox 堆积——降级路径没设计的集成，等于给系统埋雷。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据流向永远是单向的&lt;/strong&gt;。业务库 → 搜索/分析/缓存，反向只读不写。双向同步的系统没有一个不出数据事故的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;组件越少越好&lt;/strong&gt;。每接一个组件都要回答「现有组件真的解决不了吗」。SQLite 能扛的缓存别上 Redis，Sidekiq 能扛的异步别上 Kafka——&lt;strong&gt;架构的复杂度成本是持续的运维和认知负担，不是一次性接入&lt;/strong&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;至此，从入门篇的「一个人写一个应用」，到这一篇的「让应用成为系统版图里可靠的一环」，Rails 工程师的完整拼图就齐了。&lt;/p&gt;
</description>
    </item>
    <item>
      <title>我开发的keCache：面向 Spring Boot 3 的 L1 &#43; L2 两级缓存框架</title>
      <link>https://ftmao.com/blog/posts/kecache-intro/</link>
      <pubDate>Thu, 04 Sep 2025 14:16:44 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/kecache-intro/</guid>
      <description>&lt;p&gt;在高并发 Java 业务里，缓存几乎是标配。但“用缓存”这件事从来不是只有“加 Redis”一种答案：本地缓存快却解决不了多机一致，Redis 能共享却多了网络与序列化开销。我最近实现一个把两者结合起来、并针对集群一致性做了不少细节处理的小而美框架 —— &lt;strong&gt;keCache&lt;/strong&gt;，现在做一次系统梳理。&lt;/p&gt;
&lt;h2 id=&#34;一kecache-是什么&#34;&gt;一、keCache 是什么&lt;/h2&gt;
&lt;p&gt;keCache 是一个基于 &lt;strong&gt;Java 17 + Spring Boot 3.x + Caffeine + Redisson&lt;/strong&gt; 的本地缓存框架，采用 &lt;strong&gt;L1（Caffeine 本地缓存）+ L2（Redis 分布式缓存）&lt;/strong&gt; 两级架构，同时提供：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;声明式注解（&lt;code&gt;@LocalCacheable&lt;/code&gt;、&lt;code&gt;@LocalCacheEvict&lt;/code&gt;、&lt;code&gt;@LocalCacheClear&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;编程式 API（&lt;code&gt;CacheTemplate&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;集群失效广播（Redisson Pub/Sub）&lt;/li&gt;
&lt;li&gt;空值保护（防止缓存穿透）&lt;/li&gt;
&lt;li&gt;数据一致性校准 SPI&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;它的定位不是“又一个 Redis 客户端”，而是“让本地缓存能在集群环境下安全使用”的胶水层。&lt;/p&gt;
&lt;h3 id=&#34;11-整体架构&#34;&gt;1.1 整体架构&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;graph TD
    App[业务 Service] --&amp;gt;|注解或 API| Facade[&amp;#34;CacheTemplate / AOP 切面&amp;#34;]
    Facade --&amp;gt; L1{L1 Caffeine}
    L1 --&amp;gt;|命中| Return[返回数据]
    L1 --&amp;gt;|未命中| L2{L2 Redis}
    L2 --&amp;gt;|命中| Deserialize[Jackson 反序列化]
    Deserialize --&amp;gt; Backfill[回填 L1]
    Backfill --&amp;gt; Return
    L2 --&amp;gt;|未命中| Load[&amp;#34;执行方法 / 加载数据&amp;#34;]
    Load --&amp;gt; Write[&amp;#34;写入 L1 &amp;amp; L2&amp;#34;]
    Write --&amp;gt; Return
    PubSub[&amp;#34;Redisson Pub/Sub&amp;#34;] --&amp;gt;|失效事件| L1
    Calibration[CalibrationProvider SPI] --&amp;gt;|定时校准| L2&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h2 id=&#34;二开源地址&#34;&gt;二、开源地址&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Gitee：&lt;a href=&#34;https://gitee.com/soulnew/ke-cache&#34;&gt;https://gitee.com/soulnew/ke-cache&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;三为什么需要-kecache与纯-redis--纯-caffeine-的对比&#34;&gt;三、为什么需要 keCache？与纯 Redis / 纯 Caffeine 的对比&lt;/h2&gt;
&lt;p&gt;很多团队在选择缓存方案时，常常在“纯 Redis”和“纯 Caffeine”之间摇摆。keCache 试图在二者之间取得平衡。&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;维度&lt;/th&gt;
					&lt;th&gt;纯 Caffeine&lt;/th&gt;
					&lt;th&gt;纯 Redis&lt;/th&gt;
					&lt;th&gt;keCache（L1 + L2）&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;读取延迟&lt;/td&gt;
					&lt;td&gt;极低（内存内）&lt;/td&gt;
					&lt;td&gt;较高（网络 + 序列化）&lt;/td&gt;
					&lt;td&gt;热数据近似 Caffeine，冷数据走 Redis&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;单机内存压力&lt;/td&gt;
					&lt;td&gt;大（全部数据在本 JVM）&lt;/td&gt;
					&lt;td&gt;小（Redis 承担）&lt;/td&gt;
					&lt;td&gt;小（只有热数据在本地）&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;集群一致性&lt;/td&gt;
					&lt;td&gt;无，各节点独立加载&lt;/td&gt;
					&lt;td&gt;强一致&lt;/td&gt;
					&lt;td&gt;最终一致，写操作广播失效&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;缓存穿透保护&lt;/td&gt;
					&lt;td&gt;需自行实现&lt;/td&gt;
					&lt;td&gt;需自行实现&lt;/td&gt;
					&lt;td&gt;内置 NullValue 短 TTL 保护&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;多机共享&lt;/td&gt;
					&lt;td&gt;不支持&lt;/td&gt;
					&lt;td&gt;支持&lt;/td&gt;
					&lt;td&gt;支持（L2 Redis）&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;适用场景&lt;/td&gt;
					&lt;td&gt;只读/准静态数据、单节点&lt;/td&gt;
					&lt;td&gt;写多读少、需要强一致&lt;/td&gt;
					&lt;td&gt;读多写少、可接受最终一致&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&#34;31-相比纯-caffeine-的好处&#34;&gt;3.1 相比纯 Caffeine 的好处&lt;/h3&gt;
&lt;p&gt;Caffeine 是 JVM 内缓存的佼佼者，但它天然只服务于当前进程。在集群部署时：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;每台机器都要独立加载数据&lt;/strong&gt;，导致数据库在启动/预热时被打 N 次。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据更新后其他节点无法感知&lt;/strong&gt;，容易出现 A 节点已更新、B 节点仍读旧值的“ stale data ”问题。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内存占用随节点数线性增长&lt;/strong&gt;，同样一份热数据在每个 JVM 都存一份。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;keCache 用 L2 Redis 作为共享底座：首次加载从 Redis 取，后续读请求命中本地 Caffeine；写操作时通过 Redis Pub/Sub 通知其他节点清除本地缓存。这样既保留了 Caffeine 的本地速度，又解决了多机一致性难题。&lt;/p&gt;
&lt;h3 id=&#34;32-相比纯-redis-的好处&#34;&gt;3.2 相比纯 Redis 的好处&lt;/h3&gt;
&lt;p&gt;Redis 虽然性能好，但毕竟跨进程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;网络 RTT 不可忽视&lt;/strong&gt;：高频读取时，哪怕 1ms 也会累积成可观的延迟。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;序列化/反序列化开销&lt;/strong&gt;：Java 对象要先转成 JSON/二进制，再还原，CPU 和 GC 都有压力。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;热点 Key 风险&lt;/strong&gt;：大促期间某些热 Key 会被反复打到同一个 Redis 节点。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;keCache 把最热的数据放在本地 JVM 里，只有本地未命中或更新时才访问 Redis，显著降低网络和序列化开销，也分散了 Redis 热点压力。&lt;/p&gt;
&lt;h3 id=&#34;33-方案选型速查&#34;&gt;3.3 方案选型速查&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    Start([开始选缓存方案]) --&amp;gt; Q1{是否需要多机共享?}
    Q1 --&amp;gt;|否| Q2{数据是否只读 /&amp;lt;br/&amp;gt;准静态?}
    Q2 --&amp;gt;|是| Caffeine[纯 Caffeine&amp;lt;br/&amp;gt;简单高效]
    Q2 --&amp;gt;|否| Q3{能否接受最终一致?}
    Q3 --&amp;gt;|能| KeCache[keCache&amp;lt;br/&amp;gt;L1 + L2 加速]
    Q3 --&amp;gt;|否| RedisOnly[纯 Redis /&amp;lt;br/&amp;gt;DB 强一致]
    Q1 --&amp;gt;|是| Q4{写多读少 /&amp;lt;br/&amp;gt;强一致?}
    Q4 --&amp;gt;|是| RedisOnly
    Q4 --&amp;gt;|否| KeCache&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h3 id=&#34;34-一句话总结&#34;&gt;3.4 一句话总结&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;keCache 适合“读多写少、能接受最终一致、希望兼顾本地速度与集群共享”的业务场景；如果你的业务对实时一致性要求极高（如库存扣减、金融记账），建议直接走 Redis 或数据库方案。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&#34;四核心能力速览&#34;&gt;四、核心能力速览&lt;/h2&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;能力&lt;/th&gt;
					&lt;th&gt;说明&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;声明式缓存&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;@LocalCacheable&lt;/code&gt; / &lt;code&gt;@LocalCacheEvict&lt;/code&gt; / &lt;code&gt;@LocalCacheClear&lt;/code&gt;，支持 SpEL&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;多键映射&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;@LocalCacheable.Multi&lt;/code&gt; 一次方法返回构建多个索引 key&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;编程式 API&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;CacheTemplate.get/put/evict/clear&lt;/code&gt;，类型安全&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;二级缓存&lt;/td&gt;
					&lt;td&gt;L1 Caffeine + L2 Redis，读时先 L1 再 L2，未命中回调加载&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;空值保护&lt;/td&gt;
					&lt;td&gt;DB 不存在的 key 缓存短 TTL 的 NullValue，防止缓存穿透&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;集群一致性&lt;/td&gt;
					&lt;td&gt;Redisson Pub/Sub 广播失效事件，其他 JVM 清除本地 L1&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;自失效屏蔽&lt;/td&gt;
					&lt;td&gt;事件携带 &lt;code&gt;sourceId&lt;/code&gt;，本机忽略自己发出的事件，避免误清本地缓存&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;广播风暴防护&lt;/td&gt;
					&lt;td&gt;远程失效事件只清 L1，不再次广播&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;数据校准&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;CalibrationProvider&lt;/code&gt; SPI，支持时间戳扫描或 CDC 扩展&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&#34;41-读取流程&#34;&gt;4.1 读取流程&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    Start([读请求]) --&amp;gt; L1[查 L1 Caffeine]
    L1 --&amp;gt;|命中| A[直接返回]
    L1 --&amp;gt;|未命中| L2[查 L2 Redis]
    L2 --&amp;gt;|命中| Deserialize[Jackson 反序列化]
    Deserialize --&amp;gt; Backfill[回填 L1]
    Backfill --&amp;gt; Return[返回数据]
    L2 --&amp;gt;|未命中| Method[执行真实方法]
    Method --&amp;gt; NullCheck{结果是否为 null?}
    NullCheck --&amp;gt;|是| NullVal[写入 NullValue 到 L1/L2&amp;lt;br/&amp;gt;TTL: null-value-ttl]
    NullCheck --&amp;gt;|否| RealVal[写入真实值到 L1/L2]
    NullVal --&amp;gt; ReturnNull[返回 null]
    RealVal --&amp;gt; Return&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h3 id=&#34;42-写入与集群失效广播流程&#34;&gt;4.2 写入与集群失效广播流程&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;sequenceDiagram
    participant A as 节点 A（写请求）
    participant DB as 数据库
    participant R as Redis（L2）
    participant P as Redis Pub/Sub
    participant B as 节点 B
    participant C as 节点 C

    A-&amp;gt;&amp;gt;DB: 更新数据
    A-&amp;gt;&amp;gt;R: 删除/覆盖 L2 Key
    A-&amp;gt;&amp;gt;P: 发布 EvictEvent（携带 sourceId）
    A-&amp;gt;&amp;gt;A: 清除本机 L1

    P-&amp;gt;&amp;gt;B: 接收 EvictEvent
    B-&amp;gt;&amp;gt;B: sourceId ≠ 本机？
    B-&amp;gt;&amp;gt;B: 清除本地 L1
    Note over B: 只清 L1，不再次广播

    P-&amp;gt;&amp;gt;C: 接收 EvictEvent
    C-&amp;gt;&amp;gt;C: sourceId ≠ 本机？
    C-&amp;gt;&amp;gt;C: 清除本地 L1&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h2 id=&#34;五快速开始&#34;&gt;五、快速开始&lt;/h2&gt;
&lt;h3 id=&#34;51-引入-starter&#34;&gt;5.1 引入 Starter&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-xml&#34; data-lang=&#34;xml&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;com.keke&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;kecache-spring-boot-starter&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;version&amp;gt;&lt;/span&gt;1.0.0-SNAPSHOT&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;52-启用缓存&#34;&gt;5.2 启用缓存&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; com.keke.kecache.starter.annotation.EnableKeCache;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; org.springframework.boot.autoconfigure.SpringBootApplication;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;@SpringBootApplication&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;@EnableKeCache&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Application&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;void&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;main&lt;/span&gt;(String&lt;span style=&#34;color:#f92672&#34;&gt;[]&lt;/span&gt; args) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        SpringApplication.&lt;span style=&#34;color:#a6e22e&#34;&gt;run&lt;/span&gt;(Application.&lt;span style=&#34;color:#a6e22e&#34;&gt;class&lt;/span&gt;, args);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;53-基础配置&#34;&gt;5.3 基础配置&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;local-cache&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;enabled&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;namespace&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;kecache.sample&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;default-ttl&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;30m&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;null-value-ttl&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;5m&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;caffeine&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;default&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#f92672&#34;&gt;maximum-size&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;1000&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#f92672&#34;&gt;expire-after-write&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;10m&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#f92672&#34;&gt;record-stats&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;redis&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;address&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;redis://127.0.0.1:6379&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;broadcast-channel&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;kecache:evict&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;default-ttl&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;30m&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;calibration&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;enabled&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;false&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;54-声明式使用示例&#34;&gt;5.4 声明式使用示例&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;@Service&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;AccountService&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;@LocalCacheable&lt;/span&gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        cacheNames &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;tve_account_info&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        key &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&amp;#39;id_&amp;#39; + #accountId&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        unless &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;#result == null&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    )
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;public&lt;/span&gt; AccountInfo &lt;span style=&#34;color:#a6e22e&#34;&gt;getById&lt;/span&gt;(&lt;span style=&#34;color:#66d9ef&#34;&gt;long&lt;/span&gt; accountId) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; accountDao.&lt;span style=&#34;color:#a6e22e&#34;&gt;loadById&lt;/span&gt;(accountId);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#75715e&#34;&gt;// 一次查询构建多个索引 key&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;@LocalCacheable.Multi&lt;/span&gt;({
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#a6e22e&#34;&gt;@LocalCacheable&lt;/span&gt;(cacheNames &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;tve_account_info&amp;#34;&lt;/span&gt;, key &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&amp;#39;id_&amp;#39; + #accountId&amp;#34;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#a6e22e&#34;&gt;@LocalCacheable&lt;/span&gt;(cacheNames &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;tve_account_info&amp;#34;&lt;/span&gt;, key &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&amp;#39;nm_&amp;#39; + #result.accountName&amp;#34;&lt;/span&gt;, unless &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;#result == null&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    })
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;public&lt;/span&gt; AccountInfo &lt;span style=&#34;color:#a6e22e&#34;&gt;getByIdMultiIndex&lt;/span&gt;(&lt;span style=&#34;color:#66d9ef&#34;&gt;long&lt;/span&gt; accountId) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; accountDao.&lt;span style=&#34;color:#a6e22e&#34;&gt;loadById&lt;/span&gt;(accountId);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;@LocalCacheEvict&lt;/span&gt;(cacheNames &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;tve_account_info&amp;#34;&lt;/span&gt;, key &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&amp;#39;id_&amp;#39; + #accountId&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;void&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;updateById&lt;/span&gt;(&lt;span style=&#34;color:#66d9ef&#34;&gt;long&lt;/span&gt; accountId, AccountInfo info) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        accountDao.&lt;span style=&#34;color:#a6e22e&#34;&gt;update&lt;/span&gt;(accountId, info);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;@LocalCacheClear&lt;/span&gt;(cacheNames &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;tve_account_info&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;void&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;clearAll&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        accountDao.&lt;span style=&#34;color:#a6e22e&#34;&gt;clear&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;55-编程式使用&#34;&gt;5.5 编程式使用&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;@Service&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;AccountService&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;@Autowired&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;private&lt;/span&gt; CacheTemplate cacheTemplate;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;public&lt;/span&gt; AccountInfo &lt;span style=&#34;color:#a6e22e&#34;&gt;getById&lt;/span&gt;(&lt;span style=&#34;color:#66d9ef&#34;&gt;long&lt;/span&gt; accountId) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; cacheTemplate.&lt;span style=&#34;color:#a6e22e&#34;&gt;get&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;tve_account_info&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;id_&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; accountId, key &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; accountDao.&lt;span style=&#34;color:#a6e22e&#34;&gt;loadById&lt;/span&gt;(accountId);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        });
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h2 id=&#34;六模块结构&#34;&gt;六、模块结构&lt;/h2&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;graph TD
    Parent[kecache-parent] --&amp;gt; API[kecache-api&amp;lt;br/&amp;gt;注解 / SPI / 配置模型 / 事件]
    Parent --&amp;gt; Core[kecache-core&amp;lt;br/&amp;gt;统一门面 / Key策略 / 空值保护]
    Parent --&amp;gt; Caffeine[kecache-provider-caffeine&amp;lt;br/&amp;gt;L1 Caffeine 实现]
    Parent --&amp;gt; Redisson[kecache-provider-redisson&amp;lt;br/&amp;gt;L2 Redis + 失效广播]
    Parent --&amp;gt; Calibration[kecache-calibration&amp;lt;br/&amp;gt;数据一致性校准]
    Parent --&amp;gt; Starter[kecache-spring-boot-starter&amp;lt;br/&amp;gt;自动配置 + AOP]
    Parent --&amp;gt; Sample[kecache-spring-boot-sample&amp;lt;br/&amp;gt;示例应用 + 测试]

    Starter --&amp;gt; API
    Starter --&amp;gt; Core
    Core --&amp;gt; Caffeine
    Core --&amp;gt; Redisson
    Core --&amp;gt; Calibration&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h2 id=&#34;七关键约定写代码前必读&#34;&gt;七、关键约定（写代码前必读）&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;L1 key&lt;/strong&gt;：&lt;code&gt;cacheName#dataKey&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;L2 key&lt;/strong&gt;：&lt;code&gt;{namespace|cacheName}:dataKey&lt;/code&gt;（使用 hash tag 避免集群模式下 slot 分散）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;默认读写策略&lt;/strong&gt;：Cache-Aside&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;失效广播频道&lt;/strong&gt;：默认 &lt;code&gt;kecache:evict&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;序列化&lt;/strong&gt;：Jackson 启用 default typing，保留类型信息&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;八使用注意点与避坑指南&#34;&gt;八、使用注意点与避坑指南&lt;/h2&gt;
&lt;h3 id=&#34;81-redis-是硬性依赖&#34;&gt;8.1 Redis 是硬性依赖&lt;/h3&gt;
&lt;p&gt;L2 缓存和失效广播都依赖 Redis，测试与运行前请确保 Redis 可达。运行全部测试需要本地启动 Redis：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;mvn clean test&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;82-集群一致性是最终一致不是实时一致&#34;&gt;8.2 集群一致性是“最终一致”，不是实时一致&lt;/h3&gt;
&lt;p&gt;本地缓存存在一致窗口：A 节点写入并广播，B 节点收到消息前仍可能读到旧值。对于库存、余额、订单状态等强一致场景，建议绕过本地缓存，直接走 Redis 或数据库。&lt;/p&gt;
&lt;h3 id=&#34;83-空值保护是把双刃剑&#34;&gt;8.3 空值保护是把双刃剑&lt;/h3&gt;
&lt;p&gt;默认对 &lt;code&gt;null&lt;/code&gt; 结果缓存 5 分钟，可防止缓存穿透。但如果业务刚刚从 DB 写入了一条新记录，而之前查询时正好命中了 NullValue，就会出现“写入后 5 分钟内查不到”的延迟。需要时可：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;缩短 &lt;code&gt;null-value-ttl&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;在写入后主动 &lt;code&gt;@LocalCacheEvict&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;对新增数据敏感的 cacheName 关闭空值保护&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;84-序列化类型变更要兼容&#34;&gt;8.4 序列化类型变更要兼容&lt;/h3&gt;
&lt;p&gt;Redis 层使用 Jackson 并保留类型信息。如果缓存对象发生不兼容的类结构变更（如字段类型改变、删除字段、包名调整），旧数据反序列化可能失败。建议：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对缓存 DTO 做版本控制&lt;/li&gt;
&lt;li&gt;重大变更时清空 Redis 对应 key 或切换 &lt;code&gt;cacheName&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;避免把接口、匿名类、动态代理对象放入缓存&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;85-注意-key-的命名空间隔离&#34;&gt;8.5 注意 Key 的命名空间隔离&lt;/h3&gt;
&lt;p&gt;L2 key 格式为 &lt;code&gt;{namespace|cacheName}:dataKey&lt;/code&gt;。如果多个业务共用同一个 Redis 实例，务必设置不同的 &lt;code&gt;namespace&lt;/code&gt;，否则可能出现 Key 冲突。&lt;/p&gt;
&lt;h3 id=&#34;86-localcacheablemulti-的-result-引用要判空&#34;&gt;8.6 &lt;code&gt;@LocalCacheable.Multi&lt;/code&gt; 的 &lt;code&gt;result&lt;/code&gt; 引用要判空&lt;/h3&gt;
&lt;p&gt;多索引注解里如果通过 &lt;code&gt;#result.xxx&lt;/code&gt; 取字段，必须配合 &lt;code&gt;unless = &amp;quot;#result == null&amp;quot;&lt;/code&gt;，否则方法返回 null 时会触发 SpEL 计算异常。&lt;/p&gt;
&lt;h3 id=&#34;87-aop-自调用不会生效&#34;&gt;8.7 AOP 自调用不会生效&lt;/h3&gt;
&lt;p&gt;和 Spring Cache 一样，keCache 的注解基于 AOP。同类方法内部调用（&lt;code&gt;this.method()&lt;/code&gt;）不会走代理，缓存注解不会生效。需要时注入自身代理或改用编程式 &lt;code&gt;CacheTemplate&lt;/code&gt;。&lt;/p&gt;
&lt;h3 id=&#34;88-谨慎使用-localcacheclear&#34;&gt;8.8 谨慎使用 &lt;code&gt;@LocalCacheClear&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;@LocalCacheClear&lt;/code&gt; 会清空整个 &lt;code&gt;cacheName&lt;/code&gt; 的本地缓存并广播。在高并发场景下，频繁全量清除会：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;导致本地缓存命中率骤降&lt;/li&gt;
&lt;li&gt;引发集体回源，DB 压力陡增&lt;/li&gt;
&lt;li&gt;产生大量广播消息&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;建议优先使用 &lt;code&gt;@LocalCacheEvict&lt;/code&gt; 精确驱逐。&lt;/p&gt;
&lt;h3 id=&#34;89-校准任务不是开箱即用的&#34;&gt;8.9 校准任务不是开箱即用的&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;CalibrationProvider&lt;/code&gt; 目前只是 SPI 骨架，需要业务方自己实现 &lt;code&gt;fetchChanges(Instant start, Instant end)&lt;/code&gt; 逻辑（如查变更表、CDC 日志）。开启 &lt;code&gt;local-cache.calibration.enabled=true&lt;/code&gt; 后才会按 Cron 触发。&lt;/p&gt;
&lt;h3 id=&#34;810-当前-redis-部署模式限制&#34;&gt;8.10 当前 Redis 部署模式限制&lt;/h3&gt;
&lt;p&gt;自动配置里的 &lt;code&gt;RedissonClient&lt;/code&gt; 目前只实现了 &lt;strong&gt;single 单节点&lt;/strong&gt;模式。如果生产环境使用 Redis Cluster、Sentinel 或主从，需要自行扩展配置。&lt;/p&gt;
&lt;h3 id=&#34;811-关注-caffeine-内存与统计&#34;&gt;8.11 关注 Caffeine 内存与统计&lt;/h3&gt;
&lt;p&gt;默认开启 &lt;code&gt;record-stats: true&lt;/code&gt;，本地缓存命中率、加载次数等都会被记录。建议配合 actuator / Micrometer（后续可扩展）监控：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;maximum-size&lt;/code&gt; 是否设置合理&lt;/li&gt;
&lt;li&gt;本地缓存命中率&lt;/li&gt;
&lt;li&gt;加载耗时与异常率&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;九许可证&#34;&gt;九、许可证&lt;/h2&gt;
&lt;p&gt;keCache 采用 &lt;strong&gt;MIT&lt;/strong&gt; 协议开源。&lt;/p&gt;
&lt;h2 id=&#34;十总结&#34;&gt;十、总结&lt;/h2&gt;
&lt;p&gt;keCache 是一个目标聚焦、实现克制的两级缓存框架：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果你想要 &lt;strong&gt;Caffeine 的速度 + Redis 的共享能力&lt;/strong&gt;，它值得尝试。&lt;/li&gt;
&lt;li&gt;如果你希望 &lt;strong&gt;声明式注解即可解决集群本地缓存一致性问题&lt;/strong&gt;，它提供了失效广播与自失效屏蔽。&lt;/li&gt;
&lt;li&gt;如果你的业务对 &lt;strong&gt;实时一致性零容忍&lt;/strong&gt;，请把它当作“加速层”而非“唯一数据源”。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;有兴趣可以直接到 Gitee 查看源码与示例：&lt;a href=&#34;https://gitee.com/soulnew/ke-cache&#34;&gt;https://gitee.com/soulnew/ke-cache&lt;/a&gt;。&lt;/p&gt;</description>
    </item>
    <item>
      <title>我开发的KE Reliable Executor：基于 Redis/MySQL 的 Spring Boot 可靠任务执行框架</title>
      <link>https://ftmao.com/blog/posts/ke-reliable-executor/</link>
      <pubDate>Mon, 04 Aug 2025 17:10:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/ke-reliable-executor/</guid>
      <description>&lt;h1 id=&#34;我开发的ke-reliable-executor基于-redismysql-的-spring-boot-可靠任务执行框架&#34;&gt;我开发的KE Reliable Executor：基于 Redis/MySQL 的 Spring Boot 可靠任务执行框架&lt;/h1&gt;
&lt;p&gt;在微服务里，异步任务随处可见：支付后通知第三方、订单超时取消、批量对账、报表生成……这些任务有一个共同诉求——&lt;strong&gt;不能丢、不能重复执行、失败能重试、状态要能查&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;传统做法是直接引入 Kafka、RocketMQ 这类重型消息中间件。但对于很多中小型业务来说，这未免太重：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;需要额外维护 Broker、Topic、消费组；&lt;/li&gt;
&lt;li&gt;团队要熟悉一套新的运维和监控体系；&lt;/li&gt;
&lt;li&gt;为了“可靠”还要配置死信队列、事务消息等高级特性。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果你的系统里已经有 Redis（或 MySQL），并且任务量级尚未达到 MQ 的吞吐上限，那么完全可以基于 Redis 搭一个&lt;strong&gt;轻量且可靠的异步任务执行框架&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;我开发是这样一个框架：&lt;strong&gt;KE Reliable Executor&lt;/strong&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;开源地址&lt;/strong&gt;：https://gitee.com/soulnew/reliableexecutor&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;一为什么需要这个工具&#34;&gt;一、为什么需要这个工具？&lt;/h2&gt;
&lt;p&gt;先回顾一下常见异步任务场景里的痛点：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;痛点&lt;/th&gt;
					&lt;th&gt;说明&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;任务丢失&lt;/td&gt;
					&lt;td&gt;服务重启或执行线程异常退出后，未执行的任务找不到了&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;重复执行&lt;/td&gt;
					&lt;td&gt;多实例部署时，同一个任务被多个节点同时消费&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;失败即结束&lt;/td&gt;
					&lt;td&gt;业务异常或网络抖动后，没有自动重试机制&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;状态黑盒&lt;/td&gt;
					&lt;td&gt;无法查询任务当前是成功、失败、执行中还是已取消&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;接入成本高&lt;/td&gt;
					&lt;td&gt;引入 MQ 需要改代码、加依赖、搭集群&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;KE Reliable Executor 要解决的就是这些问题。它把“可靠任务执行”抽象成一个 Spring Boot Starter：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;业务方只负责&lt;strong&gt;定义任务处理器&lt;/strong&gt;和&lt;strong&gt;提交任务&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;框架负责&lt;strong&gt;存储、调度、加锁、重试、状态流转、清理&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;所有状态都可通过 API 查询，也可通过 Micrometer 指标监控。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;适合什么场景&#34;&gt;适合什么场景？&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;已有 Redis/MySQL，不想额外维护 MQ；&lt;/li&gt;
&lt;li&gt;任务量中等，对可靠性和重试有要求；&lt;/li&gt;
&lt;li&gt;需要任务状态可查询、可人工介入；&lt;/li&gt;
&lt;li&gt;团队希望以最小成本获得“类 MQ”的可靠执行能力。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;不适合什么场景&#34;&gt;不适合什么场景？&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;海量消息、高吞吐（Kafka/RocketMQ 更合适）；&lt;/li&gt;
&lt;li&gt;严格顺序消费（ZSET 按时间排序，不保证业务绝对顺序）；&lt;/li&gt;
&lt;li&gt;需要复杂路由、死信队列、事务消息等高级 MQ 特性。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;二与竞品的对比&#34;&gt;二、与竞品的对比&lt;/h2&gt;
&lt;h3 id=&#34;21-与-kafka--rocketmq-对比&#34;&gt;2.1 与 Kafka / RocketMQ 对比&lt;/h3&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;维度&lt;/th&gt;
					&lt;th&gt;Kafka / RocketMQ&lt;/th&gt;
					&lt;th&gt;KE Reliable Executor&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;基础设施&lt;/td&gt;
					&lt;td&gt;需要独立 Broker 集群&lt;/td&gt;
					&lt;td&gt;复用 Redis 或 MySQL&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;运维成本&lt;/td&gt;
					&lt;td&gt;高（Topic、消费组、监控）&lt;/td&gt;
					&lt;td&gt;低（Spring Boot Starter）&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;接入成本&lt;/td&gt;
					&lt;td&gt;需要学习 SDK 与语义&lt;/td&gt;
					&lt;td&gt;注解 + 注入接口即可&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;吞吐量&lt;/td&gt;
					&lt;td&gt;极高&lt;/td&gt;
					&lt;td&gt;中等&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;状态查询&lt;/td&gt;
					&lt;td&gt;需自建审计表&lt;/td&gt;
					&lt;td&gt;内置状态查询&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;重试机制&lt;/td&gt;
					&lt;td&gt;需配置消费重试&lt;/td&gt;
					&lt;td&gt;内置可配置重试&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;适用阶段&lt;/td&gt;
					&lt;td&gt;大型业务、高并发&lt;/td&gt;
					&lt;td&gt;中小型业务、快速落地&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;结论：KE Reliable Executor 不是取代 MQ，而是&lt;strong&gt;在“已有 Redis/MySQL、任务量可控”的场景下，提供一个更轻量的选择&lt;/strong&gt;。&lt;/p&gt;
&lt;h3 id=&#34;22-与-spring-scheduled--数据库轮询对比&#34;&gt;2.2 与 Spring &lt;code&gt;@Scheduled&lt;/code&gt; + 数据库轮询对比&lt;/h3&gt;
&lt;p&gt;很多团队最早会用数据库表 + &lt;code&gt;@Scheduled&lt;/code&gt; 轮询来实现异步任务。这种做法的问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;无分布式锁&lt;/strong&gt;：多实例同时轮询会重复执行；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;无状态机&lt;/strong&gt;：任务状态全靠字段控制，容易混乱；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;无统一重试策略&lt;/strong&gt;：重试间隔、重试次数需要手写；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;无优雅关闭&lt;/strong&gt;：应用关闭时可能丢失正在执行的任务。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;KE Reliable Executor 把这些都封装好了：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Master 选举 + 任务级锁保证不重复；&lt;/li&gt;
&lt;li&gt;明确的状态机（CREATED → RUNNING → PENDING/COMPLETED/FAILED）；&lt;/li&gt;
&lt;li&gt;可配置的重试间隔与次数；&lt;/li&gt;
&lt;li&gt;优雅释放锁与线程池关闭。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;23-与-xxl-job--powerjob-对比&#34;&gt;2.3 与 XXL-JOB / PowerJob 对比&lt;/h3&gt;
&lt;p&gt;XXL-JOB 和 PowerJob 是优秀的分布式任务调度平台，但它们更适合&lt;strong&gt;定时任务&lt;/strong&gt;和&lt;strong&gt;调度中心&lt;/strong&gt;模式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;需要单独部署调度中心；&lt;/li&gt;
&lt;li&gt;适合 Cron 表达式触发的批处理任务；&lt;/li&gt;
&lt;li&gt;任务由业务主动提交的场景下，接入不够自然。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;KE Reliable Executor 更像一个&lt;strong&gt;嵌入式任务队列&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;无额外服务，随业务应用启动；&lt;/li&gt;
&lt;li&gt;任务由业务代码主动提交；&lt;/li&gt;
&lt;li&gt;强调“提交即可靠执行”，而非定时触发。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;三整体架构&#34;&gt;三、整体架构&lt;/h2&gt;
&lt;h3 id=&#34;31-模块解构图&#34;&gt;3.1 模块解构图&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;graph TD
    A[业务服务] --&amp;gt;|submitTask| B(ReliableExecutor API)
    B --&amp;gt; C[TaskScheduler]
    C --&amp;gt; D{Master 锁}
    D --&amp;gt;|持有锁| E[扫描 ZSET 任务]
    E --&amp;gt; F[任务级锁]
    F --&amp;gt;|获取成功| G[提交线程池]
    G --&amp;gt; H[TaskExecutor]
    H --&amp;gt; I[&amp;#34;任务处理器 @ReliableTaskHandler&amp;#34;]
    I --&amp;gt;|成功| J[移入 FINISHED]
    I --&amp;gt;|失败| K[更新为 PENDING 等待重试]
    C --&amp;gt; L[TaskStorage]
    L --&amp;gt; M[RedisTaskStorage]
    L --&amp;gt; N[JdbcTaskStorage]
    H --&amp;gt; O[ReliableTaskListener]
    C --&amp;gt; P[ReliableExecutorMetrics]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h3 id=&#34;32-两层锁模型&#34;&gt;3.2 两层锁模型&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;graph TB
    subgraph 全局层
        A[Master 锁] --&amp;gt;|仅一个实例| B[负责调度扫描]
    end
    subgraph 任务层
        C[&amp;#34;任务级锁 task-lock:{taskId}&amp;#34;] --&amp;gt;|仅一个线程| D[执行任务]
    end
    B --&amp;gt; C&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Master 锁&lt;/strong&gt;：同一时刻只有一个实例负责扫描任务，避免多节点拉取同一批任务。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;任务级锁&lt;/strong&gt;：针对每个 CREATED 任务，提交执行前再抢一次锁，保证同一任务不会被并发执行。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;33-队列设计&#34;&gt;3.3 队列设计&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;graph LR
    A[提交任务] --&amp;gt;|add| B[Redis ZSET running-task-zset]
    B --&amp;gt;|ZRANGE 按 score| C[调度器]
    C --&amp;gt;|执行成功/失败/超时| D[Lua 原子转移]
    D --&amp;gt;|RPUSH + ZREM| E[Redis List finished-task-list]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;使用 ZSET 而非 List 的核心原因：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;天然支持时间调度&lt;/strong&gt;：score 编码创建时间或下次重试时间；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;任务唯一&lt;/strong&gt;：同一个 member 在队列中只有一份；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;状态分区&lt;/strong&gt;：CREATED 与 PENDING 使用不同 score 基数，一次范围查询即可区分。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Score 编码规则：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;score = BASE + epochMillis * 100 + retryCount
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;CREATED 基数：1_000_000_000_000_000
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;PENDING 基数：2_000_000_000_000_000&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;34-任务状态机&#34;&gt;3.4 任务状态机&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;stateDiagram-v2
    [*] --&amp;gt; CREATED: submitTask
    CREATED --&amp;gt; RUNNING: 调度/立即执行
    RUNNING --&amp;gt; COMPLETED: 执行成功
    RUNNING --&amp;gt; PENDING: 执行失败，未超重试
    PENDING --&amp;gt; RUNNING: 到达重试时间
    RUNNING --&amp;gt; FAILED: 超过重试次数
    RUNNING --&amp;gt; TIMEOUT: 超过最大时长
    CREATED --&amp;gt; CANCELLED: 取消任务
    PENDING --&amp;gt; CANCELLED: 取消任务
    RUNNING --&amp;gt; ERROR: 无处理器/参数错误&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;hr&gt;
&lt;h2 id=&#34;四核心设计要点&#34;&gt;四、核心设计要点&lt;/h2&gt;
&lt;h3 id=&#34;41-lua-原子转移&#34;&gt;4.1 Lua 原子转移&lt;/h3&gt;
&lt;p&gt;任务进入终态时，必须保证“写入 finished list”和“从 ZSET 移除”是原子的，否则可能出现：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;任务已判定成功但还在 ZSET 里，被下一次调度捞出来；&lt;/li&gt;
&lt;li&gt;任务已从 ZSET 移除但 finished list 没写入，查询不到结果。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;框架使用 Lua 脚本完成这一转移：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-lua&#34; data-lang=&#34;lua&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;local&lt;/span&gt; score &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; redis.call(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;ZSCORE&amp;#39;&lt;/span&gt;, KEYS[&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;], ARGV[&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;]);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;redis.call(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;RPUSH&amp;#39;&lt;/span&gt;, KEYS[&lt;span style=&#34;color:#ae81ff&#34;&gt;2&lt;/span&gt;], ARGV[&lt;span style=&#34;color:#ae81ff&#34;&gt;2&lt;/span&gt;]);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; score &lt;span style=&#34;color:#66d9ef&#34;&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    redis.call(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;ZREM&amp;#39;&lt;/span&gt;, KEYS[&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;], ARGV[&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;]);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;else&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;逻辑：先查 ZSET，无论是否存在都把终态信息写入 finished list，如果存在再删除。整个脚本在 Redis 单线程执行，天然原子。&lt;/p&gt;
&lt;h3 id=&#34;42-double-check&#34;&gt;4.2 Double-Check&lt;/h3&gt;
&lt;p&gt;调度器拿到任务级锁后、提交线程池前，会再次回查 Redis：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;任务是否仍在 running ZSET 中？&lt;/li&gt;
&lt;li&gt;任务状态是否仍是 CREATED？&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;任一条件不满足，立即释放锁并跳过。这是经典的双重检查模式，把“检查-执行”串在锁保护的临界区内。&lt;/p&gt;
&lt;h3 id=&#34;43-双存储后端&#34;&gt;4.3 双存储后端&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;graph TD
    A[TaskStorage 接口] --&amp;gt; B[RedisTaskStorage]
    A --&amp;gt; C[JdbcTaskStorage]
    B --&amp;gt; D[Redisson ZSET + List]
    C --&amp;gt; E[reliable_executor_task 表]
    F[配置 reliable-executor.task-storage] --&amp;gt;|redis/mysql| A&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;redis&lt;/code&gt;：默认，适合已有 Redis 的场景；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mysql&lt;/code&gt;：适合不希望引入 Redis 或需要更强持久化的场景。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;两种后端通过 Spring 条件装配切换，业务代码无感知。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;五快速开始&#34;&gt;五、快速开始&lt;/h2&gt;
&lt;h3 id=&#34;51-引入依赖&#34;&gt;5.1 引入依赖&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-gradle&#34; data-lang=&#34;gradle&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;dependencies &lt;span style=&#34;color:#f92672&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    implementation &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;com.ftmao:ke-reliable-executor:1.0.0-SNAPSHOT&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    implementation &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;org.redisson:redisson-spring-boot-starter:3.27.2&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;52-配置&#34;&gt;5.2 配置&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;spring&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;application&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;name&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;my-app&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;redis&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;redisson&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#f92672&#34;&gt;config&lt;/span&gt;: |&lt;span style=&#34;color:#e6db74&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;        singleServerConfig:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;          address: &amp;#34;redis://127.0.0.1:6379&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;reliable-executor&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;task-storage&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;redis&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;core-pool-size&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;5&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;max-pool-size&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;10&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;queue-capacity&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;100&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;53-定义处理器&#34;&gt;5.3 定义处理器&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;@Component&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;OrderTaskHandler&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;@ReliableTaskHandler&lt;/span&gt;(name &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;process_order&amp;#34;&lt;/span&gt;, paramType &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; OrderParam.&lt;span style=&#34;color:#a6e22e&#34;&gt;class&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;void&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;processOrder&lt;/span&gt;(OrderParam param) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#75715e&#34;&gt;// 业务逻辑&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;@ReliableTaskHandler&lt;/span&gt;(name &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;send_notify&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;void&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;sendNotify&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#75715e&#34;&gt;// 无参任务&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;54-提交任务&#34;&gt;5.4 提交任务&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;@Service&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;@RequiredArgsConstructor&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;OrderService&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;final&lt;/span&gt; ReliableExecutor reliableExecutor;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;void&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;submitOrder&lt;/span&gt;(OrderParam param) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        TaskSubmitRequest request &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; TaskSubmitRequest.&lt;span style=&#34;color:#a6e22e&#34;&gt;builder&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                .&lt;span style=&#34;color:#a6e22e&#34;&gt;taskId&lt;/span&gt;(UUID.&lt;span style=&#34;color:#a6e22e&#34;&gt;randomUUID&lt;/span&gt;().&lt;span style=&#34;color:#a6e22e&#34;&gt;toString&lt;/span&gt;())
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                .&lt;span style=&#34;color:#a6e22e&#34;&gt;taskName&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;process_order&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                .&lt;span style=&#34;color:#a6e22e&#34;&gt;taskParameter&lt;/span&gt;(param)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                .&lt;span style=&#34;color:#a6e22e&#34;&gt;retryLimit&lt;/span&gt;(3)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                .&lt;span style=&#34;color:#a6e22e&#34;&gt;retryInterval&lt;/span&gt;(2000)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                .&lt;span style=&#34;color:#a6e22e&#34;&gt;maxDurationMillis&lt;/span&gt;(60000)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                .&lt;span style=&#34;color:#a6e22e&#34;&gt;build&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        reliableExecutor.&lt;span style=&#34;color:#a6e22e&#34;&gt;submitTask&lt;/span&gt;(request);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;55-监听生命周期&#34;&gt;5.5 监听生命周期&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;@Component&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;MyTaskListener&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;implements&lt;/span&gt; ReliableTaskListener {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;@Override&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;void&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;onTaskCompleted&lt;/span&gt;(String taskId, String taskName) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#75715e&#34;&gt;// 任务成功&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;@Override&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;void&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;onTaskFailed&lt;/span&gt;(String taskId, String taskName, String status) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#75715e&#34;&gt;// 任务最终失败&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;hr&gt;
&lt;h2 id=&#34;六使用注意点与避坑指南&#34;&gt;六、使用注意点与避坑指南&lt;/h2&gt;
&lt;h3 id=&#34;61-业务幂等仍然是最后一道防线&#34;&gt;6.1 业务幂等仍然是最后一道防线&lt;/h3&gt;
&lt;p&gt;框架通过两层锁、double-check、Lua 原子转移把重复执行的概率降到极低，但&lt;strong&gt;任何分布式系统都无法 100% 保证幂等&lt;/strong&gt;。业务 Handler 仍然应该保证幂等，尤其是涉及写数据库、调用外部接口的场景。&lt;/p&gt;
&lt;h3 id=&#34;62-taskid-需要业务保证唯一&#34;&gt;6.2 taskId 需要业务保证唯一&lt;/h3&gt;
&lt;p&gt;框架使用业务传入的 &lt;code&gt;taskId&lt;/code&gt; 作为任务唯一标识。如果同一业务单据重复提交相同 &lt;code&gt;taskId&lt;/code&gt;，第二次提交会生成新的 ZSET member（如果业务字段不同）或覆盖（如果完全相同）。&lt;strong&gt;建议用业务主键 + 操作类型组合生成 taskId&lt;/strong&gt;，例如 &lt;code&gt;order_12345_pay&lt;/code&gt;。&lt;/p&gt;
&lt;h3 id=&#34;63-重试间隔与最大时长的权衡&#34;&gt;6.3 重试间隔与最大时长的权衡&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;reliable-executor&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;retryInterval&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;2000&lt;/span&gt;      &lt;span style=&#34;color:#75715e&#34;&gt;# 失败后 2s 再试&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;maxDurationMillis&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;60000&lt;/span&gt; &lt;span style=&#34;color:#75715e&#34;&gt;# 任务最长存活 60s&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;retryInterval&lt;/code&gt; 过小会导致失败任务快速反复执行，放大下游压力；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;maxDurationMillis&lt;/code&gt; 是从任务创建时间算起，不是单次执行时间；&lt;/li&gt;
&lt;li&gt;建议失败任务采用指数退避（当前版本为固定间隔，后续可扩展）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;64-mysql-分支需要自行引入-jpa&#34;&gt;6.4 MySQL 分支需要自行引入 JPA&lt;/h3&gt;
&lt;p&gt;框架中 &lt;code&gt;spring-boot-starter-data-jpa&lt;/code&gt; 是 &lt;code&gt;compileOnly&lt;/code&gt;，使用 MySQL 存储时需要在业务应用里显式引入：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-gradle&#34; data-lang=&#34;gradle&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;implementation &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;org.springframework.boot:spring-boot-starter-data-jpa&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;runtimeOnly &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;com.mysql:mysql-connector-j&amp;#39;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;并执行建表脚本 &lt;code&gt;db/migration/V1__create_reliable_executor_tables.sql&lt;/code&gt;。&lt;/p&gt;
&lt;h3 id=&#34;65-redis-集群环境注意-key-前缀&#34;&gt;6.5 Redis 集群环境注意 Key 前缀&lt;/h3&gt;
&lt;p&gt;框架默认使用 &lt;code&gt;spring.application.name&lt;/code&gt; 作为 Redis Key 前缀。如果多个应用共用同一个 Redis 实例，务必设置不同的 &lt;code&gt;spring.application.name&lt;/code&gt;，否则任务会互相串扰。&lt;/p&gt;
&lt;h3 id=&#34;66-线程池队列满载时会跳过调度&#34;&gt;6.6 线程池队列满载时会跳过调度&lt;/h3&gt;
&lt;p&gt;当线程池队列达到 &lt;code&gt;queueCapacity&lt;/code&gt; 时，调度器会跳过本次周期，避免无限制堆积。如果业务出现持续高峰，建议：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;调大 &lt;code&gt;maxPoolSize&lt;/code&gt; 和 &lt;code&gt;queueCapacity&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;监控 &lt;code&gt;reliable_executor_threadpool_queue_size&lt;/code&gt; 指标；&lt;/li&gt;
&lt;li&gt;必要时配合业务限流。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;67-不要忽略超时任务&#34;&gt;6.7 不要忽略超时任务&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;maxDurationMillis&lt;/code&gt; 是从创建时间算起的总时长。如果一个任务在队列里排队太久，可能还没执行就触发 TIMEOUT。对于允许较长延迟的任务，要把这个值设大；对于实时性任务，要把重试间隔和排队容量一起考虑。&lt;/p&gt;
&lt;h3 id=&#34;68-本地开发无-redis-怎么办&#34;&gt;6.8 本地开发无 Redis 怎么办？&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;使用 MySQL 分支（H2 也可以跑测试）；&lt;/li&gt;
&lt;li&gt;或者本地启动一个 Redis Docker 容器；&lt;/li&gt;
&lt;li&gt;框架的 Redis 集成测试会在无 Redis 时自动跳过，不影响 CI。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;七监控指标&#34;&gt;七、监控指标&lt;/h2&gt;
&lt;p&gt;框架内置 Micrometer 指标，可接入 Prometheus/Actuator：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;指标名&lt;/th&gt;
					&lt;th&gt;含义&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;reliable_executor_is_master&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;当前节点是否为 Master&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;reliable_executor_tasks_scheduled_total&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;累计调度任务数&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;reliable_executor_tasks_executed_total&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;累计执行任务数&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;reliable_executor_tasks_completed_total&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;累计成功任务数&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;reliable_executor_tasks_failed_total&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;累计失败任务数&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;reliable_executor_tasks_timeout_total&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;累计超时任务数&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;reliable_executor_tasks_retried_total&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;累计重试次数&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;reliable_executor_threadpool_queue_size&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;线程池队列大小&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;reliable_executor_task_execution_seconds&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;任务执行耗时&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id=&#34;八开源与示例&#34;&gt;八、开源与示例&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;开源地址&lt;/strong&gt;：https://gitee.com/soulnew/reliableexecutor&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;示例模块&lt;/strong&gt;：&lt;code&gt;example/&lt;/code&gt; 子项目包含可运行的 Spring Boot 应用和 REST 接口示例，可直接启动体验任务提交与执行。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;九总结&#34;&gt;九、总结&lt;/h2&gt;
&lt;p&gt;KE Reliable Executor 是一个&lt;strong&gt;轻量、可靠、易接入&lt;/strong&gt;的异步任务执行框架。它用 Redis ZSET + 两层锁 + Lua 原子转移，在“不引入重型 MQ”的前提下，提供了任务提交、调度、重试、状态查询、监控等完整能力；同时通过 MySQL 分支提供了另一种存储选择。&lt;/p&gt;
&lt;p&gt;如果你的业务符合以下特征，不妨试试它：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;已有 Redis/MySQL；&lt;/li&gt;
&lt;li&gt;任务量中等，重视可靠性；&lt;/li&gt;
&lt;li&gt;希望快速落地，不想为 MQ 付出额外运维成本；&lt;/li&gt;
&lt;li&gt;需要任务状态可观测、可人工介入。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;记住：框架层负责把重复消费的概率降到极低，业务层仍应保证 Handler 幂等，这才是工程上的稳妥做法。&lt;/p&gt;
&lt;/blockquote&gt;
</description>
    </item>
    <item>
      <title>Lua 入门：写给 Redis 脚本使用者的极简教程</title>
      <link>https://ftmao.com/blog/posts/lua-intro-for-redis/</link>
      <pubDate>Sun, 03 Oct 2021 10:00:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/lua-intro-for-redis/</guid>
      <description>&lt;h1 id=&#34;lua-入门写给-redis-脚本使用者的极简教程&#34;&gt;Lua 入门：写给 Redis 脚本使用者的极简教程&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;Redis 从 2.6 开始支持 Lua 脚本，核心目的是把多个命令打包成&lt;strong&gt;原子操作&lt;/strong&gt;。本文面向已经会用 Redis、但还没写过 Lua 脚本的同学，只讲最常用、最实用的部分，学完就能写出常见的限流、加锁、库存扣减脚本。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;一为什么-redis-需要-lua&#34;&gt;一、为什么 Redis 需要 Lua&lt;/h2&gt;
&lt;p&gt;Redis 单条命令是原子的，但多条命令组合起来就不是了。比如经典的「查库存 → 判断 → 扣减」：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;GET stock:1001      &lt;span style=&#34;color:#75715e&#34;&gt;# 返回 10&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# 业务代码判断 10 &amp;gt; 0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;DECR stock:1001     &lt;span style=&#34;color:#75715e&#34;&gt;# 返回 9&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;并发时两个客户端可能都读到 10，然后都执行 DECR，导致超卖。&lt;/p&gt;
&lt;p&gt;Lua 脚本在 Redis 里&lt;strong&gt;作为一个整体执行&lt;/strong&gt;，中间不会被其他命令打断，天然解决了这类问题。而且脚本在 Redis 服务端执行，还能减少网络往返。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;二本地-lua-环境准备&#34;&gt;二、本地 Lua 环境准备&lt;/h2&gt;
&lt;h3 id=&#34;21-安装-lua&#34;&gt;2.1 安装 Lua&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;macOS：&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;brew install lua
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;lua -v&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;Ubuntu / Debian：&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;sudo apt update
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;sudo apt install lua5.1
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;lua5.1 -v&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;Windows：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;从 &lt;a href=&#34;https://www.lua.org/download.html&#34;&gt;Lua 官方&lt;/a&gt; 下载源码或预编译二进制，也可以直接用 &lt;a href=&#34;https://github.com/rjpcomputing/luaforwindows&#34;&gt;Lua for Windows&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Docker（最省事）：&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;docker run --rm -it nickblah/lua:5.1 lua -v&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;22-第一个本地-lua-程序&#34;&gt;2.2 第一个本地 Lua 程序&lt;/h3&gt;
&lt;p&gt;创建 &lt;code&gt;hello.lua&lt;/code&gt;：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-lua&#34; data-lang=&#34;lua&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;print(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;hello, lua&amp;#34;&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;运行：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;lua hello.lua&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;23-用-redis-验证脚本&#34;&gt;2.3 用 Redis 验证脚本&lt;/h3&gt;
&lt;p&gt;如果你本地有 Redis，可以直接用 &lt;code&gt;redis-cli&lt;/code&gt; 跑脚本文件：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;redis-cli --eval myscript.lua key1 key2 , arg1 arg2&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;注意 &lt;code&gt;,&lt;/code&gt; 前后都有空格：&lt;code&gt;,&lt;/code&gt; 左边是 &lt;code&gt;KEYS&lt;/code&gt;，右边是 &lt;code&gt;ARGV&lt;/code&gt;。&lt;/p&gt;
&lt;h3 id=&#34;24-编辑器推荐&#34;&gt;2.4 编辑器推荐&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;VS Code&lt;/strong&gt;：插件 &lt;code&gt;Lua&lt;/code&gt;（sumneko）或 &lt;code&gt;Lua Language Server&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;IDEA / WebStorm&lt;/strong&gt;：安装 &lt;code&gt;EmmyLua&lt;/code&gt; 插件。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Neovim&lt;/strong&gt;：&lt;code&gt;lua_ls&lt;/code&gt; + &lt;code&gt;lspconfig&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;三hello-world第一个-redis-lua-脚本&#34;&gt;三、Hello World：第一个 Redis Lua 脚本&lt;/h2&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;EVAL &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;return &amp;#39;hello, redis lua&amp;#39;&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;EVAL&lt;/code&gt; 是执行 Lua 脚本的命令。&lt;/li&gt;
&lt;li&gt;后面的 &lt;code&gt;0&lt;/code&gt; 表示脚本用到的 key 数量是 0。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;return&lt;/code&gt; 把结果返回给客户端。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;带 key 和参数的版本：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;EVAL &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;return redis.call(&amp;#39;GET&amp;#39;, KEYS[1])&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt; user:1
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# 等同于 GET user:1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;EVAL &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;return redis.call(&amp;#39;SET&amp;#39;, KEYS[1], ARGV[1])&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt; user:1 alice
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# 等同于 SET user:1 alice&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;KEYS[1]&lt;/code&gt; 对应第 1 个 key，&lt;code&gt;KEYS[2]&lt;/code&gt; 对应第 2 个，以此类推。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ARGV[1]&lt;/code&gt; 对应第 1 个额外参数。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;所有 ARGV 都是字符串&lt;/strong&gt;，如果需要数字，要在 Lua 里做类型转换。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;四lua-基础redis-场景够用的子集&#34;&gt;四、Lua 基础：Redis 场景够用的子集&lt;/h2&gt;
&lt;h3 id=&#34;41-变量与类型&#34;&gt;4.1 变量与类型&lt;/h3&gt;
&lt;p&gt;Lua 是动态类型语言，常用类型有：&lt;code&gt;nil&lt;/code&gt;、&lt;code&gt;boolean&lt;/code&gt;、&lt;code&gt;number&lt;/code&gt;、&lt;code&gt;string&lt;/code&gt;、&lt;code&gt;table&lt;/code&gt;。&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-lua&#34; data-lang=&#34;lua&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;local&lt;/span&gt; name &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;alice&amp;#34;&lt;/span&gt;      &lt;span style=&#34;color:#75715e&#34;&gt;-- 字符串&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;local&lt;/span&gt; age &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;30&lt;/span&gt;            &lt;span style=&#34;color:#75715e&#34;&gt;-- 数字，不区分 int / float&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;local&lt;/span&gt; ok &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;true&lt;/span&gt;           &lt;span style=&#34;color:#75715e&#34;&gt;-- 布尔&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;local&lt;/span&gt; empty &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt;         &lt;span style=&#34;color:#75715e&#34;&gt;-- 空&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;局部变量用 &lt;code&gt;local&lt;/code&gt;，否则会成为全局变量。Redis 脚本里建议一律用 &lt;code&gt;local&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;条件判断时，&lt;code&gt;0&lt;/code&gt; 和空字符串 &lt;code&gt;&#39;&#39;&lt;/code&gt; 都是 &lt;code&gt;true&lt;/code&gt;，只有 &lt;code&gt;nil&lt;/code&gt; 和 &lt;code&gt;false&lt;/code&gt; 是 &lt;code&gt;false&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-lua&#34; data-lang=&#34;lua&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;local&lt;/span&gt; a &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; a &lt;span style=&#34;color:#66d9ef&#34;&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;yes&amp;#34;&lt;/span&gt;    &lt;span style=&#34;color:#75715e&#34;&gt;-- 会执行这里&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;42-字符串与数字转换&#34;&gt;4.2 字符串与数字转换&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-lua&#34; data-lang=&#34;lua&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;local&lt;/span&gt; s &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;42&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;local&lt;/span&gt; n &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; tonumber(s)      &lt;span style=&#34;color:#75715e&#34;&gt;-- 转成数字 42&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;local&lt;/span&gt; s2 &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; tostring(n)     &lt;span style=&#34;color:#75715e&#34;&gt;-- 转成字符串 &amp;#34;42&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;local&lt;/span&gt; a &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;10&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;local&lt;/span&gt; b &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;3&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;local&lt;/span&gt; c &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; a &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; b            &lt;span style=&#34;color:#75715e&#34;&gt;-- 13，自动把字符串当数字&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;在 Redis 脚本里，更推荐显式转换，避免意外：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-lua&#34; data-lang=&#34;lua&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;local&lt;/span&gt; count &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; tonumber(redis.call(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;GET&amp;#39;&lt;/span&gt;, KEYS[&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;])) &lt;span style=&#34;color:#f92672&#34;&gt;or&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;43-表table数组--字典&#34;&gt;4.3 表（table）：数组 + 字典&lt;/h3&gt;
&lt;p&gt;表是 Lua 唯一的数据结构，既能当数组用，也能当字典用。&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-lua&#34; data-lang=&#34;lua&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;-- 数组（下标从 1 开始）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;local&lt;/span&gt; arr &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; {&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;a&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;b&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;c&amp;#34;&lt;/span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; arr[&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;]    &lt;span style=&#34;color:#75715e&#34;&gt;-- &amp;#34;a&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;-- 字典&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;local&lt;/span&gt; dict &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; {name &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;alice&amp;#34;&lt;/span&gt;, age &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;30&lt;/span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; dict.name &lt;span style=&#34;color:#75715e&#34;&gt;-- &amp;#34;alice&amp;#34;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;遍历数组：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-lua&#34; data-lang=&#34;lua&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;local&lt;/span&gt; arr &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; {&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;a&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;b&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;c&amp;#34;&lt;/span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; i, v &lt;span style=&#34;color:#66d9ef&#34;&gt;in&lt;/span&gt; ipairs(arr) &lt;span style=&#34;color:#66d9ef&#34;&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    print(i, v)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;-- 输出：&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;-- 1 a&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;-- 2 b&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;-- 3 c&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;遍历字典：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-lua&#34; data-lang=&#34;lua&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;local&lt;/span&gt; dict &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; {name &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;alice&amp;#34;&lt;/span&gt;, age &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;30&lt;/span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; k, v &lt;span style=&#34;color:#66d9ef&#34;&gt;in&lt;/span&gt; pairs(dict) &lt;span style=&#34;color:#66d9ef&#34;&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    print(k, v)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;44-条件判断&#34;&gt;4.4 条件判断&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-lua&#34; data-lang=&#34;lua&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;local&lt;/span&gt; n &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;10&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; n &lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;20&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;big&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;elseif&lt;/span&gt; n &lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;5&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;medium&amp;#34;&lt;/span&gt;   &lt;span style=&#34;color:#75715e&#34;&gt;-- 会返回这个&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;else&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;small&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;45-循环&#34;&gt;4.5 循环&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-lua&#34; data-lang=&#34;lua&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;-- for 循环&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; i &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;5&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    print(i)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;-- while 循环&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;local&lt;/span&gt; i &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;while&lt;/span&gt; i &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;5&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    print(i)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    i &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; i &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;Redis 脚本里循环要慎用，避免执行时间过长阻塞主线程。&lt;/p&gt;
&lt;h3 id=&#34;46-函数&#34;&gt;4.6 函数&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-lua&#34; data-lang=&#34;lua&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;local&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;function&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;add&lt;/span&gt;(a, b)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; a &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; b
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; add(&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;2&lt;/span&gt;)  &lt;span style=&#34;color:#75715e&#34;&gt;-- 3&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;hr&gt;
&lt;h2 id=&#34;五lua-原理浅析&#34;&gt;五、Lua 原理浅析&lt;/h2&gt;
&lt;p&gt;理解 Lua 的运行机制，能帮你写出更稳的 Redis 脚本。&lt;/p&gt;
&lt;h3 id=&#34;51-解释器与字节码&#34;&gt;5.1 解释器与字节码&lt;/h3&gt;
&lt;p&gt;Lua 不是纯解释执行。它会把源码编译成&lt;strong&gt;字节码（bytecode）&lt;/strong&gt;，然后在 Lua 虚拟机（VM）上运行。&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;luac -o hello.luac hello.lua    -- 编译成字节码
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;lua hello.luac                   -- 运行字节码&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;Redis 内部集成的也是 Lua VM，脚本第一次执行时会先编译成字节码，之后再执行就复用这个结果。所以长脚本第一次跑会慢一点，后续重复执行会快一些。&lt;/p&gt;
&lt;h3 id=&#34;52-内存回收机制&#34;&gt;5.2 内存回收机制&lt;/h3&gt;
&lt;p&gt;Lua 使用&lt;strong&gt;自动垃圾回收（Garbage Collection, GC）&lt;/strong&gt;，主要策略是&lt;strong&gt;增量式标记-清除（incremental mark-and-sweep）&lt;/strong&gt;。Lua 5.4 还引入了&lt;strong&gt;分代 GC&lt;/strong&gt; 模式。&lt;/p&gt;
&lt;p&gt;对 Redis 脚本来说，关键点是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;脚本运行期间创建的 table、string 等临时对象，由 Lua GC 自动回收。&lt;/li&gt;
&lt;li&gt;Redis 脚本通常很短，GC 压力不大。&lt;/li&gt;
&lt;li&gt;不要在脚本里构造超大 table 或缓存大量中间结果，否则可能触发 GC 暂停，阻塞 Redis。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;53-table-是核心&#34;&gt;5.3 Table 是核心&lt;/h3&gt;
&lt;p&gt;Lua 的 table 本质上是一个&lt;strong&gt;哈希表 + 数组&lt;/strong&gt;的混合结构：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用连续整数索引时，底层按数组优化，访问快。&lt;/li&gt;
&lt;li&gt;用字符串或其他类型当 key 时，底层是哈希表。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这也是 Lua 能用 table 模拟数组、字典、对象、模块的原因。&lt;/p&gt;
&lt;h3 id=&#34;54-redis-中的-lua-沙箱&#34;&gt;5.4 Redis 中的 Lua 沙箱&lt;/h3&gt;
&lt;p&gt;Redis 对 Lua 做了严格限制：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不能加载外部库（如 &lt;code&gt;io&lt;/code&gt;、&lt;code&gt;os&lt;/code&gt; 大部分功能被禁用）。&lt;/li&gt;
&lt;li&gt;不能发起网络请求。&lt;/li&gt;
&lt;li&gt;不能访问文件系统。&lt;/li&gt;
&lt;li&gt;只能调用 Redis 提供的 API（如 &lt;code&gt;redis.call&lt;/code&gt;、&lt;code&gt;redis.log&lt;/code&gt;）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这个沙箱保证了脚本安全，但也意味着你&lt;strong&gt;不能直接在脚本里读系统时间&lt;/strong&gt;（&lt;code&gt;os.time&lt;/code&gt; 被禁用），需要把时间作为 &lt;code&gt;ARGV&lt;/code&gt; 传进去。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;六redis-脚本核心-api&#34;&gt;六、Redis 脚本核心 API&lt;/h2&gt;
&lt;h3 id=&#34;61-rediscall-与-redispcall&#34;&gt;6.1 redis.call 与 redis.pcall&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;redis.call(...)&lt;/code&gt;：调用 Redis 命令，出错时直接抛出 Lua 错误，脚本会终止。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;redis.pcall(...)&lt;/code&gt;：调用 Redis 命令，出错时返回错误对象，脚本继续执行。&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-lua&#34; data-lang=&#34;lua&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;-- call 版本：key 不存在会报错&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;local&lt;/span&gt; v &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; redis.call(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;GET&amp;#39;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;non_existent_key&amp;#39;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;-- pcall 版本：可以捕获错误&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;local&lt;/span&gt; ok, err &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; pcall(redis.call, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;GET&amp;#39;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;non_existent_key&amp;#39;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;not&lt;/span&gt; ok &lt;span style=&#34;color:#66d9ef&#34;&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;error: &amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;..&lt;/span&gt; tostring(err)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;一般推荐：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;简单脚本用 &lt;code&gt;redis.call&lt;/code&gt;，逻辑更清晰。&lt;/li&gt;
&lt;li&gt;需要容错时用 &lt;code&gt;redis.pcall&lt;/code&gt; 或外层 &lt;code&gt;pcall&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;62-返回值&#34;&gt;6.2 返回值&lt;/h3&gt;
&lt;p&gt;Lua 脚本可以返回单个值、数组或表：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-lua&#34; data-lang=&#34;lua&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;-- 返回单个值&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; redis.call(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;GET&amp;#39;&lt;/span&gt;, KEYS[&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;])
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;-- 返回多个值&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; {redis.call(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;GET&amp;#39;&lt;/span&gt;, KEYS[&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;]), redis.call(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;GET&amp;#39;&lt;/span&gt;, KEYS[&lt;span style=&#34;color:#ae81ff&#34;&gt;2&lt;/span&gt;])}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;Redis 会把 Lua 的 &lt;code&gt;table&lt;/code&gt; 转成数组返回给客户端。&lt;/p&gt;
&lt;h3 id=&#34;63-keys-与-argv-的规范&#34;&gt;6.3 KEYS 与 ARGV 的规范&lt;/h3&gt;
&lt;p&gt;Redis Cluster 要求脚本里用到的 key 都必须放在 &lt;code&gt;KEYS&lt;/code&gt; 中，这样 Redis 才能判断脚本是否跨 slot。&lt;code&gt;ARGV&lt;/code&gt; 里只能放非 key 参数。&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;EVAL &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;return redis.call(&amp;#39;SET&amp;#39;, KEYS[1], ARGV[1])&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt; mykey myvalue&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;这里 &lt;code&gt;mykey&lt;/code&gt; 是 key，&lt;code&gt;myvalue&lt;/code&gt; 是 value。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;七实战常见-redis-lua-模式&#34;&gt;七、实战：常见 Redis Lua 模式&lt;/h2&gt;
&lt;h3 id=&#34;71-限流滑动窗口&#34;&gt;7.1 限流（滑动窗口）&lt;/h3&gt;
&lt;p&gt;限制某个用户在 60 秒内最多请求 100 次：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-lua&#34; data-lang=&#34;lua&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;local&lt;/span&gt; key &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; KEYS[&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;local&lt;/span&gt; window &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; tonumber(ARGV[&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;])      &lt;span style=&#34;color:#75715e&#34;&gt;-- 窗口大小，毫秒&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;local&lt;/span&gt; limit &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; tonumber(ARGV[&lt;span style=&#34;color:#ae81ff&#34;&gt;2&lt;/span&gt;])       &lt;span style=&#34;color:#75715e&#34;&gt;-- 限制次数&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;local&lt;/span&gt; now &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; tonumber(ARGV[&lt;span style=&#34;color:#ae81ff&#34;&gt;3&lt;/span&gt;])         &lt;span style=&#34;color:#75715e&#34;&gt;-- 当前时间戳，毫秒&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;-- 清理窗口外的请求记录&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;redis.call(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;ZREMRANGEBYSCORE&amp;#39;&lt;/span&gt;, key, &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;, now &lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt; window)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;-- 统计当前窗口内的请求数&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;local&lt;/span&gt; count &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; redis.call(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;ZCARD&amp;#39;&lt;/span&gt;, key)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; count &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt; limit &lt;span style=&#34;color:#66d9ef&#34;&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    redis.call(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;ZADD&amp;#39;&lt;/span&gt;, key, now, now &lt;span style=&#34;color:#f92672&#34;&gt;..&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;_&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;..&lt;/span&gt; count)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    redis.call(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;PEXPIRE&amp;#39;&lt;/span&gt;, key, window)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;   &lt;span style=&#34;color:#75715e&#34;&gt;-- 允许通过&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;else&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;   &lt;span style=&#34;color:#75715e&#34;&gt;-- 拒绝&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;调用：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;EVAL &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;...脚本...&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt; rate:user:1 &lt;span style=&#34;color:#ae81ff&#34;&gt;60000&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;100&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1690000000000&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;72-分布式锁可重入简化版&#34;&gt;7.2 分布式锁（可重入简化版）&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-lua&#34; data-lang=&#34;lua&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;local&lt;/span&gt; key &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; KEYS[&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;local&lt;/span&gt; token &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; ARGV[&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;local&lt;/span&gt; ttl &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; tonumber(ARGV[&lt;span style=&#34;color:#ae81ff&#34;&gt;2&lt;/span&gt;])
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;local&lt;/span&gt; current &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; redis.call(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;GET&amp;#39;&lt;/span&gt;, key)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; current &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;false&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;or&lt;/span&gt; current &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; token &lt;span style=&#34;color:#66d9ef&#34;&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    redis.call(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;SET&amp;#39;&lt;/span&gt;, key, token, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;PX&amp;#39;&lt;/span&gt;, ttl)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;else&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;73-库存扣减防超卖&#34;&gt;7.3 库存扣减（防超卖）&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-lua&#34; data-lang=&#34;lua&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;local&lt;/span&gt; key &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; KEYS[&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;local&lt;/span&gt; delta &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; tonumber(ARGV[&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;])
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;local&lt;/span&gt; stock &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; tonumber(redis.call(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;GET&amp;#39;&lt;/span&gt;, key)) &lt;span style=&#34;color:#f92672&#34;&gt;or&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; stock &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt; delta &lt;span style=&#34;color:#66d9ef&#34;&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;    &lt;span style=&#34;color:#75715e&#34;&gt;-- 库存不足&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;redis.call(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;DECRBY&amp;#39;&lt;/span&gt;, key, delta)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; stock &lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt; delta&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;74-批量删除符合某种前缀的-key&#34;&gt;7.4 批量删除符合某种前缀的 key&lt;/h3&gt;
&lt;p&gt;注意：生产环境慎用 &lt;code&gt;KEYS&lt;/code&gt; 命令，这里仅作语法示例，大数据量请用 &lt;code&gt;SCAN&lt;/code&gt;。&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-lua&#34; data-lang=&#34;lua&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;local&lt;/span&gt; pattern &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; ARGV[&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;local&lt;/span&gt; keys &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; redis.call(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;KEYS&amp;#39;&lt;/span&gt;, pattern)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; i, k &lt;span style=&#34;color:#66d9ef&#34;&gt;in&lt;/span&gt; ipairs(keys) &lt;span style=&#34;color:#66d9ef&#34;&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    redis.call(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;DEL&amp;#39;&lt;/span&gt;, k)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;#&lt;/span&gt;keys&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;75-条件更新cas-简化版&#34;&gt;7.5 条件更新（CAS 简化版）&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-lua&#34; data-lang=&#34;lua&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;local&lt;/span&gt; key &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; KEYS[&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;local&lt;/span&gt; expected &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; ARGV[&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;local&lt;/span&gt; newValue &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; ARGV[&lt;span style=&#34;color:#ae81ff&#34;&gt;2&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;local&lt;/span&gt; current &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; redis.call(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;GET&amp;#39;&lt;/span&gt;, key)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; current &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; expected &lt;span style=&#34;color:#66d9ef&#34;&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    redis.call(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;SET&amp;#39;&lt;/span&gt;, key, newValue)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;else&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;hr&gt;
&lt;h2 id=&#34;八调试常见坑与最佳实践&#34;&gt;八、调试、常见坑与最佳实践&lt;/h2&gt;
&lt;h3 id=&#34;81-调试脚本&#34;&gt;8.1 调试脚本&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;先用本地 Redis 跑&lt;/strong&gt;：&lt;code&gt;redis-cli --eval script.lua key1 key2 , arg1 arg2&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;注意 &lt;code&gt;,&lt;/code&gt; 前后有空格，&lt;code&gt;,&lt;/code&gt; 左边是 KEYS，右边是 ARGV。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;打印日志&lt;/strong&gt;：Lua 脚本里不能直接用 &lt;code&gt;print&lt;/code&gt; 输出到客户端，但可以用 &lt;code&gt;redis.log(redis.LOG_NOTICE, msg)&lt;/code&gt; 写到 Redis 日志。&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-lua&#34; data-lang=&#34;lua&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;redis.log(redis.LOG_NOTICE, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;current stock: &amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;..&lt;/span&gt; tostring(stock))&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;ol start=&#34;3&#34;&gt;
&lt;li&gt;&lt;strong&gt;本地先用 &lt;code&gt;lua&lt;/code&gt; 命令跑语法&lt;/strong&gt;：不依赖 Redis 也能检查基础语法错误。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&#34;82-常见踩坑清单&#34;&gt;8.2 常见踩坑清单&lt;/h3&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;坑&lt;/th&gt;
					&lt;th&gt;说明&lt;/th&gt;
					&lt;th&gt;建议&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;ARGV 全是字符串&lt;/td&gt;
					&lt;td&gt;数字运算前必须 &lt;code&gt;tonumber()&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;每个数字参数都显式 &lt;code&gt;tonumber()&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;全局变量&lt;/td&gt;
					&lt;td&gt;忘了 &lt;code&gt;local&lt;/code&gt; 可能污染全局环境，Redis 甚至会杀掉脚本&lt;/td&gt;
					&lt;td&gt;所有变量一律加 &lt;code&gt;local&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;脚本执行时间&lt;/td&gt;
					&lt;td&gt;循环、大 key 操作会阻塞 Redis 主线程&lt;/td&gt;
					&lt;td&gt;脚本尽量短，避免复杂循环&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;集群 key 位置&lt;/td&gt;
					&lt;td&gt;所有 key 必须写在 &lt;code&gt;KEYS&lt;/code&gt; 里，跨 slot 会报错&lt;/td&gt;
					&lt;td&gt;业务 key 设计时确保同一 slot&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;脚本缓存&lt;/td&gt;
					&lt;td&gt;长脚本每次 EVAL 都传输浪费带宽&lt;/td&gt;
					&lt;td&gt;生产环境用 &lt;code&gt;EVALSHA&lt;/code&gt; 或客户端自动缓存&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;nil&lt;/code&gt; 中断数组&lt;/td&gt;
					&lt;td&gt;table 中间出现 &lt;code&gt;nil&lt;/code&gt; 会导致 &lt;code&gt;#&lt;/code&gt; 长度计算出错&lt;/td&gt;
					&lt;td&gt;避免在数组中间塞 &lt;code&gt;nil&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;pairs&lt;/code&gt; 遍历顺序&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;pairs&lt;/code&gt; 不保证顺序，&lt;code&gt;ipairs&lt;/code&gt; 才按数组顺序&lt;/td&gt;
					&lt;td&gt;需要顺序时用 &lt;code&gt;ipairs&lt;/code&gt; 或自己维护索引&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;字符串拼接性能&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;..&lt;/code&gt; 在循环里反复拼接是 O(n²)&lt;/td&gt;
					&lt;td&gt;用 &lt;code&gt;table.concat&lt;/code&gt; 批量拼接&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;redis.call&lt;/code&gt; 报错&lt;/td&gt;
					&lt;td&gt;命令执行失败会直接抛错终止脚本&lt;/td&gt;
					&lt;td&gt;需要容错时改用 &lt;code&gt;redis.pcall&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;缺失 key 的返回值&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;GET&lt;/code&gt; 不存在的 key 返回 &lt;code&gt;false&lt;/code&gt;，不是 &lt;code&gt;nil&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;判断时用 &lt;code&gt;== false&lt;/code&gt; 或 &lt;code&gt;if not current&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;写命令必须确定性的&lt;/td&gt;
					&lt;td&gt;主从复制/持久化要求相同的脚本产生相同结果&lt;/td&gt;
					&lt;td&gt;不要用随机数、&lt;code&gt;TIME&lt;/code&gt;、&lt;code&gt;SRANDMEMBER&lt;/code&gt; 等做写决策&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;浮点数比较&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;0.1 + 0.2 ~= 0.3&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;用整数存储金额/库存，避免浮点&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&#34;83-性能建议&#34;&gt;8.3 性能建议&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;脚本尽量短小，只处理一个原子逻辑单元。&lt;/li&gt;
&lt;li&gt;不要在 Lua 里做复杂计算或大量循环。&lt;/li&gt;
&lt;li&gt;大 key 操作（如 &lt;code&gt;HGETALL&lt;/code&gt; 几十万字段）不要在脚本里做。&lt;/li&gt;
&lt;li&gt;热 key 尽量缩短 TTL 或做拆分，避免单脚本长时间占用。&lt;/li&gt;
&lt;li&gt;对执行时间没把握的脚本，先用 &lt;code&gt;redis-cli --latency-history&lt;/code&gt; 或 &lt;code&gt;SLOWLOG&lt;/code&gt; 观察影响。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;九总结&#34;&gt;九、总结&lt;/h2&gt;
&lt;p&gt;Redis Lua 脚本不需要你成为 Lua 专家，掌握下面几点就能覆盖 80% 的场景：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;本地能跑 Lua，能写 &lt;code&gt;.lua&lt;/code&gt; 文件并用 &lt;code&gt;redis-cli --eval&lt;/code&gt; 测试。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;local&lt;/code&gt; 变量、&lt;code&gt;number&lt;/code&gt; / &lt;code&gt;string&lt;/code&gt; / &lt;code&gt;table&lt;/code&gt; 三种类型。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tonumber()&lt;/code&gt; / &lt;code&gt;tostring()&lt;/code&gt; 转换，&lt;code&gt;ipairs()&lt;/code&gt; / &lt;code&gt;pairs()&lt;/code&gt; 遍历，&lt;code&gt;table.concat&lt;/code&gt; 拼接。&lt;/li&gt;
&lt;li&gt;Lua 是编译成字节码在 VM 上运行，GC 自动回收，Redis 里是沙箱环境。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;redis.call()&lt;/code&gt; 调用命令，&lt;code&gt;KEYS[n]&lt;/code&gt; 和 &lt;code&gt;ARGV[n]&lt;/code&gt; 传参，集群 key 必须放 KEYS。&lt;/li&gt;
&lt;li&gt;脚本整体原子执行，适合「读 → 判断 → 写」的复合操作。&lt;/li&gt;
&lt;li&gt;注意 ARGV 是字符串、缺失 key 返回 &lt;code&gt;false&lt;/code&gt;、避免长脚本阻塞、写命令要确定性。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;限流、分布式锁、库存扣减、条件更新这些高频场景，本质上都是同一个模式：&lt;strong&gt;先读数据，在脚本里做判断，再决定写什么&lt;/strong&gt;。把这一点练熟，Redis Lua 就入门了。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;如果你在 Redis Lua 脚本里踩过什么坑，欢迎在评论区分享。&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    <item>
      <title>从Redis核心原理到 IPTV 视频项目落地</title>
      <link>https://ftmao.com/blog/posts/redis-in-action-iptv/</link>
      <pubDate>Tue, 28 Sep 2021 10:00:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/redis-in-action-iptv/</guid>
      <description>&lt;h1 id=&#34;从redis核心原理到-iptv-视频项目落地&#34;&gt;从Redis核心原理到 IPTV 视频项目落地&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;先系统梳理 Redis 的核心原理与面试高频知识点，再结合 IPTV（交互式网络电视）视频业务场景，给出可直接运行的 Demo。最后讨论 Redis 为什么不适合长期存储、常见踩坑点，以及 List / Pub/Sub / Stream 三种消息队列实现。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;一redis-是什么一句话定位&#34;&gt;一、Redis 是什么？一句话定位&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Redis（Remote Dictionary Server）是一个开源的、基于内存的高性能键值对数据库&lt;/strong&gt;，支持多种数据结构、持久化、高可用与分布式扩展。&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;graph LR
    A[Redis] --&amp;gt; B[内存存储]
    A --&amp;gt; C[数据结构服务器]
    A --&amp;gt; D[单线程命令执行]
    A --&amp;gt; E[IO 多路复用]
    A --&amp;gt; F[持久化 + 高可用]
    B --&amp;gt; G[读写毫秒级]
    C --&amp;gt; H[String/Hash/List/Set/ZSET/Stream/Bitmap/Geo/HyperLogLog]
    F --&amp;gt; I[RDB/AOF/主从/哨兵/Cluster]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;与 Memcached 等纯缓存不同，Redis 的核心价值在于&lt;strong&gt;丰富的数据结构与原子操作&lt;/strong&gt;，可以直接在内存中完成排序、计数、限流、去重、队列等复杂逻辑。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;二redis-为什么快&#34;&gt;二、Redis 为什么快？&lt;/h2&gt;
&lt;p&gt;面试常问「Redis 为什么快」，通常从四个层面回答：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;原因&lt;/th&gt;
					&lt;th&gt;说明&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;内存操作&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;数据存于内存，无需磁盘寻道，访问速度是 SSD 的千倍以上。&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;单线程命令执行&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;避免多线程上下文切换、锁竞争，CPU 缓存命中率更高。&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;IO 多路复用&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;基于 epoll/kqueue/select 用一个线程处理大量连接。&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;高效数据结构&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;String 底层 SDS、ZSET 底层跳表+字典、List 底层 quicklist 等。&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;graph TB
    subgraph Redis 6/7 线程模型
        M[主线程&amp;lt;br/&amp;gt;命令执行 + IO 多路复用]
        IO[IO 线程池&amp;lt;br/&amp;gt;仅网络读写]
        BG[后台进程&amp;lt;br/&amp;gt;RDB fork / AOF rewrite]
    end
    C1[Client 1] --&amp;gt; M
    C2[Client 2] --&amp;gt; M
    C3[Client 3] --&amp;gt; M
    M &amp;lt;--&amp;gt; IO
    M -.-&amp;gt; BG&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;blockquote&gt;
&lt;p&gt;注意：Redis 6/7 引入的多线程&lt;strong&gt;只用于网络 IO 读写&lt;/strong&gt;，命令执行依旧是单线程。所以单条慢命令仍会阻塞整个实例。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;三redis-数据类型与使用场景&#34;&gt;三、Redis 数据类型与使用场景&lt;/h2&gt;
&lt;h3 id=&#34;31-五种基本数据类型&#34;&gt;3.1 五种基本数据类型&lt;/h3&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;类型&lt;/th&gt;
					&lt;th&gt;底层结构&lt;/th&gt;
					&lt;th&gt;典型场景&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;String&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;SDS（简单动态字符串）&lt;/td&gt;
					&lt;td&gt;缓存、计数器、分布式锁、Session&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;Hash&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;ziplist / hashtable&lt;/td&gt;
					&lt;td&gt;对象缓存、用户信息、EPG 节目单&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;List&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;quicklist（ziplist + 双向链表）&lt;/td&gt;
					&lt;td&gt;消息队列、最新 N 条记录&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;Set&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;intset / hashtable&lt;/td&gt;
					&lt;td&gt;去重、共同关注、抽奖&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;ZSET&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;ziplist / skiplist + dict&lt;/td&gt;
					&lt;td&gt;排行榜、延时队列、范围查询&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;注：表中 ziplist 在 Redis 7 起已被 listpack 替代（动机见 §4.2），配置项改名 &lt;code&gt;*-max-listpack-*&lt;/code&gt;，旧名仍兼容。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&#34;32-三种特殊数据类型&#34;&gt;3.2 三种特殊数据类型&lt;/h3&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;类型&lt;/th&gt;
					&lt;th&gt;说明&lt;/th&gt;
					&lt;th&gt;场景&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;Bitmap&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;位数组，按位操作&lt;/td&gt;
					&lt;td&gt;用户签到、在线状态、UV 统计&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;HyperLogLog&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;基数估计算法&lt;/td&gt;
					&lt;td&gt;海量 UV 去重计数，误差约 0.81%&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;Geo&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;地理位置坐标&lt;/td&gt;
					&lt;td&gt;附近的人、POI 搜索&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&#34;33-string最常用却被低估&#34;&gt;3.3 String：最常用却被低估&lt;/h3&gt;
&lt;p&gt;String 不仅能存字符串，还能存整数、浮点数、二进制数据。常用命令：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;SET key value EX &lt;span style=&#34;color:#ae81ff&#34;&gt;3600&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;GET key
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;INCR view_count
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;INCRBY view_count &lt;span style=&#34;color:#ae81ff&#34;&gt;10&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;SETNX lock:order:123 true EX &lt;span style=&#34;color:#ae81ff&#34;&gt;30&lt;/span&gt;  &lt;span style=&#34;color:#75715e&#34;&gt;# 分布式锁核心命令&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;SDS 的优势&lt;/strong&gt;（相对 C 字符串）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;O(1) 获取长度。&lt;/li&gt;
&lt;li&gt;二进制安全，可存图片、序列化数据。&lt;/li&gt;
&lt;li&gt;预分配空间，减少内存重分配次数。&lt;/li&gt;
&lt;li&gt;杜绝缓冲区溢出。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;34-hash对象缓存首选&#34;&gt;3.4 Hash：对象缓存首选&lt;/h3&gt;
&lt;p&gt;Hash 适合存「一组字段」的对象，比 JSON String 更节省内存、更新更灵活。&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;HSET user:1001 name &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;张三&amp;#34;&lt;/span&gt; age &lt;span style=&#34;color:#ae81ff&#34;&gt;30&lt;/span&gt; city &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;北京&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;HGET user:1001 name
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;HGETALL user:1001
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;HINCRBY user:1001 age &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;35-zset排行榜的王牌&#34;&gt;3.5 ZSET：排行榜的王牌&lt;/h3&gt;
&lt;p&gt;ZSET 底层同时维护「跳表」和「字典」：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;跳表（skiplist）&lt;/strong&gt;：支持 O(log N) 的范围查询和排序。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;字典（dict）&lt;/strong&gt;：支持 O(1) 按 member 查 score。&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;ZADD hot:tv &lt;span style=&#34;color:#ae81ff&#34;&gt;1000&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;庆余年&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;ZADD hot:tv &lt;span style=&#34;color:#ae81ff&#34;&gt;800&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;狂飙&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;ZINCRBY hot:tv &lt;span style=&#34;color:#ae81ff&#34;&gt;50&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;狂飙&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;ZREVRANGE hot:tv &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;9&lt;/span&gt; WITHSCORES
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;ZREVRANK hot:tv &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;狂飙&amp;#34;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;36-list-与-stream队列双雄&#34;&gt;3.6 List 与 Stream：队列双雄&lt;/h3&gt;
&lt;p&gt;List 适合简单队列，Stream 适合生产级消息队列，后面第十四部分会详细对比。&lt;/p&gt;
&lt;h3 id=&#34;37-bitmap-与-hyperloglog海量-uv-统计的利器&#34;&gt;3.7 Bitmap 与 HyperLogLog：海量 UV 统计的利器&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Bitmap 为什么能天然去重？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Bitmap 本质是一个超长 bit 数组，每个 bit 只能是 0 或 1。把 user_id 作为偏移量（offset），用户访问一次就把对应位置 1：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;天然去重&lt;/strong&gt;：同一用户重复访问，置的都是同一个 bit，无需额外判断。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;极致省内存&lt;/strong&gt;：1 亿用户只需 1 亿 bit ≈ 12MB；而 Set 存 1 亿个 ID 要占用数 GB。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;支持位运算&lt;/strong&gt;：&lt;code&gt;BITOP AND/OR&lt;/code&gt; 可以直接算多日留存（今天访问 AND 昨天也访问）。&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;SETBIT uv:20210928 &lt;span style=&#34;color:#ae81ff&#34;&gt;10086&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;   &lt;span style=&#34;color:#75715e&#34;&gt;# 用户 10086 访问&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;SETBIT uv:20210928 &lt;span style=&#34;color:#ae81ff&#34;&gt;10086&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;   &lt;span style=&#34;color:#75715e&#34;&gt;# 重复访问，结果不变&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;BITCOUNT uv:20210928         &lt;span style=&#34;color:#75715e&#34;&gt;# 直接得出 UV 数&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;局限：要求 user_id 是连续整数，ID 稀疏时浪费内存。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;HyperLogLog 为什么 12KB 能数一亿？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;HyperLogLog 基于概率估计算法：把每个元素 hash 成均匀分布的随机数，观察 hash 值二进制串中&lt;strong&gt;前导零的最大个数&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;类比抛硬币：想连续抛出 N 次正面，平均要抛 2^N 次。反过来，如果见过最长连续 N 次正面，就估计大约抛了 2^N 次——前导零越多，说明见过的不同元素越多。&lt;/li&gt;
&lt;li&gt;单次估计误差大，所以内部用 &lt;strong&gt;16384 个分桶（寄存器，每个 6 bit）&lt;/strong&gt; 分别记录，再取调和平均数消除偶然性，最终误差稳定在 &lt;strong&gt;0.81%&lt;/strong&gt; 左右。&lt;/li&gt;
&lt;li&gt;内存恒定 &lt;strong&gt;约 12KB&lt;/strong&gt;，与元素个数无关：统计 1 亿 UV 还是 10 亿 UV 都是 12KB。&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;PFADD uv:20210928 &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;user:10086&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;PFADD uv:20210928 &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;user:10086&amp;#34;&lt;/span&gt;  &lt;span style=&#34;color:#75715e&#34;&gt;# 重复添加不影响计数&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;PFCOUNT uv:20210928             &lt;span style=&#34;color:#75715e&#34;&gt;# 估算 UV&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;局限：有误差、不可逆（不能取出具体是哪些用户）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;怎么选&lt;/strong&gt;：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;需求&lt;/th&gt;
					&lt;th&gt;推荐&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;精确计数、算多日留存（交并集）、ID 是连续整数&lt;/td&gt;
					&lt;td&gt;Bitmap&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;只要近似 UV、数据量极大、ID 是任意字符串&lt;/td&gt;
					&lt;td&gt;HyperLogLog&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id=&#34;四redis-底层数据结构速览&#34;&gt;四、Redis 底层数据结构速览&lt;/h2&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;graph TD
    A[Redis 数据类型] --&amp;gt; B[String]
    A --&amp;gt; C[Hash]
    A --&amp;gt; D[List]
    A --&amp;gt; E[Set]
    A --&amp;gt; F[ZSET]
    B --&amp;gt; G[SDS]
    C --&amp;gt; H[ziplist]
    C --&amp;gt; I[hashtable]
    D --&amp;gt; J[quicklist]
    E --&amp;gt; K[intset]
    E --&amp;gt; L[hashtable]
    F --&amp;gt; M[ziplist]
    F --&amp;gt; N[skiplist + dict]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;底层结构&lt;/th&gt;
					&lt;th&gt;特点&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;SDS&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;带长度、预分配、二进制安全。&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;ziplist&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;紧凑列表，小数据量时省内存；大数据量时改为 hashtable/skiplist。&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;listpack&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;ziplist 的修正版（Redis 7+）：entry 只记自身长度，消灭连锁更新。&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;quicklist&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;List 的底层，由多个紧凑块（Redis 7 前是 ziplist，之后是 listpack）节点组成的双向链表。&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;hashtable&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;标准哈希表，渐进式 rehash 避免一次性阻塞。&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;intset&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;Set 在元素全为整数且数量少时的紧凑实现。&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;skiplist&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;多层索引链表，实现 ZSET 的有序范围查询。&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;下面把 5 种核心结构「画」出来。&lt;/p&gt;
&lt;h3 id=&#34;41-sdssimple-dynamic-string&#34;&gt;4.1 SDS（Simple Dynamic String）&lt;/h3&gt;
&lt;p&gt;Redis 没有直接用 C 字符串，而是包了 SDS：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    L[&amp;#34;len&amp;lt;br/&amp;gt;已用长度（4B）&amp;lt;br/&amp;gt;= 5&amp;#34;] --- A[&amp;#34;alloc&amp;lt;br/&amp;gt;总容量&amp;lt;br/&amp;gt;= 8&amp;#34;] --- F[&amp;#34;flags&amp;lt;br/&amp;gt;编码位&amp;lt;br/&amp;gt;= 0&amp;#34;] --- B[&amp;#34;buf[]&amp;lt;br/&amp;gt;实际字符/二进制数据&amp;lt;br/&amp;gt;H e l l o \0 ...&amp;#34;]
    N1[&amp;#34;O(1) 取长度&amp;#34;] -.-&amp;gt; L
    N2[&amp;#34;末尾保留 \0&amp;lt;br/&amp;gt;兼容 C 字符串函数&amp;#34;] -.-&amp;gt; B&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;len&lt;/code&gt;：已用长度，取长度 &lt;code&gt;O(1)&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;alloc&lt;/code&gt;：分配的总空间；&lt;code&gt;free = alloc - len&lt;/code&gt; 可减少内存重分配。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;二进制安全&lt;/strong&gt;：内容中间可以有 &lt;code&gt;\0&lt;/code&gt;，长度由 &lt;code&gt;len&lt;/code&gt; 决定。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;预分配&lt;/strong&gt;：修改时若 &lt;code&gt;free&lt;/code&gt; 足够就不 &lt;code&gt;realloc&lt;/code&gt;；不足时按 2 倍或 1MB 上限预分配。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;42-ziplist压缩列表&#34;&gt;4.2 ziplist（压缩列表）&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;ziplist&lt;/code&gt; 是一块连续内存，把所有 entry 紧凑排列：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    A[&amp;#34;zlbytes&amp;lt;br/&amp;gt;总字节数&amp;lt;br/&amp;gt;4B&amp;#34;] --- B[&amp;#34;zltail&amp;lt;br/&amp;gt;尾偏移量&amp;lt;br/&amp;gt;4B&amp;#34;] --- C[&amp;#34;zllen&amp;lt;br/&amp;gt;节点数&amp;lt;br/&amp;gt;2B&amp;#34;] --- D[&amp;#34;entry1&amp;lt;br/&amp;gt;变长字节&amp;#34;] --- E[&amp;#34;entry2&amp;lt;br/&amp;gt;变长字节&amp;#34;] --- F[&amp;#34;...&amp;#34;] --- G[&amp;#34;zlend&amp;lt;br/&amp;gt;0xFF&amp;lt;br/&amp;gt;1B&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;每个 entry 的内部结构：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    A[&amp;#34;prevlen&amp;lt;br/&amp;gt;前节点长度&amp;lt;br/&amp;gt;1B 或 5B&amp;#34;] --- B[&amp;#34;encoding&amp;lt;br/&amp;gt;编码/长度类型&amp;lt;br/&amp;gt;1B 或更多&amp;#34;] --- C[&amp;#34;content&amp;lt;br/&amp;gt;实际数据&amp;lt;br/&amp;gt;变长&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;ul&gt;
&lt;li&gt;连续内存、无额外指针，所以&lt;strong&gt;省内存&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;插入/删除需要整体 &lt;code&gt;memmove&lt;/code&gt;，因此只用于&lt;strong&gt;小数据量&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Hash/List/ZSET&lt;/code&gt; 元素少且单元素短时先用 ziplist，超过阈值升级为 hashtable/skiplist。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;listpack（Redis 7 的替代者）&lt;/strong&gt;：ziplist 的致命伤就藏在 &lt;code&gt;prevlen&lt;/code&gt; 里——每个 entry 要记录&lt;strong&gt;前一个&lt;/strong&gt; entry 的长度：前 entry 小于 254 字节时 prevlen 占 1B，达到 254 字节时占 5B。于是一个 entry 从 253 涨到 254 字节，会让后面每个 entry 的 prevlen 从 1B 膨胀到 5B，膨胀又可能触发下一个 entry 继续膨胀——&lt;strong&gt;连锁更新&lt;/strong&gt;，最坏 O(N²)。&lt;/p&gt;
&lt;p&gt;listpack 的修法：&lt;strong&gt;每个 entry 只记录自己的总长度，且存在 entry 末尾&lt;/strong&gt;，entry 之间互不引用——改任何一个 entry 都不会波及邻居，连锁更新从根上消失。Redis 7 起 Hash/List/ZSET 的小对象编码全部换成 listpack，配置项同步改名（&lt;code&gt;hash-max-listpack-entries&lt;/code&gt; 等，旧的 &lt;code&gt;*-ziplist-*&lt;/code&gt; 名仍兼容）。面试回答「ziplist 和 listpack 的区别」，核心就是这一句：&lt;strong&gt;ziplist 记前驱长度所以连锁更新，listpack 记自身长度所以免疫&lt;/strong&gt;。&lt;/p&gt;
&lt;h3 id=&#34;43-quicklist快速列表&#34;&gt;4.3 quicklist（快速列表）&lt;/h3&gt;
&lt;p&gt;Redis 3.2 后，&lt;code&gt;List&lt;/code&gt; 的底层是 &lt;strong&gt;quicklist = 双向链表 + ziplist&lt;/strong&gt;：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    H[&amp;#34;head&amp;#34;] --&amp;gt; A[&amp;#34;ziplist A&amp;lt;br/&amp;gt;[a|b|c]&amp;lt;br/&amp;gt;count=3&amp;lt;br/&amp;gt;sz=...&amp;#34;] &amp;lt;--&amp;gt; B[&amp;#34;ziplist B&amp;lt;br/&amp;gt;[d|e|f|g]&amp;lt;br/&amp;gt;count=4&amp;lt;br/&amp;gt;sz=...&amp;#34;] &amp;lt;--&amp;gt; C[&amp;#34;...&amp;#34;]
    N[&amp;#34;每个节点是一个 ziplist&amp;lt;br/&amp;gt;节点之间用 prev/next 指针连接&amp;#34;] -.-&amp;gt; A&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;quicklistNode 结构：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    A[&amp;#34;prev&amp;lt;br/&amp;gt;指针&amp;#34;] --- B[&amp;#34;next&amp;lt;br/&amp;gt;指针&amp;#34;] --- C[&amp;#34;zl&amp;lt;br/&amp;gt;ziplist&amp;#34;] --- D[&amp;#34;sz&amp;lt;br/&amp;gt;字节数&amp;#34;] --- E[&amp;#34;count&amp;lt;br/&amp;gt;元素个数&amp;#34;] --- F[&amp;#34;encoding&amp;lt;br/&amp;gt;是否压缩&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;ul&gt;
&lt;li&gt;结合了双向链表「插入删除快」和 ziplist「省内存」的优点。&lt;/li&gt;
&lt;li&gt;每个节点只存少量元素，避免单个 ziplist 太大导致 &lt;code&gt;memmove&lt;/code&gt; 开销。&lt;/li&gt;
&lt;li&gt;Redis 7 起节点从 ziplist 换成 listpack（动机见 §4.2），但 quicklist 的「链表 + 紧凑块」思想不变。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;44-intset整数集合&#34;&gt;4.4 intset（整数集合）&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;Set&lt;/code&gt; 在元素&lt;strong&gt;全为整数且数量少&lt;/strong&gt;时使用 intset，否则升级为 &lt;code&gt;hashtable&lt;/code&gt;：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    A[&amp;#34;encoding&amp;lt;br/&amp;gt;编码类型&amp;lt;br/&amp;gt;2B/4B/8B&amp;#34;] --- B[&amp;#34;length&amp;lt;br/&amp;gt;元素个数&amp;lt;br/&amp;gt;4B&amp;#34;] --- C[&amp;#34;contents[]&amp;lt;br/&amp;gt;有序整数数组&amp;lt;br/&amp;gt;[1] [3] [5] [7] [9] ...&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;ul&gt;
&lt;li&gt;有序数组，支持二分查找 &lt;code&gt;O(log N)&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;所有元素用同一 encoding；加入更大范围整数时可能整体升级（如 int16 → int32），升级是 &lt;code&gt;O(N)&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;无重复元素（Set 语义）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;45-skiplist跳表&#34;&gt;4.5 skiplist（跳表）&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;ZSET&lt;/code&gt; 同时维护 &lt;strong&gt;skiplist + dict&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;skiplist&lt;/code&gt;：按 score 排序，支持范围查询 / 排名 &lt;code&gt;O(log N)&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dict&lt;/code&gt;：按 member 查 score，&lt;code&gt;O(1)&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;普通有序链表只能顺序扫描，跳表加了多层「快速通道」：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    subgraph L3[&amp;#34;level 3&amp;#34;]
        A3[&amp;#34;1&amp;#34;] --&amp;gt; B3[&amp;#34;9&amp;#34;] --&amp;gt; C3[&amp;#34;15&amp;#34;]
    end
    subgraph L2[&amp;#34;level 2&amp;#34;]
        A2[&amp;#34;1&amp;#34;] --&amp;gt; B2[&amp;#34;5&amp;#34;] --&amp;gt; C2[&amp;#34;9&amp;#34;] --&amp;gt; D2[&amp;#34;13&amp;#34;] --&amp;gt; E2[&amp;#34;15&amp;#34;]
    end
    subgraph L1[&amp;#34;level 1&amp;#34;]
        A1[&amp;#34;1&amp;#34;] --&amp;gt; B1[&amp;#34;3&amp;#34;] --&amp;gt; C1[&amp;#34;5&amp;#34;] --&amp;gt; D1[&amp;#34;7&amp;#34;] --&amp;gt; E1[&amp;#34;9&amp;#34;] --&amp;gt; F1[&amp;#34;11&amp;#34;] --&amp;gt; G1[&amp;#34;13&amp;#34;] --&amp;gt; H1[&amp;#34;15&amp;#34;]
    end
    A3 --- A2 --- A1
    B3 --- C2 --- E1
    C3 --- E2 --- H1
    HEAD[&amp;#34;头节点&amp;#34;] -.-&amp;gt; A1
    H1 -.-&amp;gt; TAIL[&amp;#34;尾节点&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;跳表节点结构：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    A[&amp;#34;member&amp;lt;br/&amp;gt;成员字符串&amp;lt;br/&amp;gt;（sds）&amp;#34;] --- B[&amp;#34;score&amp;lt;br/&amp;gt;分数&amp;lt;br/&amp;gt;（double）&amp;#34;] --- C[&amp;#34;forward[] 数组&amp;lt;br/&amp;gt;[L1指针， L2指针， L3指针， ...]&amp;lt;br/&amp;gt;最高层数 = 随机 1~32&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;概率平衡&lt;/strong&gt;：不用像红黑树那样做复杂旋转。&lt;/li&gt;
&lt;li&gt;查找 &lt;code&gt;score=11&lt;/code&gt; 时，先在高层大步跳跃，再逐层下降，平均 &lt;code&gt;O(log N)&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;配合 dict 实现 &lt;code&gt;ZSCORE&lt;/code&gt; 的 &lt;code&gt;O(1)&lt;/code&gt; 查询。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;五redis-持久化rdb-与-aof&#34;&gt;五、Redis 持久化：RDB 与 AOF&lt;/h2&gt;
&lt;p&gt;Redis 提供两种持久化方式，各有优劣。&lt;/p&gt;
&lt;h3 id=&#34;51-rdbredis-database&#34;&gt;5.1 RDB（Redis Database）&lt;/h3&gt;
&lt;p&gt;在指定时间间隔内将内存数据集快照写入磁盘。&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;save &lt;span style=&#34;color:#ae81ff&#34;&gt;900&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;      &lt;span style=&#34;color:#75715e&#34;&gt;# 900 秒内至少 1 次修改则触发&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;save &lt;span style=&#34;color:#ae81ff&#34;&gt;300&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;10&lt;/span&gt;     &lt;span style=&#34;color:#75715e&#34;&gt;# 300 秒内至少 10 次修改则触发&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;save &lt;span style=&#34;color:#ae81ff&#34;&gt;60&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;10000&lt;/span&gt;   &lt;span style=&#34;color:#75715e&#34;&gt;# 60 秒内至少 10000 次修改则触发&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;优点&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;文件紧凑，适合备份和灾难恢复。&lt;/li&gt;
&lt;li&gt;恢复速度快。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;缺点&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;两次快照之间数据可能丢失。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;bgsave&lt;/code&gt; 通过 &lt;code&gt;fork()&lt;/code&gt; 子进程生成，大数据集时可能阻塞主线程。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;52-aofappend-only-file&#34;&gt;5.2 AOF（Append Only File）&lt;/h3&gt;
&lt;p&gt;将每次写操作命令追加到日志文件。&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;appendonly yes
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;appendfsync everysec  &lt;span style=&#34;color:#75715e&#34;&gt;# 每秒刷盘一次（推荐）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# appendfsync always  # 每次写都刷盘，最安全但最慢&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# appendfsync no      # 由操作系统决定，最快但不安全&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;优点&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;数据安全性更高，默认最多丢失 1 秒数据。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;缺点&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;文件体积大。&lt;/li&gt;
&lt;li&gt;恢复速度慢。&lt;/li&gt;
&lt;li&gt;AOF 重写（rewrite）时会占用额外资源。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;53-混合持久化redis-40&#34;&gt;5.3 混合持久化（Redis 4.0+）&lt;/h3&gt;
&lt;p&gt;AOF 重写时先写入 RDB 全量数据，再追加增量 AOF，兼顾恢复速度和数据安全。&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;aof-use-rdb-preamble yes&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;graph LR
    A[持久化选择] --&amp;gt; B[RDB]
    A --&amp;gt; C[AOF]
    A --&amp;gt; D[混合持久化]
    B --&amp;gt; E[适合备份/快速恢复]
    C --&amp;gt; F[适合低丢失容忍]
    D --&amp;gt; G[生产推荐]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;八股重点&lt;/strong&gt;：Redis 的持久化是「灾难恢复手段」，不是「主存储方案」。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;六过期删除与内存淘汰&#34;&gt;六、过期删除与内存淘汰&lt;/h2&gt;
&lt;h3 id=&#34;61-过期删除策略&#34;&gt;6.1 过期删除策略&lt;/h3&gt;
&lt;p&gt;Redis 对过期 Key 采用两种策略结合：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;策略&lt;/th&gt;
					&lt;th&gt;说明&lt;/th&gt;
					&lt;th&gt;优点&lt;/th&gt;
					&lt;th&gt;缺点&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;惰性删除&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;访问 Key 时检查是否过期，过期则删除。&lt;/td&gt;
					&lt;td&gt;CPU 开销小&lt;/td&gt;
					&lt;td&gt;过期 Key 可能长期占用内存&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;定期删除&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;每隔一段时间随机抽查部分 Key 删除。&lt;/td&gt;
					&lt;td&gt;平衡内存与 CPU&lt;/td&gt;
					&lt;td&gt;仍可能有漏网之鱼&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&#34;62-内存淘汰策略&#34;&gt;6.2 内存淘汰策略&lt;/h3&gt;
&lt;p&gt;当内存达到 &lt;code&gt;maxmemory&lt;/code&gt; 时，根据 &lt;code&gt;maxmemory-policy&lt;/code&gt; 决定如何淘汰：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;策略&lt;/th&gt;
					&lt;th&gt;说明&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;noeviction&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;不淘汰，写操作直接报错。&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;allkeys-lru&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;所有 Key 中淘汰最近最少使用。&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;volatile-lru&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;只在设置了 TTL 的 Key 中淘汰最近最少使用。&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;allkeys-random&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;所有 Key 中随机淘汰。&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;volatile-random&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;只在设置了 TTL 的 Key 中随机淘汰。&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;volatile-ttl&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;淘汰即将过期的 Key。&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;allkeys-lfu&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;所有 Key 中淘汰使用频率最低的。&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;volatile-lfu&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;只在设置了 TTL 的 Key 中淘汰使用频率最低的。&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;生产建议&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;必须设置 &lt;code&gt;maxmemory&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;缓存场景推荐 &lt;code&gt;allkeys-lru&lt;/code&gt; 或 &lt;code&gt;allkeys-lfu&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;有冷热区分时可用 &lt;code&gt;volatile-lru&lt;/code&gt;，但必须给 Key 设置 TTL。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;七redis-高可用架构&#34;&gt;七、Redis 高可用架构&lt;/h2&gt;
&lt;h3 id=&#34;71-主从复制replication&#34;&gt;7.1 主从复制（Replication）&lt;/h3&gt;
&lt;p&gt;一主多从，读流量分摊到从节点。&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;graph LR
    M[Master&amp;lt;br/&amp;gt;写 + 读] --&amp;gt;|全量/增量同步| S1[Slave 1&amp;lt;br/&amp;gt;读]
    M --&amp;gt; S2[Slave 2&amp;lt;br/&amp;gt;读]
    M --&amp;gt; S3[Slave 3&amp;lt;br/&amp;gt;读]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;复制过程&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Slave 发送 &lt;code&gt;PSYNC runId offset&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;如果 runId 一致且 offset 在复制积压缓冲区中，则&lt;strong&gt;增量同步&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;否则&lt;strong&gt;全量同步&lt;/strong&gt;：Master 执行 &lt;code&gt;bgsave&lt;/code&gt; 生成 RDB，发送给 Slave。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;复制延迟问题&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;主从异步复制必然存在延迟。&lt;/li&gt;
&lt;li&gt;读从库时可能读到旧数据，需要业务容忍或强制走主库。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;72-哨兵sentinel&#34;&gt;7.2 哨兵（Sentinel）&lt;/h3&gt;
&lt;p&gt;实现自动故障转移，解决主节点宕机问题。&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;graph TB
    subgraph Sentinel 集群
        Se1[Sentinel 1]
        Se2[Sentinel 2]
        Se3[Sentinel 3]
    end
    M[Master] --&amp;gt; S1[Slave 1]
    M --&amp;gt; S2[Slave 2]
    Se1 --&amp;gt;|监控| M
    Se2 --&amp;gt;|监控| M
    Se3 --&amp;gt;|监控| M
    Se1 -.-&amp;gt; Se2
    Se2 -.-&amp;gt; Se3&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;核心机制&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;主观下线（SDOWN）&lt;/strong&gt;：单个 Sentinel 认为 Master 不可用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;客观下线（ODOWN）&lt;/strong&gt;：多数 Sentinel 达成一致。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Leader 选举&lt;/strong&gt;：Sentinel 之间通过 Raft 算法选举出执行故障转移的 Leader。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;故障转移&lt;/strong&gt;：选一个 Slave 提升为 Master，通知其他 Slave 和客户端。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;73-cluster-集群&#34;&gt;7.3 Cluster 集群&lt;/h3&gt;
&lt;p&gt;Redis Cluster 采用&lt;strong&gt;无中心架构&lt;/strong&gt;，数据按槽位分布。&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;graph TB
    subgraph Redis Cluster
        N1[Node 1&amp;lt;br/&amp;gt;0-5460]
        N2[Node 2&amp;lt;br/&amp;gt;5461-10922]
        N3[Node 3&amp;lt;br/&amp;gt;10923-16383]
    end
    N1 &amp;lt;--&amp;gt; N2
    N2 &amp;lt;--&amp;gt; N3
    N1 &amp;lt;--&amp;gt; N3&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;关键概念&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;集群共有 &lt;strong&gt;16384 个槽（slot）&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;每个 Key 通过 &lt;code&gt;CRC16(key) % 16384&lt;/code&gt; 计算所属槽。&lt;/li&gt;
&lt;li&gt;每个 Master 负责一部分槽，可配置 Slave 做高可用。&lt;/li&gt;
&lt;li&gt;客户端访问错误节点时，会收到 &lt;code&gt;MOVED&lt;/code&gt; 或 &lt;code&gt;ASK&lt;/code&gt; 重定向。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;MOVED vs ASK&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;MOVED&lt;/strong&gt;：槽已经永久迁移到目标节点。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ASK&lt;/strong&gt;：槽正在迁移中，先访问目标节点一次。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;八事务与-lua-脚本&#34;&gt;八、事务与 Lua 脚本&lt;/h2&gt;
&lt;h3 id=&#34;81-事务multiexecdiscardwatch&#34;&gt;8.1 事务（MULTI/EXEC/DISCARD/WATCH）&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;MULTI
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;SET a &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;INCR b
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;EXEC&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;特点&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;命令按顺序入队，EXEC 时一次性执行。&lt;/li&gt;
&lt;li&gt;执行期间不会被其他客户端打断。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;但没有回滚机制&lt;/strong&gt;：如果某条命令语法错误，整个事务不执行；如果运行时错误（如 INCR 字符串），后续命令仍会执行。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;82-lua-脚本&#34;&gt;8.2 Lua 脚本&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;EVAL &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;return redis.call(&amp;#39;incr&amp;#39;, KEYS[1])&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt; counter&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;特点&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;整个脚本作为一个整体在单线程中原子执行。&lt;/li&gt;
&lt;li&gt;适用于需要多个命令且要保证原子性的场景，如秒杀、分布式锁、限流。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;集群模式下所有 Key 必须落在同一 slot&lt;/strong&gt;，否则需要加 hash tag，如 &lt;code&gt;{user}:1001:profile&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;九缓存经典问题&#34;&gt;九、缓存经典问题&lt;/h2&gt;
&lt;h3 id=&#34;91-缓存穿透&#34;&gt;9.1 缓存穿透&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;问题&lt;/strong&gt;：查询不存在的数据，缓存不命中，请求直接打到数据库。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解法&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;缓存空值（设置短 TTL）。&lt;/li&gt;
&lt;li&gt;使用布隆过滤器预先判断 Key 是否存在。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;92-缓存击穿&#34;&gt;9.2 缓存击穿&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;问题&lt;/strong&gt;：热点 Key 突然过期，大量请求同时打到数据库。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解法&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;热点 Key 设置永不过期，异步更新。&lt;/li&gt;
&lt;li&gt;加互斥锁（SETNX），只允许一个线程重建缓存。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;93-缓存雪崩&#34;&gt;9.3 缓存雪崩&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;问题&lt;/strong&gt;：大量 Key 同时过期，或 Redis 宕机，数据库压力陡增。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解法&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;给 TTL 加随机值，避免同时失效。&lt;/li&gt;
&lt;li&gt;多级缓存（本地 + Redis）。&lt;/li&gt;
&lt;li&gt;熔断降级。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;94-缓存与数据库一致性&#34;&gt;9.4 缓存与数据库一致性&lt;/h3&gt;
&lt;p&gt;常见策略：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;策略&lt;/th&gt;
					&lt;th&gt;说明&lt;/th&gt;
					&lt;th&gt;问题&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;Cache Aside&lt;/td&gt;
					&lt;td&gt;读：先读缓存，未命中读 DB 并写回缓存；写：先更新 DB，再删缓存。&lt;/td&gt;
					&lt;td&gt;短暂不一致&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Read/Write Through&lt;/td&gt;
					&lt;td&gt;由缓存层统一维护 DB 和缓存。&lt;/td&gt;
					&lt;td&gt;实现复杂&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Write Behind&lt;/td&gt;
					&lt;td&gt;先写缓存，异步写 DB。&lt;/td&gt;
					&lt;td&gt;可能丢数据&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;最常用的是 Cache Aside&lt;/strong&gt;，配合「先更新数据库，再删除缓存」+ 延时双删 + 消息队列补偿，能在大多数场景下保证最终一致。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;十redis-在-iptv-系统中的角色&#34;&gt;十、Redis 在 IPTV 系统中的角色&lt;/h2&gt;
&lt;p&gt;IPTV 系统的特点是：&lt;strong&gt;高并发读、强实时性、热点集中、写流量脉冲式&lt;/strong&gt;。用户在换台、看 EPG、播放点播、抢购付费节目时，系统需要在毫秒级返回结果。&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TB
    subgraph 用户层
        A[机顶盒 / 手机 / Web]
    end

    subgraph 接入层
        B[CDN / 边缘节点]
        C[API Gateway]
    end

    subgraph 应用层
        D[鉴权服务]
        E[EPG 服务]
        F[播放调度服务]
        G[订单 / 秒杀服务]
    end

    subgraph 数据层
        H[(MySQL / 长视频元信息)]
        I[(对象存储 OSS)]
        J[(Redis 缓存 + 实时计算)]
    end

    A --&amp;gt; B --&amp;gt; C
    C --&amp;gt; D &amp;amp; E &amp;amp; F &amp;amp; G
    D --&amp;gt; J
    E --&amp;gt; J
    F --&amp;gt; J
    G --&amp;gt; J
    E --&amp;gt; H
    F --&amp;gt; I
    G --&amp;gt; H
    J -.-&amp;gt; H&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;场景&lt;/th&gt;
					&lt;th&gt;Redis 数据结构&lt;/th&gt;
					&lt;th&gt;作用&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;用户会话 / Token&lt;/td&gt;
					&lt;td&gt;String + EXPIRE&lt;/td&gt;
					&lt;td&gt;毫秒级鉴权&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;EPG 节目单缓存&lt;/td&gt;
					&lt;td&gt;Hash / JSON String&lt;/td&gt;
					&lt;td&gt;减少数据库查询&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;热播榜单 / 推荐位&lt;/td&gt;
					&lt;td&gt;Sorted Set (ZSET)&lt;/td&gt;
					&lt;td&gt;实时排序&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;播放限速 / 防刷&lt;/td&gt;
					&lt;td&gt;Lua + INCR + EXPIRE&lt;/td&gt;
					&lt;td&gt;滑动窗口限流&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;秒杀 / 付费解锁&lt;/td&gt;
					&lt;td&gt;SETNX + DECR + Lua&lt;/td&gt;
					&lt;td&gt;库存扣减&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;消息通知 / 任务队列&lt;/td&gt;
					&lt;td&gt;List / Stream&lt;/td&gt;
					&lt;td&gt;异步解耦&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id=&#34;十一iptv-项目-demo&#34;&gt;十一、IPTV 项目 Demo&lt;/h2&gt;
&lt;h3 id=&#34;111-用户会话缓存&#34;&gt;11.1 用户会话缓存&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; redis
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; uuid
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; time
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;r &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; redis&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;Redis(host&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;localhost&amp;#39;&lt;/span&gt;, port&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;6379&lt;/span&gt;, db&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;, decode_responses&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;True&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;login&lt;/span&gt;(user_id: str, device_id: str, ttl: int &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;3600&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; str:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    token &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; str(uuid&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;uuid4())
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    key &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;session:&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;{&lt;/span&gt;token&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    r&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;hset(key, mapping&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;user_id&amp;#34;&lt;/span&gt;: user_id,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;device_id&amp;#34;&lt;/span&gt;: device_id,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;login_at&amp;#34;&lt;/span&gt;: str(time&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;time())
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    })
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    r&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;expire(key, ttl)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; token
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;auth&lt;/span&gt;(token: str) &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; dict &lt;span style=&#34;color:#f92672&#34;&gt;|&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;None&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; r&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;hgetall(&lt;span style=&#34;color:#e6db74&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;session:&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;{&lt;/span&gt;token&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;112-epg-节目单缓存&#34;&gt;11.2 EPG 节目单缓存&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; json
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;from&lt;/span&gt; datetime &lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; datetime
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;cache_epg&lt;/span&gt;(channel_id: str, programs: list[dict]):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    key &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;epg:&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;{&lt;/span&gt;channel_id&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;:&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;{&lt;/span&gt;datetime&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;today()&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;strftime(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;%Y%m&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;%d&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;&lt;/span&gt;)&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    pipe &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; r&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;pipeline()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; p &lt;span style=&#34;color:#f92672&#34;&gt;in&lt;/span&gt; programs:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        pipe&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;hset(key, p[&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;start_time&amp;#39;&lt;/span&gt;], json&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;dumps(p))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    pipe&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;expire(key, &lt;span style=&#34;color:#ae81ff&#34;&gt;86400&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;2&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    pipe&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;execute()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;get_program&lt;/span&gt;(channel_id: str, start_time: str) &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; dict &lt;span style=&#34;color:#f92672&#34;&gt;|&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;None&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    data &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; r&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;hget(&lt;span style=&#34;color:#e6db74&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;epg:&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;{&lt;/span&gt;channel_id&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;:&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;{&lt;/span&gt;datetime&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;today()&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;strftime(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;%Y%m&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;%d&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;&lt;/span&gt;)&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;, start_time)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; json&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;loads(data) &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; data &lt;span style=&#34;color:#66d9ef&#34;&gt;else&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;None&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;113-热播排行榜&#34;&gt;11.3 热播排行榜&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;record_view&lt;/span&gt;(program_id: str, score: float &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1.0&lt;/span&gt;):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    r&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;zincrby(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;hot:tv:daily&amp;#34;&lt;/span&gt;, score, program_id)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;top_n&lt;/span&gt;(n: int &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;10&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; list[tuple[str, float]]:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; r&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;zrevrange(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;hot:tv:daily&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;, n &lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;, withscores&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;True&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;114-滑动窗口限流&#34;&gt;11.4 滑动窗口限流&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;LIMIT_SCRIPT &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&amp;#34;&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;local key = KEYS[1]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;local window = tonumber(ARGV[1])
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;local limit = tonumber(ARGV[2])
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;local now = tonumber(ARGV[3])
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;local min = now - window
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;redis.call(&amp;#39;zremrangebyscore&amp;#39;, key, &amp;#39;-inf&amp;#39;, min)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;local count = redis.call(&amp;#39;zcard&amp;#39;, key)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;if count &amp;lt; limit then
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;    redis.call(&amp;#39;zadd&amp;#39;, key, now, now)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;    redis.call(&amp;#39;expire&amp;#39;, key, math.ceil(window / 1000))
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;    return 1
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;else
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;    return 0
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;end
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;limiter &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; r&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;register_script(LIMIT_SCRIPT)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;allow&lt;/span&gt;(user_id: str, max_qps: int &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;10&lt;/span&gt;, window_ms: int &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1000&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; bool:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    key &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;rate_limit:&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;{&lt;/span&gt;user_id&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    now &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; int(time&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;time() &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1000&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; bool(limiter(keys&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;[key], args&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;[window_ms, max_qps, now]))&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;115-秒杀--限时解锁&#34;&gt;11.5 秒杀 / 限时解锁&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;SECKILL_SCRIPT &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&amp;#34;&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;local key = KEYS[1]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;local user_id = ARGV[1]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;local stock = tonumber(redis.call(&amp;#39;get&amp;#39;, key) or &amp;#39;0&amp;#39;)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;if stock &amp;lt;= 0 then
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;    return -1
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;end
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;if redis.call(&amp;#39;sismember&amp;#39;, key .. &amp;#39;:users&amp;#39;, user_id) == 1 then
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;    return 0
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;end
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;redis.call(&amp;#39;decr&amp;#39;, key)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;redis.call(&amp;#39;sadd&amp;#39;, key .. &amp;#39;:users&amp;#39;, user_id)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;return 1
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;seckill &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; r&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;register_script(SECKILL_SCRIPT)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;try_buy&lt;/span&gt;(activity_id: str, user_id: str, total_stock: int) &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; int:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    key &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;seckill:&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;{&lt;/span&gt;activity_id&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;:stock&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    r&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;setnx(key, total_stock)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; int(seckill(keys&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;[key], args&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;[user_id]))&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;hr&gt;
&lt;h2 id=&#34;十二为什么-redis-不适合做长期存储&#34;&gt;十二、为什么 Redis 不适合做长期存储？&lt;/h2&gt;
&lt;h3 id=&#34;121-持久化不是主存储&#34;&gt;12.1 持久化不是主存储&lt;/h3&gt;
&lt;p&gt;RDB 会丢数据，AOF 文件大且恢复慢。Redis 的持久化定位是「灾难恢复」，不是「可靠主存」。&lt;/p&gt;
&lt;h3 id=&#34;122-内存成本过高&#34;&gt;12.2 内存成本过高&lt;/h3&gt;
&lt;p&gt;内存价格是 SSD 的 10-50 倍。IPTV 的媒资、播放记录、订单流水动辄几十 TB，全放 Redis 不现实。&lt;/p&gt;
&lt;h3 id=&#34;123-单线程执行风险&#34;&gt;12.3 单线程执行风险&lt;/h3&gt;
&lt;p&gt;大数据集下，一个 &lt;code&gt;KEYS *&lt;/code&gt;、&lt;code&gt;HGETALL&lt;/code&gt; 或慢查询可能阻塞整个实例。&lt;/p&gt;
&lt;h3 id=&#34;124-缺乏复杂查询能力&#34;&gt;12.4 缺乏复杂查询能力&lt;/h3&gt;
&lt;p&gt;Redis 不支持 SQL、JOIN、二级索引。长期业务数据需要分析统计时，应使用 MySQL / PostgreSQL / ClickHouse。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;结论&lt;/strong&gt;：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Redis 适合「热数据 + 临时状态 + 实时计算」，价值在于速度；长期存储应交给磁盘数据库 + 对象存储。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;十三redis-容易踩坑的地方&#34;&gt;十三、Redis 容易踩坑的地方&lt;/h2&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;坑&lt;/th&gt;
					&lt;th&gt;说明&lt;/th&gt;
					&lt;th&gt;解法&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;大 Key&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;String &amp;gt; 10MB，集合元素百万级&lt;/td&gt;
					&lt;td&gt;拆分、分片、UNLINK 异步删除&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;热 Key&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;单个 Key 被高频访问&lt;/td&gt;
					&lt;td&gt;本地缓存、Key 分片、Cluster&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;缓存穿透/击穿/雪崩&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;缓存失效导致 DB 被打挂&lt;/td&gt;
					&lt;td&gt;布隆过滤器、互斥锁、随机 TTL&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;未设置 maxmemory&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;内存打满后写操作报错&lt;/td&gt;
					&lt;td&gt;设置 maxmemory 和淘汰策略&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;误用 KEYS&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;KEYS *&lt;/code&gt; 扫描全库阻塞主线程&lt;/td&gt;
					&lt;td&gt;生产禁用，改用 SCAN&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;Lua 跨 slot&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;Cluster 下脚本 Key 不在同一 slot&lt;/td&gt;
					&lt;td&gt;使用 hash tag &lt;code&gt;{user}:1001:xxx&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;事务没有回滚&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;EXEC 后某命令失败不会撤销前面命令&lt;/td&gt;
					&lt;td&gt;业务层保证幂等，或用 Lua&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;主从延迟&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;读从库可能读到旧数据&lt;/td&gt;
					&lt;td&gt;对一致性要求高的读走主库&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id=&#34;十四redis-做消息队列的方法&#34;&gt;十四、Redis 做消息队列的方法&lt;/h2&gt;
&lt;h3 id=&#34;141-list轻量队列&#34;&gt;14.1 List：轻量队列&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    P[Producer] --&amp;gt;|LPUSH queue:transcode task| R[(Redis List)]
    C[Consumer] --&amp;gt;|BRPOP queue:transcode 5| R
    C --&amp;gt; S[执行转码]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;r&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;lpush(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;queue:transcode&amp;#34;&lt;/span&gt;, json&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;dumps({&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;video_id&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;v123&amp;#34;&lt;/span&gt;}))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;_, task &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; r&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;brpop(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;queue:transcode&amp;#34;&lt;/span&gt;, timeout&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;5&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;优点&lt;/strong&gt;：简单、阻塞读。
&lt;strong&gt;缺点&lt;/strong&gt;：无 ACK、无多消费者组、无消息回溯。&lt;/p&gt;
&lt;h3 id=&#34;142-pubsub实时广播&#34;&gt;14.2 Pub/Sub：实时广播&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;r&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;publish(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;live:start&amp;#34;&lt;/span&gt;, json&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;dumps({&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;channel_id&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;cctv1&amp;#34;&lt;/span&gt;}))&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;优点&lt;/strong&gt;：实时、多订阅者。
&lt;strong&gt;缺点&lt;/strong&gt;：不持久化，消费者断线期间消息丢失。&lt;/p&gt;
&lt;h3 id=&#34;143-stream生产级队列&#34;&gt;14.3 Stream：生产级队列&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;r&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;xadd(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;mq:events&amp;#34;&lt;/span&gt;, {&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;event&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;video:play&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;user_id&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;u10086&amp;#34;&lt;/span&gt;})
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;r&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;xgroup_create(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;mq:events&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;group_log&amp;#34;&lt;/span&gt;, id&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;0&amp;#34;&lt;/span&gt;, mkstream&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;True&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;messages &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; r&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;xreadgroup(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    groupname&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;group_log&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    consumername&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;worker_1&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    streams&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;{&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;mq:events&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&amp;gt;&amp;#34;&lt;/span&gt;},
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    count&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;10&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    block&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;5000&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; stream, entries &lt;span style=&#34;color:#f92672&#34;&gt;in&lt;/span&gt; messages:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; msg_id, fields &lt;span style=&#34;color:#f92672&#34;&gt;in&lt;/span&gt; entries:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        process(fields)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        r&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;xack(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;mq:events&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;group_log&amp;#34;&lt;/span&gt;, msg_id)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;优点&lt;/strong&gt;：支持消费者组、ACK、消息回溯。&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;方式&lt;/th&gt;
					&lt;th&gt;推荐场景&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;List&lt;/td&gt;
					&lt;td&gt;简单任务队列&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Pub/Sub&lt;/td&gt;
					&lt;td&gt;实时广播&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Stream&lt;/td&gt;
					&lt;td&gt;生产级消息队列&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id=&#34;十五总结&#34;&gt;十五、总结&lt;/h2&gt;
&lt;p&gt;《Redis in Action》的核心思想可以概括为一句话：&lt;strong&gt;把 Redis 当成内存里的数据结构工具箱，而不是数据库&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Redis 适合&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;缓存加速（会话、配置、热点数据）。&lt;/li&gt;
&lt;li&gt;实时计算（排行榜、计数器、限流、秒杀）。&lt;/li&gt;
&lt;li&gt;异步解耦（List / Pub/Sub / Stream）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Redis 不适合&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;长期持久化存储。&lt;/li&gt;
&lt;li&gt;复杂查询与大数据分析。&lt;/li&gt;
&lt;li&gt;超大数据集主存储。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;避免踩坑的关键是：&lt;strong&gt;控制 Key 大小、设置合理 TTL、配置淘汰策略、慎用阻塞命令、理解持久化边界&lt;/strong&gt;。Redis 用好了是神兵利器，用错了就是生产事故的导火索。&lt;/p&gt;
</description>
    </item>
    <item>
      <title>starmall：一个微信小程序电商系统的全栈实战——预约、支付与订单架构解析</title>
      <link>https://ftmao.com/blog/posts/starmall-wechat-miniprogram-ecommerce-fullstack/</link>
      <pubDate>Tue, 20 Jul 2021 10:00:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/starmall-wechat-miniprogram-ecommerce-fullstack/</guid>
      <description>&lt;h1 id=&#34;starmall一个微信小程序电商系统的全栈实战预约支付与订单架构解析&#34;&gt;starmall：一个微信小程序电商系统的全栈实战——预约、支付与订单架构解析&lt;/h1&gt;
&lt;p&gt;这是我 2021 年做的一套完整电商系统 &lt;strong&gt;starmall&lt;/strong&gt;（六爻星辰）：微信小程序端 + Java 后端 + PC 管理后台，支持商品购买、老师咨询时段预约、择吉日查询、在线咨询等业务。整理开源出来，写篇文章做一次完整的技术复盘。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;前端（uni-app 小程序）：&lt;a href=&#34;https://gitee.com/soulnew/starmall&#34;&gt;https://gitee.com/soulnew/starmall&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;后端（Java + 管理端）：&lt;a href=&#34;https://gitee.com/soulnew/crm_server&#34;&gt;https://gitee.com/soulnew/crm_server&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;技术栈：Spring Boot 2.2 + MyBatis-Plus + MySQL + Redis / uni-app + uView / Vue 2 + Element UI（管理端）&lt;/li&gt;
&lt;li&gt;说明：后端是在开源商城 &lt;strong&gt;CRMEB Java 版&lt;/strong&gt; 基础上二次开发的，分销/秒杀/拼团/砍价等营销能力来自底座，预约系统、择吉日、名片、咨询聊天等是自研部分&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;一整体架构&#34;&gt;一、整体架构&lt;/h2&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;graph TD
    MP[&amp;#34;微信小程序端&amp;lt;br/&amp;gt;starmall uni-app + uView&amp;#34;] --&amp;gt;|&amp;#34;https://ftmao.com/api/front/**&amp;#34;| API[&amp;#34;SpringBoot 单体后端&amp;lt;br/&amp;gt;62 个 Mapper / 23 个前台 Controller&amp;#34;]
    H5[&amp;#34;H5 / App&amp;lt;br/&amp;gt;uni-app 多端编译&amp;#34;] --&amp;gt; API
    ADMIN[&amp;#34;PC 管理后台&amp;lt;br/&amp;gt;Vue 2 + Element UI + ECharts&amp;#34;] --&amp;gt;|&amp;#34;后台接口&amp;#34;| API
    API --&amp;gt; MYSQL[&amp;#34;MySQL 5.7&amp;lt;br/&amp;gt;62 张表 Druid 连接池&amp;#34;]
    API --&amp;gt; REDIS[&amp;#34;Redis&amp;lt;br/&amp;gt;token / 订单缓存 / 任务队列 / 配置缓存&amp;#34;]
    API --&amp;gt; WX[&amp;#34;微信开放平台&amp;lt;br/&amp;gt;登录 / 支付 V2 / 订阅消息&amp;#34;]
    API --&amp;gt; QN[&amp;#34;七牛云 OSS&amp;lt;br/&amp;gt;图片上传&amp;#34;]
    API --&amp;gt; QY[&amp;#34;企业微信机器人&amp;lt;br/&amp;gt;部署结果通知&amp;#34;]
    MP --&amp;gt;|&amp;#34;wss://... websocket&amp;#34;| CHAT[&amp;#34;咨询聊天服务&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;代码规模：后端约 850 个 Java 文件、12.6 万行；小程序前端业务代码约 1.2 万行；数据库初始化 SQL 全量在仓库里，共 62 张表。后端按业务分包：&lt;code&gt;front&lt;/code&gt;（小程序接口）、&lt;code&gt;order/payment&lt;/code&gt;、&lt;code&gt;marketing&lt;/code&gt;、&lt;code&gt;seckill&lt;/code&gt;、&lt;code&gt;task&lt;/code&gt;（定时任务）、&lt;code&gt;wechat&lt;/code&gt;，以及自研的 &lt;code&gt;appoint&lt;/code&gt;（预约）、&lt;code&gt;lunar&lt;/code&gt;（黄历）等模块。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;二功能介绍&#34;&gt;二、功能介绍&lt;/h2&gt;
&lt;h3 id=&#34;21-商品购买全流程&#34;&gt;2.1 商品购买全流程&lt;/h3&gt;
&lt;p&gt;标准的商城闭环：商品详情（多规格 SKU）→ 立即购买/加购物车 → 确认订单（优惠券、积分抵扣、运费计算）→ 创建订单 → 微信支付 → 轮询支付结果 → 收货评价 → 退款/售后。&lt;strong&gt;支付是真实对接的微信支付 V2&lt;/strong&gt;——后端手工实现 XML 报文 + MD5 签名调统一下单（&lt;code&gt;WeChatPayServiceImpl&lt;/code&gt;，746 行），前端弹起 &lt;code&gt;wx.requestPayment&lt;/code&gt;，不是 mock。&lt;/p&gt;
&lt;p&gt;此外还支持：余额支付、运费模板（满额包邮/指定城市包邮/按件数重量体积三种首续费）、物流查询、自提核销码、再次下单。营销玩法（秒杀、砍价、拼团、优惠券）来自 CRMEB 底座，一应俱全。&lt;/p&gt;
&lt;h3 id=&#34;22-老师咨询时段预约自研核心&#34;&gt;2.2 老师咨询时段预约（自研核心）&lt;/h3&gt;
&lt;p&gt;这是这个项目区别于普通商城的地方——卖的不只是实物商品，还有&amp;quot;老师的时间&amp;quot;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;商品分类为&amp;quot;老师类&amp;quot;时，商品详情页出现一个&lt;strong&gt;时段选择器&lt;/strong&gt;（每天 6 个时段：9/11/13/15/17/19 点）；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;teacher_plan&lt;/code&gt; 表按「日期 × 时段」预生成号源，被约走的时段前端置灰；&lt;/li&gt;
&lt;li&gt;下单时携带 &lt;code&gt;teacherPlanId&lt;/code&gt;，&lt;strong&gt;创建订单的同一个事务里&lt;/strong&gt;把该时段标记为已约——号源占用和订单创建要么一起成功要么一起回滚，不会出现&amp;quot;钱付了时段没了&amp;quot;；&lt;/li&gt;
&lt;li&gt;号源由定时任务 &lt;code&gt;TeacherPlanTask&lt;/code&gt; 每 8 小时自动向前补齐未来 8 天，并清理过期的空闲号源。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;23-其他自研功能&#34;&gt;2.3 其他自研功能&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;择吉日查询&lt;/strong&gt;：4.7 万行黄历数据入库（&lt;code&gt;lunar&lt;/code&gt; 系列表），按&amp;quot;宜&amp;quot;事反查吉日，比如查&amp;quot;最近适合搬家的日子&amp;quot;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;在线咨询&lt;/strong&gt;：基于 WebSocket 长连接的实时聊天，支持文字/语音/图片消息（详见第三章「长连接聊天」）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;老师名片&lt;/strong&gt;、文章资讯、签到积分、充值、小程序订阅消息提醒；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;管理后台&lt;/strong&gt;：商品/订单/营销/分销/财务/短信/按钮级权限/数据统计大屏（ECharts）。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;三后端能力全景&#34;&gt;三、后端能力全景&lt;/h2&gt;
&lt;p&gt;后端虽然是 SpringBoot 单体，但能力面相当完整：前台 23 个 Controller、62 个 MyBatis Mapper，按业务分包组织：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;graph TD
    subgraph M1[&amp;#34;业务模块&amp;#34;]
        A1[&amp;#34;front 前台接口&amp;lt;br/&amp;gt;23 个 Controller&amp;#34;]
        A2[&amp;#34;order / payment&amp;lt;br/&amp;gt;订单与支付&amp;#34;]
        A3[&amp;#34;marketing / seckill / bargain / combination&amp;lt;br/&amp;gt;优惠券 秒杀 砍价 拼团&amp;#34;]
        A4[&amp;#34;appoint / lunar / businessCard&amp;lt;br/&amp;gt;预约 择吉日 名片（自研）&amp;#34;]
        A5[&amp;#34;finance / statistics / export&amp;lt;br/&amp;gt;充值提现 数据统计 Excel 导出&amp;#34;]
        A6[&amp;#34;wechat / wechatMessage / sms&amp;lt;br/&amp;gt;微信生态与短信&amp;#34;]
        A7[&amp;#34;task&amp;lt;br/&amp;gt;定时任务体系&amp;#34;]
    end
    subgraph M2[&amp;#34;公共层&amp;#34;]
        B1[&amp;#34;拦截器链&amp;lt;br/&amp;gt;前台 Token / 后台 Token+权限 / Swagger&amp;#34;]
        B2[&amp;#34;工具层 RedisUtil / WxPayUtil / CrmebUtil&amp;#34;]
        B3[&amp;#34;ThreadLocal 上下文传递当前用户&amp;#34;]
    end
    A1 --&amp;gt; B1
    A2 --&amp;gt; B2
    B1 --&amp;gt; B3&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;商城主链路之外，后端还有一批容易被忽略的能力：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;微信公众号消息自动回复&lt;/strong&gt;：&lt;code&gt;wechatMessage&lt;/code&gt; 模块支持文字/语音/图片/图文四种回复类型，关注、取关、扫码等事件都有对应处理；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;模板消息与订阅消息&lt;/strong&gt;：小程序订阅消息、公众号模板消息的增删查与群发，且发送走异步任务（&lt;code&gt;AsyncWeChatProgramTempMessage&lt;/code&gt; 等），不阻塞主流程；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;渠道二维码与素材管理&lt;/strong&gt;：带参数的推广二维码、微信素材的上传管理；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据导出与统计&lt;/strong&gt;：POI 实现的 Excel 导出、交易/商品/用户多维统计接口（配合管理端 ECharts 大屏）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;短信与物流&lt;/strong&gt;：短信验证码/通知、快递物流轨迹查询；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;接口文档&lt;/strong&gt;：Swagger 在线文档 + 仓库内置 admin/app/公共三份离线接口文档。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;31-长连接聊天websocket架构&#34;&gt;3.1 长连接聊天（WebSocket）架构&lt;/h3&gt;
&lt;p&gt;在线咨询是这个系统的&amp;quot;第二前台&amp;quot;，采用的是 &lt;strong&gt;WebSocket 长连接 + HTTP 接口&lt;/strong&gt;的组合方案。部署上两个服务跑在同一台服务器，由 nginx 按路径前缀分流：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;graph TD
    MP[&amp;#34;小程序聊天页&amp;lt;br/&amp;gt;pages/chat&amp;#34;] --&amp;gt;|&amp;#34;wss://.../websocket/userId/sessionId&amp;#34;| NGX[&amp;#34;nginx 按路径分流&amp;#34;]
    MP --&amp;gt;|&amp;#34;HTTP /chat/session/**  /chat/msg/**&amp;#34;| NGX
    MP --&amp;gt;|&amp;#34;HTTP /api/front/**&amp;#34;| NGX
    NGX --&amp;gt; CHAT[&amp;#34;聊天服务&amp;lt;br/&amp;gt;WebSocket 长连接 + 会话/消息接口&amp;#34;]
    NGX --&amp;gt; MALL[&amp;#34;商城后端 crm_server&amp;lt;br/&amp;gt;SpringBoot 18080&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;从开源的小程序端实现和接口契约，可以看到这套长连接聊天的几个设计要点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;连接即身份&lt;/strong&gt;：WebSocket 握手 URL 直接携带 &lt;code&gt;userId/sessionId&lt;/code&gt;，连接建立的同时就完成了身份识别与会话路由，不需要额外的登录帧；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实时走长连接，持久化走 HTTP&lt;/strong&gt;：收发消息走 WebSocket（消息体为 JSON）；会话绑定（&lt;code&gt;session/bindSession&lt;/code&gt;）、会话列表、删除会话、历史消息拉取（&lt;code&gt;msg/msgList&lt;/code&gt;）都走 &lt;code&gt;/chat/&lt;/code&gt; 前缀的 HTTP 接口——长连接管&amp;quot;实时推送&amp;quot;，HTTP 管&amp;quot;可查询、可回溯&amp;quot;，职责分得很干净；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;统一的消息类型协议&lt;/strong&gt;：&lt;code&gt;contentType&lt;/code&gt; 1=文字、2=语音（带 &lt;code&gt;contentDuration&lt;/code&gt; 时长）、3=图片（先经 &lt;code&gt;user/upload/image&lt;/code&gt; 上传拿到 URL，再把 URL 作为消息内容发出）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;语音消息的移动端交互&lt;/strong&gt;：按住说话（&lt;code&gt;touchstart&lt;/code&gt;/&lt;code&gt;touchend&lt;/code&gt; 控制录音起止）、上滑取消（&lt;code&gt;touchmove&lt;/code&gt;/&lt;code&gt;touchcancel&lt;/code&gt;），体验和微信原生聊天对齐；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;发送体验乐观化&lt;/strong&gt;：消息发出后先本地 push 进消息列表立即上屏，&lt;code&gt;send&lt;/code&gt; 的 fail 回调再 toast 兜底提示。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;需要如实说明：聊天服务端的代码没有随这两个仓库开源（它部署在同一台服务器上，由 nginx 分流），仓库里能看到的生产实现是小程序端；&lt;code&gt;crm_server&lt;/code&gt; 里的 &lt;code&gt;com/im/SocketServer.java&lt;/code&gt; 只是当时练手的原生 TCP Socket demo（12345 端口），并非线上实现。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;四技术亮点&#34;&gt;四、技术亮点&lt;/h2&gt;
&lt;h3 id=&#34;41-下单幂等两段式--唯一键兜底&#34;&gt;4.1 下单幂等：两段式 + 唯一键兜底&lt;/h3&gt;
&lt;p&gt;小程序网络环境下用户双击、重试、断网重发是常态，下单接口必须幂等。这里用了一个两段式设计：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;graph TD
    A[&amp;#34;order/confirm 确认订单页&amp;#34;] --&amp;gt; B[&amp;#34;订单数据序列化存 Redis&amp;lt;br/&amp;gt;key = md5 当前时间&amp;lt;br/&amp;gt;有效期 60 分钟&amp;#34;]
    B --&amp;gt; C[&amp;#34;order/create/orderKey 创建订单&amp;#34;]
    C --&amp;gt; D{&amp;#34;第一步:查库判重&amp;lt;br/&amp;gt;StoreOrder.unique = orderKey&amp;#34;}
    D --&amp;gt;|&amp;#34;已存在&amp;#34;| E[&amp;#34;直接返回已有订单&amp;lt;br/&amp;gt;不重复创建&amp;#34;]
    D --&amp;gt;|&amp;#34;不存在&amp;#34;| F{&amp;#34;第二步:校验 Redis 缓存&amp;lt;br/&amp;gt;是否过期/被用过&amp;#34;}
    F --&amp;gt;|&amp;#34;过期&amp;#34;| G[&amp;#34;拒绝 提示重新确认&amp;#34;]
    F --&amp;gt;|&amp;#34;有效&amp;#34;| H[&amp;#34;事务内创建订单&amp;lt;br/&amp;gt;成功后删除缓存&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;&lt;code&gt;unique&lt;/code&gt; 字段是最后的兜底——即使 Redis 挂了、缓存丢了，数据库唯一约束也保证同一个 orderKey 不会建出两张单。&lt;/p&gt;
&lt;h3 id=&#34;42-原子扣库存防超卖&#34;&gt;4.2 原子扣库存防超卖&lt;/h3&gt;
&lt;p&gt;库存扣减没有用&amp;quot;先查后改&amp;quot;，而是一条 SQL 完成：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// setSql + 条件改写，数据库层面原子完成&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;stock &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; stock &lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt; n   且要求   stock &lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt; n &lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;=&lt;/span&gt; 0&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;数据库单行更新的原子性天然挡住并发超卖，不需要分布式锁。秒杀/砍价/拼团场景更复杂：同一事务内要扣&lt;strong&gt;活动表 + 活动 SKU 表 + 商品表 + 商品 SKU 表&lt;/strong&gt;四份库存，任何一步失败整体回滚。&lt;/p&gt;
&lt;h3 id=&#34;43-编程式事务包住整个建单过程&#34;&gt;4.3 编程式事务包住整个建单过程&lt;/h3&gt;
&lt;p&gt;创建订单是个多写操作：扣库存、占预约时段、写订单主表、写订单详情、核销优惠券、写订单日志。代码用 &lt;code&gt;TransactionTemplate&lt;/code&gt; 编程式事务（而不是 &lt;code&gt;@Transactional&lt;/code&gt; 注解）把这一串操作包起来——好处是事务边界精确可控，不会把整个方法（包括不需要事务的远程调用）都圈进去。订单核心逻辑集中在 &lt;code&gt;OrderUtils.java&lt;/code&gt;（1500+ 行）和 &lt;code&gt;OrderServiceImpl.java&lt;/code&gt;（近 1000 行）。&lt;/p&gt;
&lt;h3 id=&#34;44-支付回调防重--异步化&#34;&gt;4.4 支付回调：防重 + 异步化&lt;/h3&gt;
&lt;p&gt;微信支付的回调处理（&lt;code&gt;CallbackServiceImpl.weChat&lt;/code&gt;，448 行）有几个关键点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;解析 XML、校验 &lt;code&gt;return_code&lt;/code&gt;/&lt;code&gt;result_code&lt;/code&gt;，按 &lt;code&gt;attach&lt;/code&gt; 里携带的 userId + type 区分是订单支付还是充值；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;已支付直接返回 SUCCESS&lt;/strong&gt;——微信会多次重推回调，幂等是回调接口的生死线；&lt;/li&gt;
&lt;li&gt;事务内完成 &lt;code&gt;updatePaid&lt;/code&gt;（置订单已支付）+ 扣积分 + 拼团状态处理；&lt;/li&gt;
&lt;li&gt;后续的&amp;quot;慢动作&amp;quot;——发短信、推订阅消息、分销结算——&lt;strong&gt;不放在回调里同步做&lt;/strong&gt;，而是 &lt;code&gt;lPush&lt;/code&gt; 进 Redis 队列异步执行，回调接口尽快给微信返回，避免超时重推。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&#34;45-没有-mqredis-list--定时任务的务实替代&#34;&gt;4.5 没有 MQ：Redis List + 定时任务的务实替代&lt;/h3&gt;
&lt;p&gt;项目没有引入 RabbitMQ/RocketMQ，而是用 &lt;strong&gt;Redis List 当队列 + Spring &lt;code&gt;@Scheduled&lt;/code&gt; 定时任务消费&lt;/strong&gt;，撑起了一整套异步任务体系：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;graph LR
    A[&amp;#34;下单成功&amp;lt;br/&amp;gt;lPush ORDER_AUTO_CANCEL_KEY&amp;#34;] --&amp;gt; Q[&amp;#34;Redis List 队列&amp;#34;]
    P[&amp;#34;支付成功回调&amp;lt;br/&amp;gt;lPush 后续动作&amp;#34;] --&amp;gt; Q
    Q --&amp;gt; T1[&amp;#34;OrderAutoCancelTask&amp;lt;br/&amp;gt;每分钟消费&amp;lt;br/&amp;gt;超时未支付自动取消 + 回滚库存&amp;#34;]
    Q --&amp;gt; T2[&amp;#34;自动收货 / 自动好评&amp;#34;]
    Q --&amp;gt; T3[&amp;#34;优惠券过期 / 砍价截止&amp;lt;br/&amp;gt;佣金解冻&amp;#34;]
    Q --&amp;gt; T4[&amp;#34;TeacherPlanTask&amp;lt;br/&amp;gt;每8小时补齐号源&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;&lt;code&gt;task/order/&lt;/code&gt; 下共有 8 个定时任务。对这个体量的系统，省掉一个 MQ 的部署运维成本，换来的是完全够用的可靠性——这是小项目技术选型上我很认可的一个取舍。&lt;/p&gt;
&lt;h3 id=&#34;46-鉴权链路uuid-token--redis--threadlocal&#34;&gt;4.6 鉴权链路：UUID Token + Redis + ThreadLocal&lt;/h3&gt;
&lt;p&gt;没有用 JWT，而是更传统的方案：登录后签发 UUID 存 Redis（按模块区分 key 前缀），拦截器 &lt;code&gt;FrontTokenInterceptor&lt;/code&gt; → &lt;code&gt;CheckFrontToken&lt;/code&gt; → &lt;code&gt;TokenManagerImpl&lt;/code&gt; 逐级校验，通过后把当前用户放进 &lt;code&gt;ThreadLocal&lt;/code&gt; 供全链路取用。管理端是 Token + 权限双拦截器。好处是&lt;strong&gt;服务端可随时踢人下线&lt;/strong&gt;（删 Redis key 即可），这是纯 JWT 做不到的。&lt;/p&gt;
&lt;h3 id=&#34;47-其他值得提的实现&#34;&gt;4.7 其他值得提的实现&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;订单快照&lt;/strong&gt;：下单时把购物车内容序列化成 JSON 存进 &lt;code&gt;StoreOrderInfo.info&lt;/code&gt;，之后商品改价改名都不影响历史订单的展示；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;运费计算引擎&lt;/strong&gt;：&lt;code&gt;getOrderPriceGroup&lt;/code&gt; 支持满额包邮、指定城市包邮（件数 + 金额双条件）、按件数/重量/体积三种首续计费模式；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;XSS 转义&lt;/strong&gt;：用户输入统一 &lt;code&gt;escapeHtml4&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;部署脚本&lt;/strong&gt;：&lt;code&gt;start.sh&lt;/code&gt; nohup 启动 + 轮询日志判断启动成功 + 企业微信机器人推送部署结果——单机部署也搞出了仪式感。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;五复盘与遗憾&#34;&gt;五、复盘与遗憾&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;底座与自研的边界&lt;/strong&gt;：秒杀/拼团/砍价/分销这些&amp;quot;重&amp;quot;能力直接复用 CRMEB 底座，把精力集中在业务差异化（预约、择吉日、聊天）上——对个人开发者来说这是正确的杠杆；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;单体的局限&lt;/strong&gt;：所有东西在一个 SpringBoot 应用里，定时任务、支付、前台接口互相影响，量大了必然要拆；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;原子扣库存的返回值没检查&lt;/strong&gt;：&lt;code&gt;setSql&lt;/code&gt; 扣减失败（库存不足）时影响的行数为 0，但代码没有校验这个返回值，极端情况下会漏判超卖——这是现在回头看想补的一个洞；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;聊天服务端未开源&lt;/strong&gt;：如第三章所述，长连接聊天的服务端实现没有随仓库开源，小程序端和接口契约是完整的，算是开源的一点遗憾。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;六写在最后&#34;&gt;六、写在最后&lt;/h2&gt;
&lt;p&gt;这个项目让我完整走了一遍&amp;quot;真实收钱&amp;quot;的系统该有的样子：幂等、事务、防超卖、回调防重、异步补偿——每一个点背后都是电商系统趟过的坑。代码在 &lt;a href=&#34;https://gitee.com/soulnew/starmall&#34;&gt;https://gitee.com/soulnew/starmall&lt;/a&gt; 和 &lt;a href=&#34;https://gitee.com/soulnew/crm_server&#34;&gt;https://gitee.com/soulnew/crm_server&lt;/a&gt;，建库 SQL、接口文档都在仓库里，希望能给做小程序商城的朋友一个可参照的完整样本。&lt;/p&gt;
</description>
    </item>
    
  </channel>
</rss>
