<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>IPTV on 方糖猫Blog</title>
    <link>https://ftmao.com/blog/tags/iptv/</link>
    <description>AI 视频、AI Agent 与工程实践笔记</description>
    <generator>Hugo</generator>
    <language>zh-CN</language>
    <lastBuildDate>Mon, 19 Oct 2026 10:00:00 +0800</lastBuildDate><atom:link href="https://ftmao.com/blog/tags/iptv/index.xml" rel="self" type="application/rss+xml" /><item>
      <title>C 端 IPTV 产品的 Java 高并发架构设计：高可用、高性能、零信任与缓存一致性</title>
      <link>https://ftmao.com/blog/posts/iptv-java-high-concurrency-architecture/</link>
      <pubDate>Mon, 19 Oct 2026 10:00:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/iptv-java-high-concurrency-architecture/</guid>
      <description>&lt;h1 id=&#34;c-端-iptv-产品的-java-高并发架构设计高可用高性能零信任与缓存一致性&#34;&gt;C 端 IPTV 产品的 Java 高并发架构设计：高可用、高性能、零信任与缓存一致性&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;过去几年我一直在做一个面向海外的 C 端 IPTV 产品（STB 机顶盒 + 移动端，多品牌多租户，跑在 AWS 上）。这篇文章是我对这套系统架构的复盘：高并发、高可用、高性能、高安全、缓存一致性这几件事我们分别是怎么做的、踩过什么坑。过程中我们落地了不少 Netflix 的开源组件和公开实践（Hystrix、EVCache、Zuul 的网关思想、混沌工程等），但每一个决策都是经过我们自己生产环境验证或修正的，不是照搬。文中方案细节来自真实的系统设计文档（Video Cloud 目标模型、vod-auth、CloudRouter、DRM、网盘整改等），脱敏后分享。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;一先搞清楚对手c-端-iptv-的流量模型&#34;&gt;一、先搞清楚对手：C 端 IPTV 的流量模型&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;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;晚 8 点 - 11 点是高峰，大促、球赛、爆款剧上线时流量成倍脉冲&lt;/td&gt;
					&lt;td&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;EPG 节目单、Meta 元数据、Layout 下发占了 9 成以上流量&lt;/td&gt;
					&lt;td&gt;缓存优先，读路径尽量不打 DB&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;播放鉴权是超级热点&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;每次起播、切台都要鉴权取播放链接，QPS 随在线用户数线性涨&lt;/td&gt;
					&lt;td&gt;热点接口单独优化（缓存 + 短 TTL + 滑动续期）&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;设备在线心跳&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;app 轮询 session，百万级设备定时上报&lt;/td&gt;
					&lt;td&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;同一套系统服务多个品牌（巴西、俄语区等）&lt;/td&gt;
					&lt;td&gt;分库隔离爆炸半径，account_id 前缀区分品牌&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;这个流量形状——读多写少、晚间高峰、播放路径是生命线——决定了后面所有的架构取舍。&lt;/p&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 TB
    subgraph 终端
        STB[&amp;#34;STB 机顶盒&amp;#34;]
        APP[&amp;#34;移动 App&amp;#34;]
        WEB[&amp;#34;Web / Portal&amp;#34;]
    end

    subgraph 接入层
        ELB[&amp;#34;AWS ELB&amp;#34;]
        APISIX[&amp;#34;APISIX 网关&amp;lt;br/&amp;gt;限流 Sentinel / WAF / 鉴权前置&amp;#34;]
    end

    subgraph 微服务层
        AUTH[&amp;#34;vod-auth&amp;lt;br/&amp;gt;登录注册 / 账号 / 子账号&amp;#34;]
        SESS[&amp;#34;common-session&amp;lt;br/&amp;gt;会话管理&amp;#34;]
        META[&amp;#34;vod-meta-api&amp;lt;br/&amp;gt;元数据 / Layout / EPG&amp;#34;]
        PROF[&amp;#34;profile&amp;lt;br/&amp;gt;观看历史 / 收藏 / 偏好&amp;#34;]
        CLOUD[&amp;#34;vod-cloud&amp;lt;br/&amp;gt;个人网盘&amp;#34;]
        ROUTER[&amp;#34;CloudRouter&amp;lt;br/&amp;gt;播放链接路由&amp;#34;]
        DRM[&amp;#34;drmlicense&amp;lt;br/&amp;gt;DRM 鉴权与 license 下发&amp;#34;]
    end

    subgraph 数据层
        MYSQL[&amp;#34;MySQL RDS&amp;lt;br/&amp;gt;按品牌分库&amp;#34;]
        REDIS[&amp;#34;Redis Cluster&amp;lt;br/&amp;gt;Session / 验证码 / Meta 缓存&amp;#34;]
        METAREDIS[&amp;#34;Meta 专用 Redis Cluster&amp;#34;]
        ES[&amp;#34;Elasticsearch&amp;lt;br/&amp;gt;检索字段&amp;#34;]
        KAFKA[&amp;#34;Kafka MSK&amp;lt;br/&amp;gt;Meta 同步 / 网盘同步&amp;#34;]
    end

    subgraph 内容侧
        STREAM[&amp;#34;Streaming 流媒体&amp;#34;]
        CDN[&amp;#34;CDN / Squid 边缘缓存&amp;#34;]
        CP[&amp;#34;cloudProxy&amp;lt;br/&amp;gt;第三方网盘适配&amp;#34;]
    end

    STB --&amp;gt; ELB
    APP --&amp;gt; ELB
    WEB --&amp;gt; ELB
    ELB --&amp;gt; APISIX
    APISIX --&amp;gt; AUTH
    APISIX --&amp;gt; SESS
    APISIX --&amp;gt; META
    APISIX --&amp;gt; PROF
    APISIX --&amp;gt; CLOUD
    APISIX --&amp;gt; ROUTER
    ROUTER --&amp;gt; DRM
    ROUTER --&amp;gt; STREAM
    STREAM --&amp;gt; CDN
    STREAM --&amp;gt; CP
    META --&amp;gt; METAREDIS
    META --&amp;gt; ES
    AUTH --&amp;gt; MYSQL
    AUTH --&amp;gt; REDIS
    SESS --&amp;gt; REDIS
    CLOUD --&amp;gt; KAFKA
    META --&amp;gt; KAFKA&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;app -&amp;gt; ELB -&amp;gt; APISIX -&amp;gt; vod 微服务&lt;/code&gt;，限流用 Sentinel，网关做鉴权前置和 WAF。网关选型时我认真研究过 Netflix Zuul 2 从同步阻塞改异步非阻塞的历程——结论是网关本身绝不能成为瓶颈。我们体量没那么大，APISIX（底层同样是 Nginx + Lua 的高性能异步模型）已经够用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;服务拆分&lt;/strong&gt;：按业务域拆（Auth / Profile / Meta / Cloud / Session），不是按技术层拆。领域拆分的完整过程在&lt;a href=&#34;https://ftmao.com/blog/blog/posts/iptv/&#34;&gt;视频系统 DDD 实战&lt;/a&gt;里写过。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据层&lt;/strong&gt;：MySQL 按品牌分库（&lt;code&gt;{brand}_account_db&lt;/code&gt; 等）；Redis 承载 Session、验证码、热点数据；Meta 用&lt;strong&gt;独立的 Redis Cluster&lt;/strong&gt; 全量缓存，与业务缓存物理隔离；检索字段入 ES。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;异步化&lt;/strong&gt;：Kafka（MSK）只干两件事——Meta 同步管道和网盘同步，不滥用。这是我给自己立的规矩：每加一个 MQ topic 都要回答「同步调用为什么不行」。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;三高并发把流量挡在离用户最近的地方&#34;&gt;三、高并发：把流量挡在离用户最近的地方&lt;/h2&gt;
&lt;p&gt;高并发的核心心法就一句：&lt;strong&gt;每一层都尽量把请求消化掉，别让它穿透到下一层&lt;/strong&gt;。越靠近 DB 越贵。&lt;/p&gt;
&lt;h3 id=&#34;31-网关层第一道削峰闸&#34;&gt;3.1 网关层：第一道削峰闸&lt;/h3&gt;
&lt;p&gt;我们在网关层做了三件事：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;限流&lt;/strong&gt;：APISIX 自带 limit-req / limit-count 插件做粗粒度限流，Sentinel 做服务级细粒度熔断限流（现网组合就是「ELK + Skywalking + Sentinel（APISIX）」）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;鉴权前置&lt;/strong&gt;：明显非法的请求（无 token、签名错误、UA 黑名单）在网关直接 401/403，不放行到业务服务。被攻击时这一层挡掉的流量经常比业务层处理的还多。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;静态配置下发走 CDN&lt;/strong&gt;：&lt;code&gt;/ui/cfg/v1&lt;/code&gt;、&lt;code&gt;/layout/v1&lt;/code&gt; 这类终端配置接口，内容变化频率低，直接走 CDN 缓存，源站只承担回源请求。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;32-热点接口播放鉴权链接的缓存设计&#34;&gt;3.2 热点接口：播放鉴权链接的缓存设计&lt;/h3&gt;
&lt;p&gt;CloudRouter（播放源路由服务）是我手里最热的路径：每次起播都要按 &lt;code&gt;logicStreamId&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;graph LR
    A[&amp;#34;App 起播请求&amp;lt;br/&amp;gt;logicStreamId&amp;#34;] --&amp;gt; B{&amp;#34;查 Redis&amp;#34;}
    B -- &amp;#34;命中&amp;#34; --&amp;gt; C[&amp;#34;刷新 TTL 滑动续期&amp;lt;br/&amp;gt;直接返回链接&amp;#34;]
    B -- &amp;#34;未命中&amp;#34; --&amp;gt; D[&amp;#34;查 MySQL 两表&amp;lt;br/&amp;gt;logic_stream + file_source&amp;#34;]
    D --&amp;gt; E[&amp;#34;构建下载链接 DownloadBO&amp;lt;br/&amp;gt;link / fileSize / expired&amp;#34;]
    E --&amp;gt; F[&amp;#34;写回 Redis&amp;#34;]
    F --&amp;gt; G[&amp;#34;返回&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;命中即续期（滑动 TTL）&lt;/strong&gt;：热播内容的链接永不过期，冷内容自然淘汰——不需要人工预热。这个设计替我省掉了整个「热点内容运营」的麻烦。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缓存对象自带过期时间&lt;/strong&gt;：链接本身有 &lt;code&gt;expired&lt;/code&gt; 字段，客户端在链接到期前会重新请求，和缓存 TTL 是两层独立的过期机制，互不相信。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;失败快速抛出&lt;/strong&gt;：查不到直接抛业务异常，不做 fallback 到第三方存储的慢路径——播放链路每一毫秒都值钱。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&#34;33-多级缓存l1--l2&#34;&gt;3.3 多级缓存：L1 + L2&lt;/h3&gt;
&lt;p&gt;纯 Redis 方案在超热点 key 上依然会打满单个分片（新片上线当晚的 metaId 我真实遇到过）。为了解决这个问题我研究了 Netflix 的 EVCache——它的多副本加客户端本地缓存思路给了我启发，但我们体量用不上 EVCache 全套，于是我基于 Caffeine（L1 进程内缓存）+ Redis（L2）自己实现了一个两级缓存框架并开源，见&lt;a href=&#34;https://ftmao.com/blog/blog/posts/kecache-intro/&#34;&gt;我开发的 keCache：面向 Spring Boot 3 的 L1 + L2 两级缓存框架&lt;/a&gt;，要点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;L1 短 TTL（秒级），扛住瞬时洪峰；&lt;/li&gt;
&lt;li&gt;L1 失效消息走 Redis Pub/Sub 广播，保证多实例间 L1 不一致的窗口可控；&lt;/li&gt;
&lt;li&gt;序列化用 Kryo，比 JDK 序列化小 3-5 倍。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;34-写路径异步化mq-削峰&#34;&gt;3.4 写路径异步化：MQ 削峰&lt;/h3&gt;
&lt;p&gt;写少但集中的场景（新片上线批量导入 Meta、网盘大规模刷新）我全部走 Kafka：&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;vod-sync-dispatcher&amp;lt;br/&amp;gt;每 30 秒轮询上游片源&amp;#34;] --&amp;gt; B[&amp;#34;Kafka MSK&amp;#34;]
    B --&amp;gt; C[&amp;#34;vod-imdb-meta-sync&amp;lt;br/&amp;gt;元数据入 Redis + ES&amp;#34;]
    D[&amp;#34;vod-cloud-sync-dispatcher&amp;#34;] --&amp;gt; B
    B --&amp;gt; E[&amp;#34;vod-cloud-sync-worker&amp;lt;br/&amp;gt;刷新网盘&amp;#34;]
    F[&amp;#34;http2Msk&amp;lt;br/&amp;gt;大数据 HTTP 回调转 MQ&amp;#34;] --&amp;gt; B
    B --&amp;gt; G[&amp;#34;偏好计算&amp;lt;br/&amp;gt;you may like&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;生产者-消费者模型 + MQ 缓冲，让上游脉冲（片源方一次推几万条 meta）被摊平成消费者能吃得动的匀速流。当时的 MQ 选型过程见&lt;a href=&#34;https://ftmao.com/blog/blog/posts/mq-comparison-rabbitmq-kafka-rocketmq/&#34;&gt;消息队列总结：RabbitMQ、Kafka、RocketMQ 与 Redis MQ&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;另一个异步化的例子是我设计的转码调度：&lt;strong&gt;Quartz + DB 状态机&lt;/strong&gt;驱动，每个片源转出 1-3 个分辨率、每个分辨率一个任务，用「动态归一化权重调度算法」分配执行器（priority = 客户权重 / 活跃任务数，兼顾多客户公平与并发槽位控制），30 分钟轮询任务进度。慢不要紧，稳和公平才要紧——这是典型的「离线任务用状态机换可靠性」。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;四高可用假设一切依赖都会挂&#34;&gt;四、高可用：假设一切依赖都会挂&lt;/h2&gt;
&lt;p&gt;我做高可用的两条心法，是从 Netflix 的工程实践里学来并在我们系统里反复验证过的：&lt;strong&gt;Design for failure&lt;/strong&gt;（为失败而设计）和 &lt;strong&gt;Blast radius&lt;/strong&gt;（控制爆炸半径）。落地到我们系统：&lt;/p&gt;
&lt;h3 id=&#34;41-熔断降级与兜底重试&#34;&gt;4.1 熔断、降级与兜底重试&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;熔断&lt;/strong&gt;：我们早期直接用 Netflix 开源的 Hystrix 做熔断隔离，Hystrix 停更后迁移到 Resilience4j（网关层用 Sentinel）。工具换了，规则不变：下游超时率/错误率超阈值就熔断，快速失败，绝不线程排队等死。Hystrix 的线程池隔离思想对我们影响很大——核心链路（登录、起播）和非核心链路（埋点上报）的资源池必须分开。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;降级而非失败&lt;/strong&gt;：Meta 自动匹配失败时我定的策略是&lt;strong&gt;降级创建默认元数据&lt;/strong&gt;，不影响文件源状态——内容能播永远比信息完美重要。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;兜底重试&lt;/strong&gt;：CloudAsset 的异常处理是我给全系统定的统一模式——&lt;strong&gt;3 次以内本轮调度重试，超过 3 次任务进失败状态，由独立的失败任务调度器兜底&lt;/strong&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;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;graph TB
    A[&amp;#34;任务执行&amp;#34;] --&amp;gt; B{&amp;#34;成功?&amp;#34;}
    B -- &amp;#34;是&amp;#34; --&amp;gt; C[&amp;#34;完成&amp;#34;]
    B -- &amp;#34;否, failCount+1&amp;#34;] --&amp;gt; D{&amp;#34;failCount ＞ 3?&amp;#34;}
    D -- &amp;#34;否&amp;#34; --&amp;gt; A
    D -- &amp;#34;是&amp;#34; --&amp;gt; E[&amp;#34;status = fail&amp;#34;]
    E --&amp;gt; F[&amp;#34;独立失败任务调度器&amp;lt;br/&amp;gt;定时扫描兜底重试&amp;#34;]
    F --&amp;gt; A&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;后来我把这类「一定要跑到」的可靠执行逻辑抽成了通用组件开源，见&lt;a href=&#34;https://ftmao.com/blog/blog/posts/ke-reliable-executor/&#34;&gt;我开发的 KE Reliable Executor&lt;/a&gt;。&lt;/p&gt;
&lt;h3 id=&#34;42-多租户隔离分库就是隔舱&#34;&gt;4.2 多租户隔离：分库就是隔舱&lt;/h3&gt;
&lt;p&gt;多品牌共享一套代码，但我在数据层做了物理隔离：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;MySQL 按品牌分库（&lt;code&gt;{brand}_account_db&lt;/code&gt;、&lt;code&gt;{brand}_profile_db&lt;/code&gt;），公共数据在 &lt;code&gt;vod_meta_db&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;account_id&lt;/code&gt; 前缀区分品牌（MFC:11、TVELA:12、WAKA:13），应用层强制路由；&lt;/li&gt;
&lt;li&gt;实体普遍带 &lt;code&gt;env&lt;/code&gt; / &lt;code&gt;vnoId&lt;/code&gt; 字段，&lt;code&gt;logicStreamId&lt;/code&gt; 内嵌环境标识。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;某品牌数据被打爆、误删、需要回滚时，影响面天然被限制在单库内。这就是水密隔舱——后来某品牌做数据订正时我真切体会到这个设计的价值。&lt;/p&gt;
&lt;h3 id=&#34;43-依赖解耦订阅模式&#34;&gt;4.3 依赖解耦：订阅模式&lt;/h3&gt;
&lt;p&gt;DRM 链路里我有个坚持：drmlicense 不直接依赖 Headend 和 drmencryptor 的在线接口，而是以&lt;strong&gt;订阅模式&lt;/strong&gt;收 &lt;code&gt;DrmContentInfoBO&lt;/code&gt; 和 &lt;code&gt;ContentKeyBO&lt;/code&gt;，自己组装 logicalStream 与 PSSH 的关系。上游挂了，license 下发用已订阅到的数据照常工作。原因是算术：&lt;strong&gt;同步调用是可用性的乘法，异步订阅是加法&lt;/strong&gt;——一个可用性 99.9% 的服务同步串依赖十个同样 99.9% 的服务，链路可用性就只剩 99% 了。&lt;/p&gt;
&lt;h3 id=&#34;44-混沌工程轻量版就够&#34;&gt;4.4 混沌工程：轻量版就够&lt;/h3&gt;
&lt;p&gt;Netflix 的 Chaos Monkey 是随机杀生产实例，逼所有服务习惯「邻居随时消失」。我评估后没敢直接上生产，但保留了这个思想的两个轻量实践：预发环境定期随机关停单个实例观察告警与自愈；每个季度做一次依赖演练（手动切断 Redis 从库、Kafka 单 broker），验证降级路径真的能用。我的原则：&lt;strong&gt;降级方案不演练就等于没有&lt;/strong&gt;——每次演练几乎都能发现某条降级路径已经悄悄失效。&lt;/p&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;strong&gt;Meta 全量入内存&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;元数据全量进专用 Redis Cluster，终端 &lt;code&gt;/root/v1&lt;/code&gt;、&lt;code&gt;/tree/v1&lt;/code&gt; 等接口零 DB 访问&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;检索走 ES&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;只有关键检索字段入 Elasticsearch，不全量双写&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;播放内容边缘缓存&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;Streaming 前置 Squid + CDN，热点片源命中边缘节点，源站只承担回源（对应 Netflix Open Connect 的角色，我们用商用 CDN 实现）&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;大表治理&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;观看历史表现网 2-3 亿行，是我一直盯着的最大风险点：每天定时任务删 90 天前历史 + 按账号双表拆分（设备账号/普通账号）&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;JVM 与连接池&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;G1/ZGC、HikariCP 连接池按「连接数 = (核数 × 2) + 磁盘数」起步压测调优，慢 SQL 进 Skywalking 告警&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;原则：&lt;strong&gt;性能优化的 80% 是让请求根本到不了慢的地方&lt;/strong&gt;，剩下 20% 才是调参数。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;六缓存一致性与缓存高可用&#34;&gt;六、缓存一致性与缓存高可用&lt;/h2&gt;
&lt;p&gt;这是全文我最想细说的一章。缓存是性能的来源，也是我事故最多的地方。&lt;/p&gt;
&lt;h3 id=&#34;61-一致性策略先写-db后删缓存&#34;&gt;6.1 一致性策略：先写 DB，后删缓存&lt;/h3&gt;
&lt;p&gt;vod-auth 的账号数据我用的是标准的 &lt;strong&gt;Cache Aside + 写后删除&lt;/strong&gt;：先写 DB，成功后再删缓存，下次读时回源重建。为什么选「删除」而不是「更新」？我想清楚过：「先写 DB 再删缓存」只有在「读线程恰好踩着写间隙回源了旧值」的极端窗口才会短暂不一致，而「更新缓存」在并发双写下会乱序覆盖出脏数据。删除的失败模式是 miss（回源一次，便宜），更新的失败模式是脏数据（播错内容，贵）。&lt;/p&gt;
&lt;p&gt;对一致性要求更高的场景（套餐权益、付费状态），我在删除之上再加一层：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;延迟双删&lt;/strong&gt;：写 DB → 删缓存 → 延迟 N 秒 → 再删一次，兜住间隙回源；&lt;/li&gt;
&lt;li&gt;或用 MQ 发缓存失效消息，由消费者负责删除 + 失败重试，把「删缓存失败」这个尾巴也闭环掉。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;62-缓存三大经典问题的组合拳&#34;&gt;6.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;穿透&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;恶意刷不存在的 metaId / accountId&lt;/td&gt;
					&lt;td&gt;布隆过滤器拦截 + 空值短 TTL 缓存（30 秒）&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;逻辑过期 + 互斥重建：value 里带逻辑过期时间，过期后只放一个线程回源，其余返回旧值&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 同 TTL 同时到期&lt;/td&gt;
					&lt;td&gt;TTL 加随机抖动（基础值 ± 20%）+ Redis Cluster 多主分摊&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;逻辑过期方案有个工程细节：回源重建要用分布式锁（Meta 绑定我用的就是分布式锁，获取失败快速失败、重新调度），且锁的 TTL 必须大于回源耗时，finally 里释放——我在 CloudAsset 方案里专门写了「任何异常都会释放分布式锁、清除 ThreadLocal」，这是血泪经验，锁泄漏一次就够排查一晚上。&lt;/p&gt;
&lt;h3 id=&#34;63-缓存自身的高可用&#34;&gt;6.3 缓存自身的高可用&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Redis Cluster 多主 + 从&lt;/strong&gt;：Meta 缓存我用独立集群，和 Session/验证码的集群物理隔离——Meta 集群被打挂不影响用户登录。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多级缓存兜底&lt;/strong&gt;：Redis 整体不可用时，L1（Caffeine）还能扛住读过期窗口，服务降级而不是全灭。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;从 EVCache 学到的运营观&lt;/strong&gt;：我研究 Netflix EVCache 时最大的收获不是 memcached 本身，而是&lt;strong&gt;他们把缓存当成 tier-0 服务运营&lt;/strong&gt;——独立容量规划、独立监控、独立 SLO。我照这个思路给缓存集群配了和业务服务同级别的告警与容量评审。缓存在高并发系统里不是优化项，是一级基础设施——这点很多团队要到出事才懂，我庆幸自己学得早。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;七零信任安全与必要防御&#34;&gt;七、零信任安全与必要防御&lt;/h2&gt;
&lt;h3 id=&#34;71-零信任永不信任始终验证&#34;&gt;7.1 零信任：永不信任，始终验证&lt;/h3&gt;
&lt;p&gt;我们的安全体系是在几次整改后逐步按零信任思路重建的——内网不再等于可信，&lt;strong&gt;每一次调用都要验证身份，不论它来自哪里&lt;/strong&gt;。落地到我的系统，是这几件事：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;用户侧&lt;/strong&gt;：accessToken（JWT，&lt;code&gt;sub = accountId&lt;/code&gt;）+ 播放票据 tk12（内含 &lt;code&gt;expireTime / accountId / streamId&lt;/code&gt;，由平台密钥签发）。App 起播时两个都要验，token 验「你是谁」，tk12 验「你能不能播这条流、能播到什么时候」。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;设备侧&lt;/strong&gt;：账号卡与设备绑定（&lt;code&gt;ac_device_binding&lt;/code&gt;），&lt;code&gt;device_limit&lt;/code&gt; 限制并发设备数，挤掉盗号共享的盈利空间。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;服务间&lt;/strong&gt;：内部接口（&lt;code&gt;/internal-api/...&lt;/code&gt;）不走公网网关，服务间调用带服务身份凭证。我评估过 mTLS + SPIFFE 这套工作负载身份方案（Netflix 零信任改造用的就是这条路），对我们体量偏重，最终落地为「内部接口不暴露 + 调用方白名单」，等规模上来再升级。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;最小权限&lt;/strong&gt;：cloudProxy 是我专门做的收敛层——把对第三方网盘（Mega 等）的所有访问收敛成一个适配层，只暴露 &lt;code&gt;checkPassword&lt;/code&gt; 和 &lt;code&gt;getDownloadLink&lt;/code&gt; 两个接口，业务服务根本不接触第三方凭证。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;72-一次真实的安全整改网盘密码泄露&#34;&gt;7.2 一次真实的安全整改：网盘密码泄露&lt;/h3&gt;
&lt;p&gt;这是我亲手推动的整改，值得完整复盘。背景：网盘打包工具生成的加密网盘包里&lt;strong&gt;含账号 + 加密后的登录密码&lt;/strong&gt;，加解密密钥一旦泄露，所有网盘账号密码全部泄露。&lt;/p&gt;
&lt;p&gt;整改分两步走：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第一步，密码不落地。&lt;/strong&gt; 新网盘包不再写密码，改为写入与网盘账号一一对应的 &lt;code&gt;shareKey&lt;/code&gt;（UUID）。导入时只校验 shareKey 一致性，通过后把网盘与用户关联。按网盘包 &lt;code&gt;version&lt;/code&gt; 字段区分新老包走新旧两条校验逻辑，平滑迁移——&lt;strong&gt;存量兼容是安全改造里最容易被低估的工作量&lt;/strong&gt;，这一步我花的精力比新逻辑本身还多。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第二步，密钥体系升级。&lt;/strong&gt; 对称加密改 RSA 非对称加密：&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
    A[&amp;#34;CP / Reseller&amp;lt;br/&amp;gt;只拿到 RSA 公钥&amp;#34;] --&amp;gt; B[&amp;#34;公钥加密网盘包&amp;lt;br/&amp;gt;公钥泄露也无法解包&amp;#34;]
    C[&amp;#34;VNO 上传私钥&amp;lt;br/&amp;gt;加密保存于平台 pkg_rsa_key 表&amp;#34;] --&amp;gt; D[&amp;#34;导入时按 VNO 查私钥&amp;#34;]
    B --&amp;gt; E[&amp;#34;网盘包带签名&amp;lt;br/&amp;gt;含 pkgId + keyId&amp;#34;]
    E --&amp;gt; D
    D --&amp;gt; F[&amp;#34;先验签再解密&amp;lt;br/&amp;gt;解密成功才关联用户&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;配套我设计的&lt;strong&gt;密钥轮换机制&lt;/strong&gt;：平台同时最多两个 Key 生效；更新时旧密钥先进入最长 30 天缓冲期，再生成新密钥对；存在生效中的旧密钥则不允许再次更新——防止轮换频率失控导致历史包无法解密。&lt;/p&gt;
&lt;p&gt;另外扫码登录链路我也推动做过同类加固：扫码站点从 S3 静态托管（明文登录）迁移到托管服务并接入加固方案，鉴权统一收口到 vod-auth。&lt;/p&gt;
&lt;h3 id=&#34;73-接口消息体加密https-之上的第二道锁&#34;&gt;7.3 接口消息体加密：HTTPS 之上的第二道锁&lt;/h3&gt;
&lt;p&gt;有人会问：全站都上 HTTPS 了，为什么还要在应用层再加密一次消息体？我的理由有三个，都来自实际教训：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;HTTPS 防不住终端侧的抓包&lt;/strong&gt;。Charles/Fiddler 挂个代理证书，App 和网关之间的明文报文一览无余——协议字段、业务参数、token 用法全暴露。C 端 App 发出去的那一刻起，客户端环境就不可信。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;HTTPS 到网关就终止了&lt;/strong&gt;。TLS 在 ELB/APISIX 卸载后，内网转发的是明文；零信任的原则是内网也不可信，敏感报文在网关之后依然应该处于加密状态。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;IPTV 场景的特殊性&lt;/strong&gt;：播放鉴权、充值、账号接口一旦被逆向出报文格式，黑产就可以脱离 App 直接伪造请求刷接口。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;我设计的方案是 &lt;strong&gt;TLS 为底线 + 应用层 AES 加密消息体&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;App 启动&amp;lt;br/&amp;gt;ECDH 密钥协商&amp;lt;br/&amp;gt;网关公钥经 SSL Pinning 校验&amp;#34;] --&amp;gt; B[&amp;#34;得到会话密钥 sessionKey&amp;lt;br/&amp;gt;按 session 绑定&amp;#34;]
    B --&amp;gt; C[&amp;#34;业务请求&amp;lt;br/&amp;gt;AES-256-GCM 加密 body&amp;#34;]
    C --&amp;gt; D[&amp;#34;明文头带 token + 时间戳&amp;lt;br/&amp;gt;+ nonce + HMAC 签名&amp;#34;]
    D --&amp;gt; E[&amp;#34;网关统一解密过滤器&amp;lt;br/&amp;gt;验签名 / 验时间窗 / 验 nonce&amp;#34;]
    E --&amp;gt; F[&amp;#34;业务服务收到明文&amp;lt;br/&amp;gt;响应同样加密回传&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;工程上的几个取舍，都是试出来的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;密钥协商用 ECDH，不用硬编码密钥&lt;/strong&gt;。早期图省事在 App 里内置过 AES 密钥，App 被反编译后全线失守，改成登录/启动时 ECDH 动态协商、密钥绑定 session、服务端定期强制轮换。SSL Pinning（证书固定）挡住大部分中间人代理，让 ECDH 协商本身不被劫持。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AES-256-GCM 而不是 CBC&lt;/strong&gt;：GCM 自带完整性校验（AEAD），加密和防篡改一次完成；密钥 nonce 绝不复用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;头部明文、body 加密&lt;/strong&gt;：token、时间戳、nonce、签名放明文头里，网关不解密就能先做限流和鉴权前置；body 整体加密，解密收敛在网关的统一过滤器里，业务服务无感知——加解密逻辑散落进每个 Controller 是维护灾难。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不是所有接口都加密&lt;/strong&gt;。EPG、Meta 这类公开内容接口只走 HTTPS + 签名；登录、充值、播放鉴权、账号操作这些敏感接口才上消息体加密。全量加密的 CPU 成本和排查问题的痛苦不成比例——加密后的日志排障要靠专门的脱密工具，这也是成本。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;绝不自研算法&lt;/strong&gt;。所有原语都用成熟实现（AES-GCM、ECDH、HMAC-SHA256），自己拼协议可以，自己造算法不行。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;74-必要防御防攻击清单&#34;&gt;7.4 必要防御：防攻击清单&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;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;接口刷量 / CC 攻击&lt;/td&gt;
					&lt;td&gt;网关限流 + Sentinel 熔断；登录、验证码接口按 IP + 账号双维度计数&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;短信/邮件轰炸机&lt;/td&gt;
					&lt;td&gt;验证码存 Redis 带 TTL，&lt;strong&gt;验证成功后立即删除&lt;/strong&gt;防重放；单账号/单 IP 频次限制&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;重放攻击&lt;/td&gt;
					&lt;td&gt;请求带时间戳 + nonce，token 短过期&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;抓包 / 中间人&lt;/td&gt;
					&lt;td&gt;HTTPS + SSL Pinning + 敏感接口消息体加密（见 7.3）&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;越权访问&lt;/td&gt;
					&lt;td&gt;tk12 绑定 &lt;code&gt;accountId + streamId&lt;/code&gt;，换账号播放直接鉴权失败&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;篡改网盘包&lt;/td&gt;
					&lt;td&gt;RSA 签名验签，验签不过不解密&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;撞库 / 盗号&lt;/td&gt;
					&lt;td&gt;登录失败计数锁定（&lt;code&gt;account_black_list&lt;/code&gt;）、设备绑定、子账号体系隔离&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;敏感信息外泄&lt;/td&gt;
					&lt;td&gt;交接文档脱敏（我们的交接文档里曾出现明文环境密码，是我抓出来的典型反面教材）；日志脱敏；DB 密码字段只存哈希&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&#34;75-drm内容安全的最后一道门&#34;&gt;7.5 DRM：内容安全的最后一道门&lt;/h3&gt;
&lt;p&gt;DRM 链路的安全设计我同样按零信任思路做：ContentKey 由独立密钥服务（drmencryptor）管理，&lt;strong&gt;先查缓存没有再向外部 DRM 厂商申请并落库缓存&lt;/strong&gt;；播放优先走离线 license（有缓存用缓存），PSSH 异常才降级在线 license。license 本身带过期时间，App 本地不持久化密钥。系统支持 Goose DRM 与 Multi-DRM（Widevine）切换，切换动作就是删旧流加新流——密钥体系与业务流解耦，这个设计让后来的 DRM 厂商迁移几乎没动业务代码。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;八可观测性没有度量就没有高可用&#34;&gt;八、可观测性：没有度量就没有高可用&lt;/h2&gt;
&lt;p&gt;我们的现网组合：&lt;strong&gt;ELK 日志 + Skywalking 链路追踪 + Prometheus/Grafana 指标 + Nacos 配置中心&lt;/strong&gt;，每个子系统独立 ES index。&lt;/p&gt;
&lt;p&gt;我压测过的容量数据（v7.16 集群）：只写日志约 11 万条/秒，只写 Skywalking 约 2 万条/秒，混合写时日志 6 万 + 链路 1 万/秒。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;血泪教训&lt;/strong&gt;：周一高峰 Skywalking 曾出现 4 小时写入延迟，日志量增长也让 Logstash 排队。事后我立了两条规矩：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;日志与链路采样分离&lt;/strong&gt;：高峰期链路数据降采样，保住日志；两者绝不能互相拖死。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;本地日志保留 30 天&lt;/strong&gt;，crontab 定时清理——磁盘写满导致的 ELK 雪崩比业务故障还常见。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;我还坚持一点：告警要打在&lt;strong&gt;用户可感知的指标&lt;/strong&gt;上（起播成功率、鉴权失败率、链接过期率），而不是只打 CPU 内存——机器好好的但用户看不了，是最尴尬的监控盲区。服务间的依赖关系复杂到一定程度后，「告警来了不知道谁影响了谁」会成为新问题，Netflix 为此专门做过一套服务拓扑系统，我研究过它的实现并整理了笔记：&lt;a href=&#34;https://ftmao.com/blog/blog/posts/netflix-service-topology-at-scale/&#34;&gt;Netflix 服务拓扑系统笔记&lt;/a&gt;。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;九总结一张-checklist&#34;&gt;九、总结：一张 checklist&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;用到的 Netflix 开源组件 / 实践&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;高并发&lt;/td&gt;
					&lt;td&gt;网关限流、缓存优先、MQ 削峰、多级缓存&lt;/td&gt;
					&lt;td&gt;Zuul 2 异步化思想、EVCache 启发 keCache&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;高可用&lt;/td&gt;
					&lt;td&gt;熔断降级、失败兜底调度器、多租户分库隔离、订阅模式解耦&lt;/td&gt;
					&lt;td&gt;Hystrix（后迁 Resilience4j/Sentinel）、Chaos Monkey 轻量版&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;高性能&lt;/td&gt;
					&lt;td&gt;Meta 全量入内存、ES 检索、CDN/Squid 边缘缓存、大表治理&lt;/td&gt;
					&lt;td&gt;Open Connect 的边缘缓存角色&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;缓存一致性&lt;/td&gt;
					&lt;td&gt;先写 DB 后删缓存 + 延迟双删、逻辑过期防击穿、随机 TTL 防雪崩&lt;/td&gt;
					&lt;td&gt;EVCache 的缓存运营观&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;零信任&lt;/td&gt;
					&lt;td&gt;双票据鉴权、设备绑定、内部接口白名单、最小权限适配层&lt;/td&gt;
					&lt;td&gt;零信任改造思路（mTLS/SPIFFE 评估后暂缓）&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;攻击防御&lt;/td&gt;
					&lt;td&gt;网关 WAF/限流、验证码防重放、RSA 验签、黑名单、敏感接口消息体加密&lt;/td&gt;
					&lt;td&gt;—&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;可观测&lt;/td&gt;
					&lt;td&gt;ELK + Skywalking + Prometheus，日志链路采样分离&lt;/td&gt;
					&lt;td&gt;服务拓扑系统的思路&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;最后一句实话：&lt;strong&gt;这套架构没有一样是发明出来的，全部是踩坑踩出来的&lt;/strong&gt;。网盘密码泄露、Skywalking 四小时延迟、2-3 亿行的历史大表——每个设计决策背后都有一次事故或一次险些事故。Netflix 的开源组件和博客帮我少踩了很多坑，但终究要自己踩过才知道坑有多深。架构师的职责不是画出完美的图，而是让每个坑只踩一次。&lt;/p&gt;
</description>
    </item>
    <item>
      <title>视频系统 DDD 实战：巴西 IPTV 项目的领域拆分</title>
      <link>https://ftmao.com/blog/posts/iptv/</link>
      <pubDate>Fri, 23 Aug 2024 17:00:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/iptv/</guid>
      <description>&lt;h1 id=&#34;视频系统-ddd-实战巴西-iptv-项目的领域拆分&#34;&gt;视频系统 DDD 实战：巴西 IPTV 项目的领域拆分&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;2021 年到 2025 年，我负责面向巴西市场的 IPTV 视频点播系统。这是一个用户量过百万、业务持续演进的系统。在几次大的产品迭代中，我们引入了领域驱动设计（DDD）来控制系统复杂度。这篇文章会完整复盘当时的领域拆分过程。&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;ul&gt;
&lt;li&gt;视频上传：支持 tus / FTP / 网盘 / m3u8 等多种来源&lt;/li&gt;
&lt;li&gt;视频转码：h264 / h265 多码率输出&lt;/li&gt;
&lt;li&gt;DRM 加密：永新 / 自研加密方案&lt;/li&gt;
&lt;li&gt;元数据管理：同步 IMDb / TMDB / PP 等外部数据源&lt;/li&gt;
&lt;li&gt;播放列表：专题、分类、推荐&lt;/li&gt;
&lt;li&gt;播放鉴权：用户权限、设备管理、防盗链&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;服务用户：&lt;strong&gt;200 万+&lt;/strong&gt;，峰值日活 &lt;strong&gt;40 万&lt;/strong&gt;。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;二为什么需要-ddd&#34;&gt;二、为什么需要 DDD？&lt;/h2&gt;
&lt;h3 id=&#34;21-早期的问题&#34;&gt;2.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&gt;controller/
service/
dao/
entity/&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;这种结构在业务简单时没问题，但随着功能增加，开始出现：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一个需求改动涉及多个「层」，改哪里不确定&lt;/li&gt;
&lt;li&gt;业务逻辑散落在 Service 中，相似功能重复实现&lt;/li&gt;
&lt;li&gt;转码、鉴权、元数据互相耦合，改一个模块影响多个模块&lt;/li&gt;
&lt;li&gt;新成员上手慢，需要全局理解才敢改代码&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;22-ddd-带来的改变&#34;&gt;2.2 DDD 带来的改变&lt;/h3&gt;
&lt;p&gt;DDD 不是银弹，但它帮我们做了两件事：&lt;/p&gt;
&lt;ol&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;/ol&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;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 TD
    content[&amp;#34;内容域 (Content)&amp;lt;br/&amp;gt;媒资管理、视频文件、转码任务、DRM 加密&amp;#34;]
    metadata[&amp;#34;元数据域 (Metadata)&amp;lt;br/&amp;gt;影片信息、演员、分类、标签、外部数据源同步&amp;#34;]
    playback[&amp;#34;播放域 (Playback)&amp;lt;br/&amp;gt;播放鉴权、播放地址生成、防盗链、码率适配&amp;#34;]
    user[&amp;#34;用户域 (User)&amp;lt;br/&amp;gt;用户账户、订阅、设备、播放历史&amp;#34;]
    recommendation[&amp;#34;推荐域 (Recommendation)&amp;lt;br/&amp;gt;播放列表、专题、个性化推荐&amp;#34;]
    billing[&amp;#34;计费域 (Billing)&amp;lt;br/&amp;gt;套餐、订单、支付、用量统计&amp;#34;]
    content --- metadata --- playback --- user --- recommendation --- billing&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;flowchart LR
    playback[&amp;#34;播放域&amp;#34;] -- 依赖 --&amp;gt; content[&amp;#34;内容域&amp;#34;]
    playback -- 依赖 --&amp;gt; user[&amp;#34;用户域&amp;#34;]
    recommendation[&amp;#34;推荐域&amp;#34;] -- 依赖 --&amp;gt; metadata[&amp;#34;元数据域&amp;#34;]
    recommendation -- &amp;#34;依赖（获取热门数据）&amp;#34; --&amp;gt; playback
    billing[&amp;#34;计费域&amp;#34;] -- 依赖 --&amp;gt; user
    metadata -- 依赖 --&amp;gt; external[&amp;#34;外部数据源&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;依赖方向尽量保持单向，避免循环依赖。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;四内容域详细设计&#34;&gt;四、内容域详细设计&lt;/h2&gt;
&lt;p&gt;内容域是最复杂的域之一，我们以它为例展开。&lt;/p&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
    root[&amp;#34;MediaAsset（媒资聚合根）&amp;#34;]
    file[&amp;#34;MediaFile（视频文件）&amp;#34;]
    raw[&amp;#34;原始文件&amp;#34;]
    transcoded[&amp;#34;转码后文件（多码率）&amp;#34;]
    drmfile[&amp;#34;DRM 加密文件&amp;#34;]
    job[&amp;#34;TranscodingJob（转码任务）&amp;#34;]
    license[&amp;#34;DrmLicense（DRM 许可证信息）&amp;#34;]
    root --&amp;gt; file
    file --&amp;gt; raw
    file --&amp;gt; transcoded
    file --&amp;gt; drmfile
    root --&amp;gt; job
    root --&amp;gt; license&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;&lt;code&gt;MediaAsset&lt;/code&gt; 是聚合根，外部系统只能通过它访问内部实体。&lt;/p&gt;
&lt;h3 id=&#34;42-关键领域事件&#34;&gt;4.2 关键领域事件&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;&lt;code&gt;MediaAssetUploaded&lt;/code&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;TranscodingCompleted&lt;/code&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;DrmEncryptionCompleted&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;DRM 加密完成&lt;/td&gt;
					&lt;td&gt;播放域&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;MetadataSynced&lt;/code&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;MediaAssetPublished&lt;/code&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;43-状态流转&#34;&gt;4.3 状态流转&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;flowchart TD
    UPLOADED --&amp;gt; PENDING_TRANSCODE --&amp;gt; TRANSCODING --&amp;gt; PENDING_DRM --&amp;gt; DRM_ENCRYPTED --&amp;gt; PENDING_METADATA --&amp;gt; METADATA_READY --&amp;gt; FINAL[&amp;#34;PUBLISHED / REJECTED&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;每个状态变更都通过领域事件触发下一步操作。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;五分层架构&#34;&gt;五、分层架构&lt;/h2&gt;
&lt;p&gt;每个领域内部采用 DDD 经典四层架构：&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
    ui[&amp;#34;用户接口层&amp;lt;br/&amp;gt;REST Controller / DTO&amp;#34;]
    app[&amp;#34;应用层&amp;lt;br/&amp;gt;Application Service / 用例编排&amp;#34;]
    domain[&amp;#34;领域层&amp;lt;br/&amp;gt;Entity / Value Object / Domain Service / Repository Interface / Domain Event&amp;#34;]
    infra[&amp;#34;基础设施层&amp;lt;br/&amp;gt;Repository Impl / MQ Client / External API Client / DB / Cache&amp;#34;]
    ui --- app --- domain --- infra&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&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-java&#34; data-lang=&#34;java&#34;&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;MediaAsset&lt;/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; MediaAssetId id;
&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; String title;
&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; MediaStatus status;
&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; List&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt;MediaFile&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; files;
&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; List&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt;TranscodingJob&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; transcodingJobs;
&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;upload&lt;/span&gt;(MediaFile file) {
&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;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;files&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;add&lt;/span&gt;(file);
&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;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;status&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; MediaStatus.&lt;span style=&#34;color:#a6e22e&#34;&gt;UPLOADED&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        registerEvent(&lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; MediaAssetUploadedEvent(&lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;id&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:#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;completeTranscoding&lt;/span&gt;(TranscodingJob job) {
&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;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;transcodingJobs&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;add&lt;/span&gt;(job);
&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;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;status&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; MediaStatus.&lt;span style=&#34;color:#a6e22e&#34;&gt;TRANSCODING_COMPLETED&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        registerEvent(&lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; TranscodingCompletedEvent(&lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;id&lt;/span&gt;, job.&lt;span style=&#34;color:#a6e22e&#34;&gt;getOutputFiles&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;52-防腐层设计&#34;&gt;5.2 防腐层设计&lt;/h3&gt;
&lt;p&gt;与外部系统（IMDb / TMDB / PP）对接时，使用防腐层（Anti-Corruption Layer）：&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:#66d9ef&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;interface&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;MetadataSourceClient&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    ExternalMetadata &lt;span style=&#34;color:#a6e22e&#34;&gt;fetch&lt;/span&gt;(String externalId);
&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;// IMDb 适配器&lt;/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;ImdbClientAdapter&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;implements&lt;/span&gt; MetadataSourceClient {
&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; ImdbApiClient imdbClient;
&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; ExternalMetadata &lt;span style=&#34;color:#a6e22e&#34;&gt;fetch&lt;/span&gt;(String externalId) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        ImdbResponse response &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; imdbClient.&lt;span style=&#34;color:#a6e22e&#34;&gt;fetch&lt;/span&gt;(externalId);
&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; MetadataConverter.&lt;span style=&#34;color:#a6e22e&#34;&gt;fromImdb&lt;/span&gt;(response);
&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;这样外部 API 的数据结构变化不会直接影响到我们内部的元数据模型。&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;早期我们按技术栈拆：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;upload-service&lt;/li&gt;
&lt;li&gt;transcoding-service&lt;/li&gt;
&lt;li&gt;metadata-service&lt;/li&gt;
&lt;li&gt;playback-service&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;结果导致一个业务操作要跨多个服务，分布式事务复杂。&lt;/p&gt;
&lt;h3 id=&#34;62-第二次拆分按领域边界&#34;&gt;6.2 第二次拆分：按领域边界&lt;/h3&gt;
&lt;p&gt;调整后按 DDD 领域拆分：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;content-service（内容域）&lt;/li&gt;
&lt;li&gt;metadata-service（元数据域）&lt;/li&gt;
&lt;li&gt;playback-service（播放域）&lt;/li&gt;
&lt;li&gt;user-service（用户域）&lt;/li&gt;
&lt;li&gt;recommendation-service（推荐域）&lt;/li&gt;
&lt;li&gt;billing-service（计费域）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;每个服务有清晰的业务边界和聚合根。&lt;/p&gt;
&lt;h3 id=&#34;63-服务间通信&#34;&gt;6.3 服务间通信&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;/td&gt;
					&lt;td&gt;RocketMQ&lt;/td&gt;
					&lt;td&gt;异步、最终一致&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;同步查询&lt;/td&gt;
					&lt;td&gt;HTTP / gRPC&lt;/td&gt;
					&lt;td&gt;强一致查询&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;配置/字典数据&lt;/td&gt;
					&lt;td&gt;Redis 缓存&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;h3 id=&#34;71-事件定义&#34;&gt;7.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-java&#34; data-lang=&#34;java&#34;&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;TranscodingCompletedEvent&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;extends&lt;/span&gt; DomainEvent {
&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; MediaAssetId mediaAssetId;
&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; List&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt;FileUrl&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; outputFiles;
&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; Instant completedAt;
&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;72-事件发布&#34;&gt;7.2 事件发布&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-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;@Transactional&lt;/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;handleTranscodingCallback&lt;/span&gt;(TranscodingJobId jobId, List&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt;FileUrl&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; outputs) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    MediaAsset asset &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; mediaAssetRepository.&lt;span style=&#34;color:#a6e22e&#34;&gt;findByJobId&lt;/span&gt;(jobId);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    asset.&lt;span style=&#34;color:#a6e22e&#34;&gt;completeTranscoding&lt;/span&gt;(&lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; TranscodingJob(jobId, outputs));
&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;    mediaAssetRepository.&lt;span style=&#34;color:#a6e22e&#34;&gt;save&lt;/span&gt;(asset);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    eventPublisher.&lt;span style=&#34;color:#a6e22e&#34;&gt;publish&lt;/span&gt;(asset.&lt;span style=&#34;color:#a6e22e&#34;&gt;getDomainEvents&lt;/span&gt;());
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    asset.&lt;span style=&#34;color:#a6e22e&#34;&gt;clearDomainEvents&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;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-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;PlaybackEventListener&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;@RocketMQMessageListener&lt;/span&gt;(topic &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;content-events&amp;#34;&lt;/span&gt;, selectorExpression &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;TranscodingCompleted&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;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Listener&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;implements&lt;/span&gt; RocketMQListener&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt;TranscodingCompletedEvent&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:#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;onMessage&lt;/span&gt;(TranscodingCompletedEvent event) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            playbackService.&lt;span style=&#34;color:#a6e22e&#34;&gt;preparePlayUrls&lt;/span&gt;(event.&lt;span style=&#34;color:#a6e22e&#34;&gt;getMediaAssetId&lt;/span&gt;(), event.&lt;span style=&#34;color:#a6e22e&#34;&gt;getOutputFiles&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&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;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&gt;com.ftmao.iptv.content
com.ftmao.iptv.metadata
com.ftmao.iptv.playback&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;这样可以在不引入分布式复杂度的前提下，先验证领域划分是否合理。&lt;/p&gt;
&lt;h3 id=&#34;82-从同步调用到事件驱动&#34;&gt;8.2 从同步调用到事件驱动&lt;/h3&gt;
&lt;p&gt;转码完成后，原本播放域通过接口轮询内容域查询状态。后来改为内容域发布 &lt;code&gt;TranscodingCompleted&lt;/code&gt; 事件，播放域订阅。&lt;/p&gt;
&lt;p&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;h3 id=&#34;83-引入-cqrs-处理播放查询&#34;&gt;8.3 引入 CQRS 处理播放查询&lt;/h3&gt;
&lt;p&gt;播放接口对性能要求很高，我们单独做了读模型：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;写模型：MediaAsset 聚合根&lt;/li&gt;
&lt;li&gt;读模型：Redis 中的播放信息缓存&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当内容发布或更新时，通过事件同步更新读模型。&lt;/p&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;早期我们划分了 10 多个领域，导致服务数量爆炸，运维成本剧增。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;教训&lt;/strong&gt;：领域数量要匹配团队规模。小团队 4-6 个领域足够。&lt;/p&gt;
&lt;h3 id=&#34;92-领域事件丢失&#34;&gt;9.2 领域事件丢失&lt;/h3&gt;
&lt;p&gt;消息队列配置不当，出现过领域事件丢失，导致播放域看不到新发布的内容。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;教训&lt;/strong&gt;：关键领域事件必须持久化，支持重放和补偿。&lt;/p&gt;
&lt;h3 id=&#34;93-贫血领域模型&#34;&gt;9.3 贫血领域模型&lt;/h3&gt;
&lt;p&gt;很多「DDD」项目最后变成了 Transaction Script 套壳，实体只有 getter/setter。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;教训&lt;/strong&gt;：要把业务规则和状态变更放在领域对象里，而不是 Service 里。&lt;/p&gt;
&lt;h3 id=&#34;94-过度设计聚合根&#34;&gt;9.4 过度设计聚合根&lt;/h3&gt;
&lt;p&gt;一开始想把所有内容相关的东西都放到 &lt;code&gt;MediaAsset&lt;/code&gt; 聚合根里，结果聚合根过大，事务边界混乱。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;教训&lt;/strong&gt;：聚合根要小而聚焦，跨聚合用领域事件协作。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;十效果与反思&#34;&gt;十、效果与反思&lt;/h2&gt;
&lt;h3 id=&#34;101-量化效果&#34;&gt;10.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;/td&gt;
					&lt;td&gt;40 个&lt;/td&gt;
					&lt;td&gt;20 个&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;线上因耦合导致的 Bug&lt;/td&gt;
					&lt;td&gt;10 次/月&lt;/td&gt;
					&lt;td&gt;2 次/月&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;新成员独立上手时间&lt;/td&gt;
					&lt;td&gt;3 周&lt;/td&gt;
					&lt;td&gt;1 周&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&#34;102-核心反思&#34;&gt;10.2 核心反思&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;DDD 不是先拆分微服务，而是先拆清楚业务边界&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;DDD 需要团队共识&lt;/strong&gt;，不是架构师一个人画几张图就行的。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2 id=&#34;十一总结&#34;&gt;十一、总结&lt;/h2&gt;
&lt;p&gt;巴西 IPTV 项目让我对 DDD 有了真实的体感。它不是教科书上的概念，而是解决复杂业务系统演化问题的工程方法。&lt;/p&gt;
&lt;p&gt;对于视频系统这类业务复杂、流程长、多团队协作的系统，DDD 能带来的最大价值是：&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;p&gt;&lt;em&gt;如果你也在做 DDD 实践，欢迎在评论区交流领域划分和事件驱动的经验。&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>Elasticsearch 从原理到 IPTV 媒资搜索实战：基于 ftmao-meta 项目的完整复盘</title>
      <link>https://ftmao.com/blog/posts/elasticsearch-in-action-iptv/</link>
      <pubDate>Sat, 28 Aug 2021 14:00:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/elasticsearch-in-action-iptv/</guid>
      <description>&lt;h1 id=&#34;elasticsearch-从原理到-iptv-媒资搜索实战基于-ftmao-meta-项目的完整复盘&#34;&gt;Elasticsearch 从原理到 IPTV 媒资搜索实战：基于 ftmao-meta 项目的完整复盘&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;以我们线上运行的 IPTV 媒资元数据服务 &lt;strong&gt;ftmao-meta&lt;/strong&gt; 为样本，讲清楚一个真实的 ES（实际为 AWS 托管 OpenSearch）应用是怎么设计索引、同步数据、支撑搜索和下游订阅的。所有配置、代码、索引设计均来自项目本身，踩过的坑也一并奉上。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;一项目背景这是一个什么系统&#34;&gt;一、项目背景：这是一个什么系统&lt;/h2&gt;
&lt;p&gt;ftmao-meta 是一个面向多地区运营的 IPTV/OTT 视频内容平台的&lt;strong&gt;媒资元数据微服务&lt;/strong&gt;，Spring Boot 3.3 + Java 21，单模块 Gradle 工程，包结构 &lt;code&gt;gtt.ftmao.meta&lt;/code&gt;，分 &lt;code&gt;api / app / bean / core&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;：多语言姓名、热度值（pgcHot / ugcHot / imdbHot）、代表作；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;物料&lt;/strong&gt;：剧照海报（artwork）、预告片（trailer）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;C 端搜索&lt;/strong&gt;：全文检索 + 输入自动补全。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些数据量巨大（千万级文档）、多语言（英语/西语/葡语，服务拉美市场）、多租户（多家运营商公司共用），而且需要被下游多个服务&lt;strong&gt;增量订阅&lt;/strong&gt;。MySQL 是事实源（Source of Truth），ES 承担所有读路径的检索。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;技术选型上的关键事实&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;后端是 &lt;strong&gt;AWS 托管 OpenSearch&lt;/strong&gt;，客户端用官方 &lt;code&gt;opensearch-java 2.8.1&lt;/code&gt;（&lt;code&gt;org.opensearch.client.opensearch.OpenSearchClient&lt;/code&gt;），&lt;strong&gt;没有用 spring-data-elasticsearch，也没有 RestHighLevelClient&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;全文检索与自动补全能力封装在公司内部库 &lt;code&gt;searchengine-api/core/sync&lt;/code&gt; 里，业务代码只调用 &lt;code&gt;SearchApi&lt;/code&gt; / &lt;code&gt;SearchSync&lt;/code&gt; 门面；&lt;/li&gt;
&lt;li&gt;数据同步&lt;strong&gt;不走 MQ&lt;/strong&gt;（Kafka 依赖在启动类里被显式排除），而是&amp;quot;MySQL 任务表 + 定时轮询 + 分布式锁&amp;quot;。&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;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;graph TB
    subgraph 上游[&amp;#34;上游数据来源&amp;#34;]
        BOSS[&amp;#34;运营后台 / 内容录入&amp;#34;]
        PP[&amp;#34;PP 开放平台&amp;lt;br/&amp;gt;（REST 拉取外部媒资）&amp;#34;]
    end

    subgraph META[&amp;#34;ftmao-meta 服务&amp;#34;]
        API[&amp;#34;REST API 层&amp;#34;]
        SVC[&amp;#34;业务 Service&amp;lt;br/&amp;gt;MetaService / ArtworkService ...&amp;#34;]
        JOB[&amp;#34;定时 Job&amp;lt;br/&amp;gt;PpMetaSyncJob 每 15s&amp;#34;]
        LOCK[&amp;#34;分布式锁&amp;lt;br/&amp;gt;AsyncJobExecutorManager&amp;#34;]
    end

    subgraph STORE[&amp;#34;存储层&amp;#34;]
        MYSQL[(&amp;#34;MySQL&amp;lt;br/&amp;gt;事实源 + 同步任务表&amp;#34;)]
        subgraph OS[&amp;#34;AWS OpenSearch&amp;#34;]
            DETAIL[&amp;#34;meta_detail_index&amp;lt;br/&amp;gt;媒资详情（重索引）&amp;#34;]
            PERSON[&amp;#34;meta_person_index&amp;lt;br/&amp;gt;人物详情&amp;#34;]
            ART[&amp;#34;meta_artwork_index&amp;lt;br/&amp;gt;剧照物料&amp;#34;]
            TRAILER[&amp;#34;meta_trailer_index&amp;lt;br/&amp;gt;预告片&amp;#34;]
            SEARCH[&amp;#34;search_engine_index&amp;lt;br/&amp;gt;C端搜索（轻索引）&amp;#34;]
        end
    end

    subgraph DOWN[&amp;#34;下游消费方&amp;#34;]
        PORTAL[&amp;#34;门户/推荐服务&amp;#34;]
        SUB[&amp;#34;增量订阅方&amp;lt;br/&amp;gt;（时间水位拉取变更）&amp;#34;]
    end

    BOSS --&amp;gt; SVC
    PP --&amp;gt; JOB
    SVC --&amp;gt;|&amp;#34;事务内：先写库，再直写 ES&amp;#34;| MYSQL
    SVC --&amp;gt; DETAIL
    SVC --&amp;gt; SEARCH
    JOB --&amp;gt; LOCK --&amp;gt; MYSQL
    JOB --&amp;gt;|&amp;#34;批量灌入&amp;#34;| SEARCH
    API --&amp;gt;|&amp;#34;全文搜索/补全&amp;#34;| SEARCH
    API --&amp;gt;|&amp;#34;mget 回查详情&amp;#34;| DETAIL
    API --&amp;gt; PERSON &amp;amp; ART &amp;amp; TRAILER
    DETAIL --&amp;gt;|&amp;#34;lastModifiedIndicator 水位&amp;#34;| SUB
    SEARCH --&amp;gt; PORTAL&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;hr&gt;
&lt;h2 id=&#34;二先补原理理解后面设计决策的三个支点&#34;&gt;二、先补原理：理解后面设计决策的三个支点&lt;/h2&gt;
&lt;p&gt;在进入项目细节前，用三句话回顾《Elasticsearch in Action》里最重要的原理，后面每个设计决策都会用到它们。&lt;/p&gt;
&lt;h3 id=&#34;21-倒排索引与分词&#34;&gt;2.1 倒排索引与分词&lt;/h3&gt;
&lt;p&gt;ES 搜索快的根本原因是&lt;strong&gt;倒排索引&lt;/strong&gt;：term → 文档列表。而文本进入倒排索引前要经过分析链：&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;O Auto da Compadecida&amp;#34;] --&amp;gt; B[&amp;#34;Character Filter&amp;lt;br/&amp;gt;变音符号映射 á→a&amp;#34;]
    B --&amp;gt; C[&amp;#34;Tokenizer&amp;lt;br/&amp;gt;standard 分词&amp;#34;]
    C --&amp;gt; D[&amp;#34;Token Filter&amp;lt;br/&amp;gt;lowercase + stopword + stemmer&amp;#34;]
    D --&amp;gt; E[&amp;#34;词项 Term&amp;lt;br/&amp;gt;auto / compadecida&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;一个字段建索引时用哪个 analyzer、搜索时用哪个 analyzer，直接决定了&amp;quot;搜不搜得到&amp;quot;。&lt;strong&gt;这就是为什么 mapping 必须在灌数据之前设计好&lt;/strong&gt;——改 analyzer 意味着重建索引。&lt;/p&gt;
&lt;h3 id=&#34;22-近实时nrt与-refresh&#34;&gt;2.2 近实时（NRT）与 refresh&lt;/h3&gt;
&lt;p&gt;文档写入后先进内存缓冲区，默认 1 秒后 refresh 生成 segment 才可被搜索。批量灌数据时调大 &lt;code&gt;refresh_interval&lt;/code&gt;、或显式控制 &lt;code&gt;refresh&lt;/code&gt; 时机，是写入性能优化的第一手段——后面会看到项目里批量写入怎么用它。&lt;/p&gt;
&lt;h3 id=&#34;23-query-与-filter-的区别&#34;&gt;2.3 Query 与 Filter 的区别&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Query&lt;/strong&gt;：算相关性得分 &lt;code&gt;_score&lt;/code&gt;，用于全文匹配排序；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Filter&lt;/strong&gt;：只判断 yes/no，&lt;strong&gt;不算分、结果可被缓存&lt;/strong&gt;，用于精确过滤。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;项目里几乎所有查询都是&amp;quot;filter 套 query&amp;quot;的 bool 结构：关键词进 &lt;code&gt;must&lt;/code&gt;，租户、类型、软删除标记进 &lt;code&gt;filter&lt;/code&gt;。&lt;/p&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;p&gt;项目中&lt;strong&gt;没有任何 &lt;code&gt;@Document&lt;/code&gt; 注解或 mapping JSON 文件&lt;/strong&gt;，索引 DDL 采用类似 Liquibase 的 changelog 形式管理（&lt;code&gt;changelog/elasticsearch/*.yaml&lt;/code&gt;，HTTP method + path + body，由外部部署工具执行），别名字段统一管理：&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;code&gt;meta_detail_index&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;ftmao-meta-detail-1&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;6 主 2 副&lt;/td&gt;
					&lt;td&gt;媒资详情全集&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;meta_person_index&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;ftmao-meta-person-detail&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;6 主 2 副&lt;/td&gt;
					&lt;td&gt;演职人员详情&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;meta_artwork_index&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;ftmao-meta-media-artwork&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;6 主 2 副&lt;/td&gt;
					&lt;td&gt;剧照物料&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;meta_trailer_index&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;ftmao-meta-media-trailer&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;6 主 2 副&lt;/td&gt;
					&lt;td&gt;预告片&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;search_engine_index&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;ftmao-meta-search-engine&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;1 分片&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;C 端搜索/补全&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;别名与文档类的映射集中在一个枚举里（&lt;code&gt;OpenSearchIndexEnum&lt;/code&gt;），业务代码只认别名——这是&amp;quot;换索引不改代码&amp;quot;的基本功，比如 &lt;code&gt;ftmao-meta-detail-1&lt;/code&gt; 后面出现 &lt;code&gt;-2&lt;/code&gt;，只需要 reindex 后切别名。&lt;/p&gt;
&lt;h3 id=&#34;32-详情索引重存储轻检索&#34;&gt;3.2 详情索引：重存储、轻检索&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;meta_detail_index&lt;/code&gt; 的 mapping 有个鲜明特征：&lt;strong&gt;几乎所有字段都是 keyword，标题、剧情简介都是 keyword + nested，不参与分词检索&lt;/strong&gt;，大量 nested 对象（displayTitles / plots / posters / casts）配 &lt;code&gt;dynamic: false&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;为什么？因为详情索引的职责是**&amp;ldquo;按 ID/结构化条件精确取数&amp;rdquo;**，不是全文搜索。keyword 不做分词，聚合、terms 过滤、精确匹配都快；&lt;code&gt;dynamic: false&lt;/code&gt; 防止上游数据乱塞字段把 mapping 撑爆（mapping explosion 是生产事故常客）。&lt;/p&gt;
&lt;h3 id=&#34;33-搜索索引整个项目最精彩的部分&#34;&gt;3.3 搜索索引：整个项目最精彩的部分&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;search_engine_index&lt;/code&gt; 只有 1 个分片（轻量、追求查询 RT），但它的 analysis 配置是整个项目技术含量最高的地方。以主搜索字段 &lt;code&gt;mainTitle&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-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;# 摘自 changelog（简化）&lt;/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;mainTitle&lt;/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;type&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;text&lt;/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;store&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;term_vector&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;with_positions_offsets  &lt;/span&gt; &lt;span style=&#34;color:#75715e&#34;&gt;# 为 fast-vector-highlighter 高亮做准备&lt;/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;analyzer&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;en_es_pt                    &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:#f92672&#34;&gt;fields&lt;/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;cut_word&lt;/span&gt;:    { &lt;span style=&#34;color:#f92672&#34;&gt;analyzer&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;en_es_pt }             &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:#f92672&#34;&gt;all_letters&lt;/span&gt;: { &lt;span style=&#34;color:#f92672&#34;&gt;analyzer&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;N_gram(4-15) }         &lt;/span&gt; &lt;span style=&#34;color:#75715e&#34;&gt;# 长 ngram：前缀/模糊匹配&lt;/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;word_letters:{ analyzer: N_gram(2-10) }         &lt;/span&gt; &lt;span style=&#34;color:#75715e&#34;&gt;# 短 ngram：更激进的模糊匹配&lt;/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;origin&lt;/span&gt;:      { &lt;span style=&#34;color:#f92672&#34;&gt;type: keyword, normalizer&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;lowercase } &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;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;多语分析器 &lt;code&gt;en_es_pt&lt;/code&gt;&lt;/strong&gt;：standard tokenizer + 英语/西语/葡语三套 stopword + stemmer；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;变音符号 char_filter&lt;/strong&gt;：把 &lt;code&gt;áéíóúàèç&lt;/code&gt; 等西葡变音符号映射为 ASCII——拉美用户搜 &lt;code&gt;compadecida&lt;/code&gt; 也能命中 &lt;code&gt;Compadecida&lt;/code&gt;，&lt;strong&gt;对多语言市场这是体验生死线&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ngram tokenizer&lt;/strong&gt; 一批（1–10、4–15 等不同窗口），&lt;code&gt;max_ngram_diff=14&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;completion suggester&lt;/strong&gt;：&lt;code&gt;suggest&lt;/code&gt; 字段配 &lt;code&gt;suggester_trigram&lt;/code&gt; 分析器，带 3 个 &lt;strong&gt;category context&lt;/strong&gt;（&lt;code&gt;markCate&lt;/code&gt; / &lt;code&gt;tenantCate&lt;/code&gt; / &lt;code&gt;comboCate&lt;/code&gt;）——自动补全可以按内容类型和租户隔离，A 运营商的用户不会看到 B 运营商专有的片名提示。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;设计启示&lt;/strong&gt;：一个&amp;quot;能搜&amp;quot;的字段，实际需要四层子字段分工——正常分词管召回、长 ngram 管前缀联想、短 ngram 管纠错、keyword 管精确和排序。这也正是《Elasticsearch in Action》里&amp;quot;多字段映射&amp;quot;思想的生产级演绎。&lt;/p&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;graph LR
    subgraph 写入[&amp;#34;写入侧&amp;#34;]
        D[&amp;#34;MetaDetail 完整文档&amp;lt;br/&amp;gt;（几十 KB，含 nested 海报/演员/多语言）&amp;#34;] --&amp;gt; DETAIL[&amp;#34;详情索引&amp;lt;br/&amp;gt;6分片 重存储&amp;#34;]
        D --&amp;gt;|&amp;#34;裁剪：docId + mainTitle +&amp;lt;br/&amp;gt;suggest + 过滤字段 + 租户&amp;#34;| SEARCH[&amp;#34;搜索索引&amp;lt;br/&amp;gt;1分片 轻量&amp;#34;]
    end
    subgraph 查询[&amp;#34;查询侧&amp;#34;]
        U[&amp;#34;用户搜索 &amp;#39;auto&amp;#39;&amp;#34;] --&amp;gt;|&amp;#34;① matchSearch 命中&amp;lt;br/&amp;gt;返回 docId 列表&amp;#34;| SEARCH
        SEARCH --&amp;gt;|&amp;#34;② 按 docId mget&amp;#34;| DETAIL
        DETAIL --&amp;gt;|&amp;#34;组装完整详情返回&amp;#34;| U
    end&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;搜索索引里&lt;strong&gt;不存详情&lt;/strong&gt;，只有：docId、mainTitle（及 ngram 子字段）、suggest、40+ 个 keyword 过滤字段（&lt;code&gt;ext-field-to-index-map&lt;/code&gt; 里配置，如媒资类型、地区、年份、分级）、租户字段。好处有三个：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;搜索索引小&lt;/strong&gt;，1 个分片 + 内存友好，C 端 QPS 扛得住；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;详情变更不用动搜索索引&lt;/strong&gt;——海报换了只 update 详情索引；搜索索引只在标题/过滤字段变化时更新；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;两类索引可以独立扩缩容、独立重建&lt;/strong&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;代价也很明显：多一次 mget 回查、数据一致性要维护两份。这个项目用下一节的同步机制来兜底。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;四数据同步mysql--es-的三条路径&#34;&gt;四、数据同步：MySQL → ES 的三条路径&lt;/h2&gt;
&lt;p&gt;ES 是&amp;quot;读模型&amp;quot;，MySQL 才是事实源。项目没用 Canal/MQ，而是自己实现了一套&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;flowchart TD
    A[&amp;#34;业务写请求&amp;lt;br/&amp;gt;MetaService.save()&amp;#34;] --&amp;gt; B{&amp;#34;@Transactional&amp;#34;}
    B --&amp;gt; C[&amp;#34;① 写 MySQL 业务表&amp;#34;]
    B --&amp;gt; D[&amp;#34;② 写 MySQL 同步任务表&amp;lt;br/&amp;gt;MixedMetaSyncTask&amp;#34;]
    B --&amp;gt; E[&amp;#34;③ 同事务内直写详情索引&amp;lt;br/&amp;gt;bulkOperationByBatch(refresh=True)&amp;#34;]
    B --&amp;gt; F[&amp;#34;④ saveMetaToSearchEngine&amp;lt;br/&amp;gt;同步搜索索引&amp;#34;]
    E --&amp;gt; G{&amp;#34;ES 写失败?&amp;#34;}
    G --&amp;gt;|&amp;#34;是，事务回滚或留下任务&amp;#34;| H[&amp;#34;任务表残留记录&amp;#34;]
    D --&amp;gt; I[&amp;#34;PpMetaSyncJob&amp;lt;br/&amp;gt;@Scheduled 每 15s&amp;#34;]
    H --&amp;gt; I
    I --&amp;gt; J[&amp;#34;分布式锁&amp;lt;br/&amp;gt;executeWithLock&amp;#34;]
    J --&amp;gt; K[&amp;#34;MetaTaskProcessService&amp;lt;br/&amp;gt;分批 Lists.partition(batchSize)&amp;#34;]
    K --&amp;gt;|&amp;#34;searchSync.save(batch)&amp;#34;| L[&amp;#34;搜索索引&amp;#34;]
    M[&amp;#34;失败重试 Job&amp;lt;br/&amp;gt;每小时&amp;#34;] --&amp;gt; J&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;（主路径）：保证&amp;quot;后台保存完，前台立刻能搜到&amp;quot;。写入时统一刷新 &lt;code&gt;updateTime&lt;/code&gt; 和 &lt;code&gt;lastModifiedIndicator&lt;/code&gt;（格式：&lt;code&gt;时间戳_docId&lt;/code&gt; 的版本游标，下一节详述）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;任务表 + 定时轮询&lt;/strong&gt;（兜底路径）：15 秒一轮，分布式锁防多实例并发，分批灌入；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;失败重试 Job&lt;/strong&gt;（保险）：每小时扫描失败任务重放。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;为什么不纯用 MQ？&lt;/strong&gt; 这个系统的写入量级不大（媒资是低频高价值数据，不是日志），事务内直写的一致性收益远大于引入 MQ 的复杂度。这是&amp;quot;按场景选方案&amp;quot;而不是&amp;quot;按潮流选方案&amp;quot;的好例子。&lt;/p&gt;
&lt;h3 id=&#34;41-批量写入的性能细节&#34;&gt;4.1 批量写入的性能细节&lt;/h3&gt;
&lt;p&gt;工具类 &lt;code&gt;OpenSearchUtil.bulkUpsertDocuments&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-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;// 按配置的 bulkSize（默认 200）分批，避免单个 bulk 过大压垮集群&lt;/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;// 每批显式 refresh(WaitFor)：写完后“等 refresh 完成再返回”，&lt;/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 style=&#34;color:#960050;background-color:#1e0010&#34;&gt;“&lt;/span&gt;写完立即可见&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;”，&lt;/span&gt;同时比 refresh&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:#960050;background-color:#1e0010&#34;&gt;（&lt;/span&gt;每文档刷新&lt;span style=&#34;color:#960050;background-color:#1e0010&#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;对照第 2.2 节的 NRT 原理：&lt;code&gt;refresh=WaitFor&lt;/code&gt; 是吞吐和可见性之间的甜点——批量场景不要无脑 &lt;code&gt;refresh=true&lt;/code&gt;。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;五查询实战从业务代码反推-es-用法&#34;&gt;五、查询实战：从业务代码反推 ES 用法&lt;/h2&gt;
&lt;h3 id=&#34;51-一切查询的起点软删除过滤&#34;&gt;5.1 一切查询的起点：软删除过滤&lt;/h3&gt;
&lt;p&gt;项目里文档不物理删除，而是软删除——所有读路径都要叠一层 &lt;code&gt;isDeleted=0&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-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;// OpenSearchUtil.getUndeletedQuery：所有查询都要过这层包装&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Query undeletedQuery &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; openSearchUtil.&lt;span style=&#34;color:#a6e22e&#34;&gt;getUndeletedQuery&lt;/span&gt;(boolQueryBuilder.&lt;span style=&#34;color:#a6e22e&#34;&gt;build&lt;/span&gt;().&lt;span style=&#34;color:#a6e22e&#34;&gt;_toQuery&lt;/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; 底层就是 bool.&lt;span style=&#34;color:#a6e22e&#34;&gt;filter&lt;/span&gt;(terms(isDeleted, &lt;span style=&#34;color:#f92672&#34;&gt;[&lt;/span&gt;0&lt;span style=&#34;color:#f92672&#34;&gt;]&lt;/span&gt;))&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;，&lt;/span&gt;可能再叠加 status&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;1&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;（&lt;/span&gt;已发布&lt;span style=&#34;color:#960050;background-color:#1e0010&#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;ArtworkService&lt;/code&gt;，按媒资 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-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Query metaIdQuery &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; Query.&lt;span style=&#34;color:#a6e22e&#34;&gt;of&lt;/span&gt;(q &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; q.&lt;span style=&#34;color:#a6e22e&#34;&gt;terms&lt;/span&gt;(t &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; t.&lt;span style=&#34;color:#a6e22e&#34;&gt;field&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;metaId&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;terms&lt;/span&gt;(tv &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; tv.&lt;span style=&#34;color:#a6e22e&#34;&gt;value&lt;/span&gt;(Stream.&lt;span style=&#34;color:#a6e22e&#34;&gt;of&lt;/span&gt;(request.&lt;span style=&#34;color:#a6e22e&#34;&gt;getMetaId&lt;/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;map&lt;/span&gt;(FieldValue::of).&lt;span style=&#34;color:#a6e22e&#34;&gt;toList&lt;/span&gt;()))));
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;boolQueryBuilder.&lt;span style=&#34;color:#a6e22e&#34;&gt;filter&lt;/span&gt;(metaIdQuery);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Query undeletedQuery &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; openSearchUtil.&lt;span style=&#34;color:#a6e22e&#34;&gt;getUndeletedQuery&lt;/span&gt;(boolQueryBuilder.&lt;span style=&#34;color:#a6e22e&#34;&gt;build&lt;/span&gt;().&lt;span style=&#34;color:#a6e22e&#34;&gt;_toQuery&lt;/span&gt;());
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;SortOptions sortOption &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; SortOptions.&lt;span style=&#34;color:#a6e22e&#34;&gt;of&lt;/span&gt;(s &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; s.&lt;span style=&#34;color:#a6e22e&#34;&gt;field&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;-&amp;gt;&lt;/span&gt; f.&lt;span style=&#34;color:#a6e22e&#34;&gt;field&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;pos&amp;#34;&lt;/span&gt;).&lt;span style=&#34;color:#a6e22e&#34;&gt;order&lt;/span&gt;(SortOrder.&lt;span style=&#34;color:#a6e22e&#34;&gt;Asc&lt;/span&gt;)));
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;List&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt;MediaArtwork&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; artworks &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; openSearchUtil.&lt;span style=&#34;color:#a6e22e&#34;&gt;searchDoc&lt;/span&gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    OpenSearchIndexEnum.&lt;span style=&#34;color:#a6e22e&#34;&gt;MEDIA_ARTWORK&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;getIndexName&lt;/span&gt;(),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    undeletedQuery, MediaArtwork.&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;    request.&lt;span style=&#34;color:#a6e22e&#34;&gt;getPageId&lt;/span&gt;(), request.&lt;span style=&#34;color:#a6e22e&#34;&gt;getPageSize&lt;/span&gt;(),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    Collections.&lt;span style=&#34;color:#a6e22e&#34;&gt;singletonList&lt;/span&gt;(sortOption), totalCount);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;这正是第 2.3 节&amp;quot;filter 不算分、可缓存&amp;quot;原理的落地：&lt;strong&gt;结构化条件全部进 filter&lt;/strong&gt;。&lt;/p&gt;
&lt;h3 id=&#34;52-软删除的实现updatebyquery--painless-脚本&#34;&gt;5.2 软删除的实现：updateByQuery + Painless 脚本&lt;/h3&gt;
&lt;p&gt;删除不是 &lt;code&gt;delete&lt;/code&gt;，而是 &lt;code&gt;updateByQuery&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-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;String script &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34; ctx._source.isDeleted = 1;&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:#f92672&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;ctx._source.updateTime = params.currentTime;&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:#f92672&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;ctx._source.lastModifiedIndicator = params.indicatorPrefix + &amp;#39;_&amp;#39; + ctx._id&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;UpdateByQueryRequest request &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; UpdateByQueryRequest.&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;index&lt;/span&gt;(indexName).&lt;span style=&#34;color:#a6e22e&#34;&gt;query&lt;/span&gt;(query)
&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;script&lt;/span&gt;(s &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; s.&lt;span style=&#34;color:#a6e22e&#34;&gt;inline&lt;/span&gt;(i &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; i.&lt;span style=&#34;color:#a6e22e&#34;&gt;source&lt;/span&gt;(finalScript)
&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;params&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;currentTime&amp;#34;&lt;/span&gt;, JsonData.&lt;span style=&#34;color:#a6e22e&#34;&gt;of&lt;/span&gt;(currentTime))
&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;params&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;indicatorPrefix&amp;#34;&lt;/span&gt;, JsonData.&lt;span style=&#34;color:#a6e22e&#34;&gt;of&lt;/span&gt;(String.&lt;span style=&#34;color:#a6e22e&#34;&gt;valueOf&lt;/span&gt;(modifyTime)))))
&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;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;注意脚本里同时更新了 &lt;code&gt;lastModifiedIndicator&lt;/code&gt;——&lt;strong&gt;软删除也是一次&amp;quot;变更&amp;quot;，必须让下游订阅方感知到&lt;/strong&gt;（下游才能同步删）。这个细节是订阅体系能闭环的关键。&lt;/p&gt;
&lt;h3 id=&#34;53-两阶段全文搜索&#34;&gt;5.3 两阶段全文搜索&lt;/h3&gt;
&lt;p&gt;C 端搜索入口 &lt;code&gt;SearchService.searchMedia&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-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; (StringUtils.&lt;span style=&#34;color:#a6e22e&#34;&gt;isNotEmpty&lt;/span&gt;(fuzzyMatchingParams.&lt;span style=&#34;color:#a6e22e&#34;&gt;getKeyword&lt;/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;// 有关键词：内部库全文检索 → 返回 docId + mark（类型）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    resp &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; searchApi.&lt;span style=&#34;color:#a6e22e&#34;&gt;matchSearch&lt;/span&gt;(keyword, marks, pageId, pageSize, tenantIds, params);
&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:#75715e&#34;&gt;// 无关键词：纯结构化筛选（找片页、分类浏览）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    resp &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; searchApi.&lt;span style=&#34;color:#a6e22e&#34;&gt;find&lt;/span&gt;(marks, pageId, pageSize, tenantIds, params);
&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;// 按 mark 分流 person / meta，回查详情索引取完整数据&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;openSearchUtil.&lt;span style=&#34;color:#a6e22e&#34;&gt;batchGetDocuments&lt;/span&gt;(PERSON_DETAIL.&lt;span style=&#34;color:#a6e22e&#34;&gt;getIndexName&lt;/span&gt;(), personDocIds, PersonDetail.&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;openSearchUtil.&lt;span style=&#34;color:#a6e22e&#34;&gt;batchGetDocuments&lt;/span&gt;(META_DETAIL.&lt;span style=&#34;color:#a6e22e&#34;&gt;getIndexName&lt;/span&gt;(), metaDocIds, MetaDetail.&lt;span style=&#34;color:#a6e22e&#34;&gt;class&lt;/span&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;查询条件从哪来？API 层收的是 &lt;strong&gt;SQL 风格的 where 模板&lt;/strong&gt;（&lt;code&gt;MetaQuery.whereTemplate&lt;/code&gt; + &lt;code&gt;parameters&lt;/code&gt;），&lt;code&gt;WhereClauseUtil&lt;/code&gt; 用正则把 &lt;code&gt;in (:x)&lt;/code&gt;、&lt;code&gt;between and&lt;/code&gt;、&lt;code&gt;JSON_CONTAINS_ALL/ANY&lt;/code&gt; 解析成 ES 的 Condition（IN / GREATER_THAN 等），字段名经 &lt;code&gt;FieldNameMappingEnum&lt;/code&gt; 做外部名→索引字段名映射。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;这个设计的好处&lt;/strong&gt;：对前端/上游服务来说，查询语法是稳定的数据库风格，不暴露 ES DSL；ES mapping 改名、换引擎，外部无感。&lt;/p&gt;
&lt;h3 id=&#34;54-自动补全completion-suggester--租户隔离&#34;&gt;5.4 自动补全：completion suggester + 租户隔离&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:#75715e&#34;&gt;// SearchService.suggest&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;List&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt;Prompt&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; prompts &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; searchApi.&lt;span style=&#34;color:#a6e22e&#34;&gt;getPromptList&lt;/span&gt;(keyword, marks, &lt;span style=&#34;color:#66d9ef&#34;&gt;true&lt;/span&gt;, companyId, size, uuid);
&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;// 底层 = 搜索索引 suggest 字段的 completion suggester，&lt;/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;// context 里带 tenantCate=companyId → 补全结果按运营商隔离&lt;/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; 拿到 docId 后再回查详情索引组装标题和海报&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;举例如&amp;quot;用户输入 &lt;code&gt;int&lt;/code&gt;&amp;quot;：&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;sequenceDiagram
    participant U as 用户(运营商A)
    participant S as SearchService
    participant SE as 搜索索引 suggester
    participant D as 详情索引
    U-&amp;gt;&amp;gt;S: suggest(&amp;#34;int&amp;#34;, companyId=A)
    S-&amp;gt;&amp;gt;SE: completion suggest&amp;lt;br/&amp;gt;context: tenantCate=A
    SE--&amp;gt;&amp;gt;S: [&amp;#34;Interestelar&amp;#34;, &amp;#34;Intocáveis&amp;#34;]&amp;lt;br/&amp;gt;+ docId 列表
    S-&amp;gt;&amp;gt;D: mget(docIds)
    D--&amp;gt;&amp;gt;S: 标题 + 海报 URL
    S--&amp;gt;&amp;gt;U: 补全列表（带封面）&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h3 id=&#34;55-一个常被忽略的性能点只取-id-就别加载-_source&#34;&gt;5.5 一个常被忽略的性能点：只取 ID 就别加载 _source&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:#75715e&#34;&gt;// fastGetDocumentById：判断文档存在/取 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:#75715e&#34;&gt;// storedFields(&amp;#34;_id&amp;#34;) + size(1) + trackTotalHits(false)&lt;/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 style=&#34;color:#960050;background-color:#1e0010&#34;&gt;：&lt;/span&gt;类似 MySQL 的 LIMIT 1&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;trackTotalHits(false)&lt;/code&gt;：不精确统计总数（默认只算到 10000），省掉一次全量计数；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;storedFields(&amp;quot;_id&amp;quot;)&lt;/code&gt;：不加载 &lt;code&gt;_source&lt;/code&gt;，网络传输量从几十 KB 降到几十 B。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;列表页&amp;quot;先查 ID 再回查&amp;quot;的场景，这两招能把响应时间砍掉一个量级。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;六增量订阅es-作为变更数据源&#34;&gt;六、增量订阅：ES 作为&amp;quot;变更数据源&amp;quot;&lt;/h2&gt;
&lt;p&gt;下游服务（推荐、缓存、其他中台）需要感知媒资变更。项目没有用 CDC 或 MQ 广播，而是让下游&lt;strong&gt;基于水位游标主动拉取 ES&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;sequenceDiagram
    participant SUB as 下游订阅方
    participant Q as SubqueryService
    participant ES as 详情索引

    Note over SUB: 记录上次水位 W0&amp;lt;br/&amp;gt;（lastModifiedIndicator 字符串）
    SUB-&amp;gt;&amp;gt;Q: querySubQuery(from=W0, size=500)
    Q-&amp;gt;&amp;gt;ES: range(lastModifiedIndicator: gt W0, lt 当前时间-2s)&amp;lt;br/&amp;gt;+ 按 lastModifiedIndicator 升序
    ES--&amp;gt;&amp;gt;Q: 本批变更文档（含软删除的）
    Q--&amp;gt;&amp;gt;SUB: 文档 + 新水位 W1
    SUB-&amp;gt;&amp;gt;SUB: 落库/更新缓存，记录 W1
    Note over Q,ES: 对账：subQueryCount 用 count + sort desc + size(1)&amp;lt;br/&amp;gt;取区间最大水位，生产方/消费方核对数量&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;：&lt;code&gt;lastModifiedIndicator = 时间戳 + &amp;quot;_&amp;quot; + docId&lt;/code&gt;。纯时间戳会在同一毫秒内有多个文档时漏数；拼上 docId 后字典序唯一，range &lt;code&gt;gt&lt;/code&gt; 翻页不会漏、不会重。这是项目&lt;strong&gt;用手写游标替代 &lt;code&gt;search_after&lt;/code&gt;&lt;/strong&gt; 的原因——行为完全可控，且不依赖 ES 的 PIT（point in time）上下文。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;lt 当前时间 - 2s&lt;/code&gt; 的回调偏移&lt;/strong&gt;：规避&amp;quot;刚写入还没 refresh 可见&amp;quot;的窗口（第 2.2 节 NRT 原理的直接应用），宁可晚 2 秒，不可漏数据。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;软删除文档也带着新水位出现&lt;/strong&gt;，下游据此删除本地副本——和第 5.2 节的脚本闭环。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2 id=&#34;七多租户设计docid-里藏着租户&#34;&gt;七、多租户设计：docId 里藏着租户&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-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;// OpenSearchUtil.getDocId：文档 ID = companyId + &amp;#34;#&amp;#34; + metaId&lt;/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 style=&#34;color:#960050;background-color:#1e0010&#34;&gt;：&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;1001#mv_8848123&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;、&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;public#mv_8848123&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;（&lt;/span&gt;公共库&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;）&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;同一部影片，A 运营商和 B 运营商是&lt;strong&gt;两份文档&lt;/strong&gt;（各自的标题、定价、上架状态可能不同），天然物理隔离；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sourceType&lt;/code&gt; 字段区分 PRIVATE（私有）/ PUBLIC（公共）/ MIXED（混合），公共内容用 &lt;code&gt;public#&lt;/code&gt; 前缀，查询时按租户策略合并；&lt;/li&gt;
&lt;li&gt;所有查询 filter 里都带 &lt;code&gt;companyId&lt;/code&gt; / &lt;code&gt;tenant_id&lt;/code&gt;，补全用 context 隔离（第 5.4 节）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;经验&lt;/strong&gt;：多租户搜索系统，把租户编进 docId 比单纯加过滤字段更稳——docId 冲突本身就是一道防串租户的保险。&lt;/p&gt;
&lt;hr&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;Mapping 先行，改 analyzer 要重建索引&lt;/td&gt;
					&lt;td&gt;索引 DDL 用 changelog 版本化管理，上线流程化&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Filter 不算分、可缓存&lt;/td&gt;
					&lt;td&gt;租户/类型/软删除/状态全部进 filter，只有关键词算分&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;多字段映射解决&amp;quot;一个字段多种搜法&amp;quot;&lt;/td&gt;
					&lt;td&gt;mainTitle 四层子字段：分词/长ngram/短ngram/keyword&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;NRT 的 refresh 是性能旋钮&lt;/td&gt;
					&lt;td&gt;批量写入用 &lt;code&gt;refresh=WaitFor&lt;/code&gt;；订阅端留 2s 偏移&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;深度翻页要避免 from+size 深分页&lt;/td&gt;
					&lt;td&gt;手写 &lt;code&gt;lastModifiedIndicator&lt;/code&gt; 游标替代 search_after&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;聚合昂贵，慎用&lt;/td&gt;
					&lt;td&gt;项目几乎零聚合；取最大值用 sort+size(1) 替代 max agg&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;分片数创建时定死，规划要前置&lt;/td&gt;
					&lt;td&gt;详情索引 6 分片按数据增长预估；搜索索引刻意 1 分片&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;如果把这套架构压缩成一张图：&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
    W[&amp;#34;写路径&amp;lt;br/&amp;gt;MySQL 事务&amp;lt;br/&amp;gt;+ 任务表兜底&amp;#34;] --&amp;gt; ES
    subgraph ES[&amp;#34;OpenSearch&amp;#34;]
        S[&amp;#34;搜索索引&amp;lt;br/&amp;gt;小而快：ngram+suggester&amp;lt;br/&amp;gt;+租户 context&amp;#34;]
        D[&amp;#34;详情索引&amp;lt;br/&amp;gt;大而全：keyword+nested&amp;lt;br/&amp;gt;dynamic:false&amp;#34;]
    end
    ES --&amp;gt; R1[&amp;#34;读路径 ①&amp;lt;br/&amp;gt;搜索/补全 → docId&amp;#34;]
    R1 --&amp;gt; R2[&amp;#34;读路径 ②&amp;lt;br/&amp;gt;mget 详情组装&amp;#34;]
    ES --&amp;gt; R3[&amp;#34;订阅路径&amp;lt;br/&amp;gt;水位游标增量拉取&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;索引按&amp;quot;查询模式&amp;quot;而不是&amp;quot;数据表&amp;quot;来设计&lt;/strong&gt;：搜索索引和详情索引分开，一个为召回优化（ngram、suggester、多语分析器），一个为取数优化（keyword、nested、禁动态字段）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;一致性靠机制不靠运气&lt;/strong&gt;：事务内直写是主路径，任务表 + 定时 Job + 失败重试是兜底，水位游标让下游最终一致——三层防护各管一段；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;所有&amp;quot;黑科技&amp;quot;都是基本原理的应用&lt;/strong&gt;：refresh 偏移、filter 缓存、storedFields 取 ID、游标翻页……吃透《Elasticsearch in Action》那套原理，生产方案都是它的排列组合。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;参考资料：Radu Gheorghe, Matthew Lee Hinman, Roy Russo. 《Elasticsearch in Action》. Manning Publications, 2015；ftmao-meta 项目源码（OpenSearchConfig / OpenSearchUtil / SearchService / SubqueryService / changelog/elasticsearch）。&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    
  </channel>
</rss>
