<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>缓存 on 追风笔记</title><link>https://88ok.github.io/tags/%E7%BC%93%E5%AD%98/</link><description>Recent content in 缓存 on 追风笔记</description><generator>Hugo</generator><language>zh</language><lastBuildDate>Tue, 25 Aug 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://88ok.github.io/tags/%E7%BC%93%E5%AD%98/index.xml" rel="self" type="application/rss+xml"/><item><title>Redis 缓存设计</title><link>https://88ok.github.io/tech/redis/cache-design/</link><pubDate>Tue, 25 Aug 2026 00:00:00 +0000</pubDate><guid>https://88ok.github.io/tech/redis/cache-design/</guid><description>&lt;p&gt;引入缓存能显著降低数据库压力，但设计不当会引入穿透、击穿、雪崩等经典问题。下面逐一拆解成因与对策。&lt;/p&gt;&#10;&lt;h2 id="三大经典问题"&gt;三大经典问题&#10;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;缓存穿透&lt;/strong&gt;：查询不存在的数据，缓存与数据库都查不到，请求每次都打到 DB。应对：缓存空值（短过期）、布隆过滤器拦截非法 key。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;缓存击穿&lt;/strong&gt;：某个热点 key 过期瞬间，大量并发同时回源 DB。应对：互斥锁（只放一个线程重建）、逻辑过期（异步刷新）、热点 key 不过期。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;缓存雪崩&lt;/strong&gt;：大量 key 同一时刻集中失效，或 Redis 整体不可用，DB 被冲垮。应对：过期时间加随机抖动、多级缓存、Redis 高可用（哨兵/集群）。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="cache-aside-与一致性"&gt;Cache-Aside 与一致性&#10;&lt;/h2&gt;&#10;&lt;p&gt;最常用的是 &lt;strong&gt;Cache-Aside（旁路缓存）&lt;/strong&gt;：&lt;/p&gt;&#10;&lt;div class="td-code td-code--untitled" id="td-code-9a055417-fence-0" data-td-code data-td-code-auto-id&#10; data-td-language="text" data-td-line-count="2"&gt;&#10; &lt;div class="td-code__viewport" id="td-code-9a055417-fence-0-viewport" data-td-code-viewport&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;读: 先读缓存 -&amp;gt; 命中返回; 未命中读 DB -&amp;gt; 写入缓存&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;写: 先更新 DB -&amp;gt; 再删除缓存 (而非更新缓存)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;「先更新 DB 再删缓存」可降低并发下读到旧值的概率；极端不一致可通过延迟双删或订阅 binlog（如 Canal）补偿。注意：不要盲目「更新缓存」，否则并发写可能产生脏数据。&lt;/p&gt;&#10;&lt;h2 id="大-key-与热-key"&gt;大 key 与热 key&#10;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;大 key&lt;/strong&gt;：如包含百万元素的 hash/list，删除或序列化会阻塞主线程。应对：拆分、分批删除（&lt;code&gt;unlink&lt;/code&gt;）、定期清理。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;热 key&lt;/strong&gt;：单个 key 访问极高，单节点成为瓶颈。应对：本地缓存副本、key 打散到多个副本、Redis 集群分片。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="过期与淘汰策略"&gt;过期与淘汰策略&#10;&lt;/h2&gt;&#10;&lt;p&gt;Redis 提供主动 + 惰性两种过期删除，内存不足时按 &lt;code&gt;maxmemory-policy&lt;/code&gt; 淘汰：&lt;/p&gt;</description></item><item><title>缓存设计的那些坑</title><link>https://88ok.github.io/blog/redis-cache-pitfall/</link><pubDate>Wed, 08 Jul 2026 00:00:00 +0000</pubDate><guid>https://88ok.github.io/blog/redis-cache-pitfall/</guid><description>&lt;p&gt;缓存是最容易加的优化，也是最容易埋雷的优化。加的时候只要几行代码，出问题时往往是深夜的数据不一致或者数据库被打穿。&lt;/p&gt;&#10;&lt;h2 id="一致性先更库还是先删缓存"&gt;一致性：先更库还是先删缓存&#10;&lt;/h2&gt;&#10;&lt;p&gt;这个问题的正确答案是&lt;strong&gt;先更新数据库，再删除缓存&lt;/strong&gt;（Cache Aside）。但要理解它为什么仍然不完美。&lt;/p&gt;&#10;&lt;p&gt;先删缓存再更库的问题很直接：删完缓存到更库完成之间，另一个请求读到旧数据并把它写回缓存，之后缓存里就一直是脏数据。&lt;/p&gt;&#10;&lt;p&gt;先更库再删缓存也有极小概率出问题：读请求恰好在缓存失效后读到旧库值，且回写发生在删除动作之后。概率很低，但在高并发下不等于零。&lt;/p&gt;&#10;&lt;div class="td-code td-code--untitled" id="td-code-8d7ae05e-fence-0" data-td-code data-td-code-auto-id&#10; data-td-language="java" data-td-line-count="11"&gt;&#10; &lt;div class="td-code__viewport" id="td-code-8d7ae05e-fence-0-viewport" data-td-code-viewport&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nd"&gt;@Transactional&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;updateLimit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;accountNo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Money&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;newLimit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;limitRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;accountNo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;newLimit&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 1. 先落库&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 2. 事务提交后再删缓存，避免删除后事务回滚导致缓存空档&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;TransactionSynchronizationManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;registerSynchronization&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;TransactionSynchronization&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nd"&gt;@Override&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;afterCommit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;limitKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;accountNo&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;&lt;code&gt;afterCommit&lt;/code&gt; 这一步很关键。在事务内删缓存，如果事务随后回滚，缓存已经被清掉，下一次读会把旧值再加载回来——看起来是对的，但实际上你丢失了一次删除的语义保证。&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;更重要的判断是：这个数据到底能不能容忍短暂不一致。限额、开关、风控规则这类直接影响资金准入的配置，我们干脆不缓存或者只做几秒的本地缓存，宁可多查一次库。&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;h2 id="三大经典故障"&gt;三大经典故障&#10;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;穿透&lt;/strong&gt;：查一个根本不存在的 key，每次都落库。恶意刷不存在的账号就是这么打穿数据库的。做法是缓存空值并设短过期（比如 60 秒），或者前置布隆过滤器。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;击穿&lt;/strong&gt;：热点 key 过期瞬间大量请求同时落库。做法是加互斥锁，只让一个线程去加载，其余等待或返回旧值。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;雪崩&lt;/strong&gt;：大批 key 同时过期，或者 Redis 整体不可用。前者靠给过期时间加随机抖动解决；后者必须有降级预案——限流加返回兜底数据，不能让流量全量透传到数据库。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;div class="td-code td-code--untitled" id="td-code-8d7ae05e-fence-1" data-td-code data-td-code-auto-id&#10; data-td-language="java" data-td-line-count="6"&gt;&#10; &lt;div class="td-code__viewport" id="td-code-8d7ae05e-fence-1-viewport" data-td-code-viewport&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// 过期时间加抖动，避免批量同时失效&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;private&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;ttlWithJitter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;long&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;jitter&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ThreadLocalRandom&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;current&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;nextLong&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toSeconds&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;10&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 上下浮动约 10%&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;plusSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jitter&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&#10;&lt;/div&gt;&#10;&lt;h2 id="热-key-与大-key"&gt;热 key 与大 key&#10;&lt;/h2&gt;&#10;&lt;p&gt;这两个是运维阶段最常见的问题，开发时基本不会注意到。&lt;/p&gt;</description></item></channel></rss>