资讯详情

给工具结果加了 TTL,脏读还是从写侧漏进来

📅 2026/10/28 7:19:19 | 华诺云谱 👁 阅读
给工具结果加了 TTL,脏读还是从写侧漏进来
版权与内容来源声明本文为原创整理。文中涉及官方文档、开源仓库、论文与公开报道的内容均在附表 A 中标注来源引用官方原文保持原样不作改写。文中命令、版本号与界面截图以本文成文时的实测/核验结果为准标注「待验证」的部分请以你本地环境实际输出为判断依据。本文不推荐任何不合规的软件获取方式也不对任何收益结果作承诺。转载请注明出处。先分清两块开销回源次数 与 脏读窗口 不是同一件事把 TTL 从 60 秒拧到 900 秒回源次数会从 1023 掉到 100脏读次数会从 88 涨到 1431——同一份代码、同一条事件流只是那个数字转了两圈。TTL 的性格全在这两个数里它是一个旋钮却管着两笔方向相反的开销。四个词先落地后面所有的账都用这四个定义算不换说法。工具结果缓存把一次工具调用的返回按入参存一份下次同样入参直接把存的那份交给模型。查库存、查订单状态、读汇率表、调一次检索都算。TTLtime to live存活时长这份存档最多能用多久到点作废。回源缓存里没有可用值必须真的再调一次工具。它花掉的是调用次数、token 量和延迟。脏读缓存里有值但那个值已经不是当前真相。它花掉的是「错」。一个旋钮两笔账维度回源次数脏读窗口由什么决定读间隔 Δt 与 TTL 的比值写事件发生到缓存被作废之间的时间计量方式次按读计先按秒量窗口再折算成写后读的次数TTL 变长时下降变长能不能事前算出能误差不到一个百分点只能算上限实际取决于通知送没送到有没有替代手段批量合并、预取、异步刷新写完显式失效、版本号、干脆不缓存这张表里最该盯的是第一行两件事的自变量不一样。回源由「读间隔与 TTL 的比值」决定这个比值能测脏读由「写事件到缓存作废之间的时间」决定而这段时间里有一项——通知延迟——通常没人埋点。两个常见误判都出在这一行上。一个是拿命中率当唯一指标。命中率只回答「省了多少回源」对脏读一个字都没说。我这条事件流里「只 TTL」策略在 900 秒档的命中率是 97.5%4000 次读里回源 100 次同期脏读 1431 次——这个组合完全可以出现在一张很漂亮的图上而且不会触发任何告警。另一个是反过来以为「脏读为 0 就说明缓存设计得对」。「只显式失效」在同一个场景里脏读 279 次、回源只有 110 次把 TTL 压到 30 秒脏读能压到 0回源涨到 1619 次。三个数摆在一起才是完整的取舍任意两个都会骗人。所以这一章要立的判断是手上只有一个 TTL 参数时你必然在某一头上持续妥协在把两笔账分开计量之前你不知道自己妥协的是哪一头。TTL 只能按「读间隔」估回源量估不出脏读回源能事前算误差不到一个百分点脏读算不出来只能给一个上限——因为它的自变量是你没观测到的那部分写入。回源一个除法加一次修正粗估式是这个某 key 平均每 Δt 被读一次、TTL 为 T、写不触发失效时每次读的回源概率 ≈ 1 / ceil(T / Δt)。直觉就是「每 T 秒最多回源一次」TTL 只装得下 ceil(T/Δt) 个读间隔其中第一个间隔不管怎样都要回源。把 T/Δt 拉大1/ceil(T/Δt) 趋近 Δt/T这就是「TTL 翻倍、回源大致减半」的出处——注意它只在 T 远大于 Δt 时成立。拿我这份事件流对一遍20 个 key、平均每秒一次读每个 key 的 Δt ≈ 20 秒30 秒档粗估 50%实测 40.0%60 秒档粗估 33%实测 25.6%300 秒档粗估 6.7%实测 6.5%900 秒档粗估 2.2%实测 2.5%。前两档差得明显方向都是粗估偏高。原因不复杂这个式子把读当成均匀到达而真实的读是随机到达的缓存只在回源那一刻重置计时于是一个 key 有相当一段时间处在「已经过期、但还没人来读」的空转里这段空转不产生回源。按泊松到达推空转占比是1/λ/T 1/λλ 是这个 key 的读速率于是回源概率 ≈ 1 /1 λT代入 λ 0.0530 秒 → 1/2.5 40.0%和实测一模一样60 秒 → 25.0%实测 25.6%300 秒 → 6.25%实测 6.5%900 秒 → 2.17%实测 2.5%。后两档实测略高因为 20 个 key 各有一次冷启动首读摊进 4000 次读里大约半个百分点。两个式子都留着用途不同报预算用 1/ceil(T/Δt)它偏高不会让你低估成本做容量规划用 1/(1λT)它更接近期望不会让你按最坏情况多买一倍。脏读只能给一个上限同一个 key 上写发生在 t缓存里那份值从 t 起就旧了直到三件事之一发生失效通知到达、TTL 到期、下一次回源把它覆盖。取最早的那一个所以单次写的脏窗口 ≤ min通知延迟TTL到下次回源的时间三个候选项里你能设的只有 TTL。「通知延迟」如果没埋点就是未知数「到下次回源的时间」由读间隔决定。这就是脏读估不出来的原因——它取决于一个你没度量的量。而且这三项是取最小不是相加。这句话解释了两种完全不同的处境通知延迟 5 秒、TTL 设了 900 秒脏窗口就是 5 秒。这时候把 TTL 从 900 压到 60脏读一秒都没少只把回源从 151 次抬到 1047 次——纯亏。通知压根没送到延迟视为无穷脏窗口就等于 minTTL到下次回源的时间。TTL 是唯一那一项压 TTL 也是唯一手段。同一个参数在两种处境下的价值差了一个档。所以动 TTL 之前先回答一句你的失效通道丢不丢这个问题不回答TTL 调到多少都是猜。失效通知缺失的代价TTL 只能兜一部分通知丢一次TTL 就把这次脏读截断在 TTL 之内截断不等于消除——100 次写里丢掉的 7 次通知在 900 秒档最终变成 153 次脏读。三种策略同一条事件流 实测环境Python 3.13.12 / macOS / 仅标准库 脚本路径work/exp1027/e01_ttl.py在项目根目录下运行e01_ttl.py —— 固定种子的读/写事件流对比「只 TTL」「只显式失效」「TTL 显式失效」。 可复现固定随机种子不使用 time.time / time.perf_counter输出逐字可复现。 口径回源 缓存没命中必须去取一次真实值脏读 读到的缓存版本落后于当前版本。 importmathimportrandom SEED20261027N_KEYS20N_READS4000MEAN_GAP1.0# 相邻两次读的平均间隔秒WRITE_EVERY40# 每 40 次读之后插入一次写NOTIFY_LOSS0.08# 失效通知丢失的比例TTLS[30,60,300,900]defbuild_trace():reads [(时刻, key)]writes [(时刻, key, 通知是否送达)]。rngrandom.Random(SEED)reads,writes[],[]t0.0foriinrange(N_READS):t-MEAN_GAP*math.log(1.0-rng.random())keyrng.randrange(N_KEYS)reads.append((t,key))if(i1)%WRITE_EVERY0:wkeyrng.randrange(N_KEYS)writes.append((t,wkey,rng.random()NOTIFY_LOSS))returnreads,writesdefrun(reads,writes,ttl,use_ttl,use_invalidate):按一条固定事件流跑一种策略返回 (回源次数, 脏读次数)。version[0]*N_KEYS cache{}# key - [缓存里的版本, 到期时刻]sourcedirty0wi0events[(t,0,k)fort,kinreads][(t,1,k)fort,k,_inwrites]events.sort(keylambdae:(e[0],e[1]))# 同一时刻先处理读再处理写fort,kind,keyinevents:ifkind1:version[key]1ifuse_invalidateandwrites[wi][2]:cache.pop(key,None)wi1continueentrycache.get(key)ifentryisnotNoneand(notuse_ttlortentry[1]):ifentry[0]!version[key]:dirty1else:source1cache[key][version[key],tttlifuse_ttlelsefloat(inf)]returnsource,dirtydefpct(n):return%.1f%%%(n*100.0/N_READS)reads,writesbuild_trace()lostsum(1forwinwritesifnotw[2])print(事件流%d 个 key / %d 次读 / 平均读间隔 %.1f 秒 / %d 次写 / 通知丢失 %d 次%.1f%%%(N_KEYS,N_READS,MEAN_GAP,len(writes),lost,lost*100.0/len(writes)))print()print(%-12s %8s %8s %8s %10s %10s %8s%(TTL(秒),只TTL回源,回源占比,脏读,TTL失效回源,回源占比,脏读))print(-*76)forttlinTTLS:s1,d1run(reads,writes,ttl,True,False)s2,d2run(reads,writes,ttl,True,True)print(%-12d %8d %8s %8d %10d %10s %8d%(ttl,s1,pct(s1),d1,s2,pct(s2),d2))s3,d3run(reads,writes,0,False,True)print(%-12s %8s %8s %8s %10d %10s %8d%(只失效,-,-,-,s3,pct(s3),d3))事件流20 个 key / 4000 次读 / 平均读间隔 1.0 秒 / 100 次写 / 通知丢失 7 次7.0% TTL(秒) 只TTL回源 回源占比 脏读 TTL失效回源 回源占比 脏读 ---------------------------------------------------------------------------- 30 1602 40.0% 42 1619 40.5% 0 60 1023 25.6% 88 1047 26.2% 1 300 261 6.5% 665 307 7.7% 41 900 100 2.5% 1431 151 3.8% 153 只失效 - - - 110 2.8% 279读法有三处和直觉不一样。第一处TTL 设 30 秒时「TTL 失效」的回源是 1619 次比「只 TTL」的 1602 次还高。多开一条失效通道并没有让回源变少因为失效会把本来还能用的条目提前作废。所以「两个都开更省」是错的准确的说法是「两个都开是用更多回源买更少脏读」。第二处只显式失效的回源只有 110 次而它脏读 279 次。110 这个数很规整——20 个 key 各一次冷启动首读加上 90 次写后首读。它是最省调用的策略也是错得最多的策略。如果评估只看成本这一栏这个方案永远会赢。第三处脏读对 TTL 不是线性的。30 秒档 0 次60 秒档 1 次300 秒档 41 次900 秒档 153 次。TTL 从 300 到 900 变成三倍脏读涨到 3.7 倍——因为更长的窗口里还套进了更多写事件每个写都开一个新的脏窗口。这笔账怎么算把 279 次脏读压到 0代价是回源从 110 次涨到 1619 次多出 1509 次调用反过来从 0 次脏读退到 153 次能省下 1468 次调用。判据写成一句能对着数字用的话当「脏读次数 × 单次错的代价」大于「多出来的回源次数 × 单次回源成本」时才值得继续压 TTL否则压 TTL 只是在为一件不会发生的错付钱。单次错的代价怎么定按「这个错会不会被下游当成事实写进别的表」分。会写的代价是清理和回溯成本不会写的只影响一次回答的措辞代价接近零——这类结果根本不该进「需要压脏读」的那一档第 4 章的分档就是按这条线切的。还有一个容易忽略的细节TTL 到期与 key 被删除不是同一时刻。Redis 文档写明2.6 起过期时间的误差是 0 到 1 毫秒——那是「到期时刻」的精度不是「删除动作」的精度删除本身分被动和主动两种客户端访问到已超时的 key 时被动删掉其余靠周期性随机抽样。所以「过期了」在读路径上即时生效读到就判定过期在内存上不是。这两件事在第 6 章查内存时要分开用。三档 TTL 各适合什么按工具结果的可重复性分分档的依据不是「结果多久变一次」而是「这个结果错了之后会不会被下游当成真的用」——错得越隐蔽TTL 就得越短。结果类型典型工具结果TTL失效方式错了会怎样只依赖入参、外部状态不变单位换算、词典、协议常量、静态配置表小时级版本号写进 key不删旧 key答案仍然对只是旧依赖外部状态库存、订单状态、工单进度、账号额度分钟级写完显式失效TTL 只做兜底给出过期的业务事实有副作用下单、发消息、扣款、发券不进缓存改用幂等键重复副作用缓存补救不了三档的分界线第一档只依赖入参。单位换算、词典、协议常量、静态配置表——这类结果的可重复性是「同入参必同结果」。做法是把版本号写进 key例如cfg:v7:usd_cny发版就换 key不删旧 key。TTL 可以给到小时级因为旧 key 只是占内存不会给出错的答案。代价是内存随版本累积第 6 章那条「内存涨」就查这里。第二档依赖外部状态。库存、订单状态、工单进度、账号额度——真相在别人的库里你缓存的那一刻它就已经在变旧。做法是写完显式失效TTL 只做兜底。TTL 取多少按「你能忍的最长脏窗口」取不按「命中率最高」取。这两个目标在这一档天然冲突必须选一个当约束、另一个当优化目标反过来选就会出现「命中率上去了、对账对不上」。第三档有副作用。下单、发消息、扣款、发券——不进缓存。这类操作要防的是重复执行对应手段是幂等键键在最外层入口按业务语义生成、全链路透传缓存替代不了它。把有副作用的调用放进缓存省下的那点回源换来的是一旦缓存给出旧结果、下游又照着执行一次的麻烦。三个写法细节避免踩语义坑写入就带过期别分两条命令。SET k v EX 60一条搞定写成SET k v再加一条EXPIRE k 60两条之间进程挂掉就留下一个永不过期的 key。EX 的单位是秒PX 是毫秒官方文档里 EX、PX、EXAT、PXAT、KEEPTTL 这几个过期选项是互斥的。覆盖值时想清楚要不要保留剩余时间。SET带KEEPTTL表示保留这个 key 原来的存活时间不带它重写一次值原来的存活时间就不再被保留。定时把远程配置同步进缓存这种场景带 KEEPTTL刷新内容、不延长寿命反过来每次写都当作重新开始计时就别带。读时续期要当心它把两个概念绑在一起。GETEX k EX 60在读的同时把过期时间推后 60 秒官方文档特别注明它是写命令、语义上类似 GET 但会改过期时间。这个语义适合「最近有人用过就别过期」的滑动窗口但它同时把「活跃度」和「新鲜度」绑成一件事读得越勤这个 key 越不过期脏窗口也跟着越长。上线前想清楚你要的是哪一种。用一个最小服务把三种策略跑一遍TTL 加到某个档位之后就不再降回源了因为回源有一个下限每次写之后的第一次读必然回源——写多的时候这个下限就是回源的全部。上一段已经把三种策略放在同一条事件流上比过这里只留「TTL 显式失效」这一种线上最常见的那种把变量换成写入频率看加大 TTL 还能不能一直换到收益。两档负载跑出来的分叉 实测环境Python 3.13.12 / macOS / 仅标准库 脚本路径work/exp1027/e01_write.py在项目根目录下运行e01_write.py —— 同一个最小服务两种负载看 TTL 加大能不能一直降回源。 可复现固定随机种子与固定时间网格不使用任何计时函数。 口径读固定每秒一次写按固定间隔发生且失效通知全部送达只留「写入频率」这一个变量。 importrandom SEED20261027N_KEYS20N_READS4000HORIZONfloat(N_READS)# 读事件覆盖的时间长度秒TTLS[30,60,120,240,480,960,1920]LOADS[(A 读多写少,600.0),(B 写多读少,15.0)]defbuild(write_gap):rngrandom.Random(SEED)reads[(float(i),rng.randrange(N_KEYS))foriinrange(N_READS)]writes,t[],write_gapwhiletHORIZON:writes.append((t,rng.randrange(N_KEYS)))twrite_gapreturnreads,writesdefrun(reads,writes,ttl):version[0]*N_KEYS cache{}# key - [缓存里的版本, 到期时刻]source0events[(t,0,k)fort,kinreads][(t,1,k)fort,kinwrites]events.sort(keylambdae:(e[0],e[1]))# 同一时刻先读后写fort,kind,keyinevents:ifkind1:version[key]1cache.pop(key,None)# 失效通知全部送达continueentrycache.get(key)ifentryisnotNoneandtentry[1]:continuesource1cache[key][version[key],tttl]returnsource result,meta{},{}forname,gapinLOADS:reads,writesbuild(gap)result[name][run(reads,writes,ttl)forttlinTTLS]meta[name](gap,len(writes),gap*N_KEYS)print(%s写间隔 %.0f 秒 / 写入 %d 次 / 每 key 写入间隔约 %.0f 秒%(name,gap,len(writes),gap*N_KEYS))print()print(%-8s %9s %9s %10s %9s %9s %10s%(TTL(秒),A 回源,A 占比,B 回源,B 占比,B/写入,A 档降幅))print(-*76)prevNonefori,ttlinenumerate(TTLS):a,bresult[A 读多写少][i],result[B 写多读少][i]drop-ifprevisNoneelse%.0f%%%((prev-a)*100.0/prev)print(%-8d %9d %8.1f%% %10d %8.1f%% %9.2f %10s%(ttl,a,a*100.0/N_READS,b,b*100.0/N_READS,b/meta[B 写多读少][1],drop))prevaprint()a,bresult[A 读多写少],result[B 写多读少]print(A 档 240→480 秒回源降 %d 次B 档同区间降 %d 次B 档 960→1920 秒只降 %d 次。%(a[3]-a[4],b[3]-b[4],b[5]-b[6]))print(B 档每写一次的回源次数TTL 30 秒时 %.2fTTL 1920 秒时 %.2f。%(b[0]/meta[B 写多读少][1],b[6]/meta[B 写多读少][1]))A 读多写少写间隔 600 秒 / 写入 6 次 / 每 key 写入间隔约 12000 秒 B 写多读少写间隔 15 秒 / 写入 266 次 / 每 key 写入间隔约 300 秒 TTL(秒) A 回源 A 占比 B 回源 B 占比 B/写入 A 档降幅 ---------------------------------------------------------------------------- 30 1645 41.1% 1692 42.3% 6.36 - 60 1033 25.8% 1101 27.5% 4.14 37% 120 584 14.6% 693 17.3% 2.61 43% 240 321 8.0% 453 11.3% 1.70 45% 480 171 4.3% 329 8.2% 1.24 47% 960 100 2.5% 280 7.0% 1.05 42% 1920 61 1.5% 276 6.9% 1.04 39% A 档 240→480 秒回源降 150 次B 档同区间降 124 次B 档 960→1920 秒只降 4 次。 B 档每写一次的回源次数TTL 30 秒时 6.36TTL 1920 秒时 1.04。A 档是读多写少写间隔 600 秒回源从 1645 一路降到 61TTL 每翻一倍降 37% 到 47%。注意第一档 30→60 只降 37%不是 50%。原因在第 2 章那个式子里TTL 还小于两倍读间隔时ceil 的量化效应明显加上每个 key 的首读必回源翻倍拿不到一半。B 档是写多读少写间隔 15 秒每个 key 约 300 秒被写一次回源从 1692 降到 276然后停住了。960 秒档 280 次1920 秒档 276 次翻倍只降 4 次而 A 档同一区间降了 39 次。停住的位置有名字TTL 越过「同一个 key 的写入间隔」之后能挤掉的只剩「两次写之间那一段」而每次写之后的第一次读挤不掉。B 档每 key 约 300 秒被写一次TTL 到 960 秒时「每次写的回源次数」已经贴着 1 的底1.05再翻倍只能在 1 附近磨。这就是「TTL 翻倍、回源减半」失效的地方。它只在 T 远大于 Δt、且写入极少时成立一旦写入频率和 TTL 乘起来超过 1回源量主要由写入频率决定跟 TTL 只剩很弱的关系。TTL 该取多少四步估算估 Δt观察时长 ÷ 该 key 在窗口内被读的次数。估 W观察时长 × key 数 ÷ 写总次数得到每个 key 的写入间隔。若 W 远大于 TTL回源按 1/(1λT) 估TTL 翻倍大致减半可以继续加。若 W 已经小于或接近 TTL回源下限就是写次数继续加 TTL 只是在逼近这条底线收益按「每次写 1 次回源」封顶。这两步的分界线就是我这两档负载的分界A 档 W 约 12000 秒全程落在第 3 步B 档 W 约 300 秒TTL 一过 480 秒就掉进第 4 步。负载与 TTL 对照页把「读多写少」和「写多读少」两档的 TTL 取值、回源次数与判断顺序整理成了一页和本章那两个档位是同一套口径。放在资料包里扫码即可获取按现象排障命中率低 / 脏读仍在 / 内存涨分别查哪里四个现象对应四个不同的因用同一个 TTL 去治最多治好一个剩下三个会被治坏。现象先查什么常见原因处置命中率长期低于估算key 的基数不同 key 数 ÷ 读次数高基数入参进了 key时间戳、trace id、随机数、整个用户对象把入参裁到真正影响结果的字段TTL 先别动命中率高但脏读仍在写总次数与失效调用次数的差失效通知丢失、跨进程没接上、批量写漏发让写与失效走同一条路径差值不为零先修通道内存持续上涨key 数量曲线、平均 TTL、只用一次的 key 占比版本号方案加长 TTL旧 key 等自然过期只读类结果单独给短 TTL或设最大条目数失效后短暂回源尖峰失效是否在同一秒成批发生批量导入、定时同步一次作废大量 keyTTL 加随机抖动摊开或先给旧值后台刷新四条处置各自的判据命中率长期低于估算。先算 key 的基数。如果这个比值接近 1每个 key 只被读一次TTL 再长也没用——缓存对只用一次的 key 没有任何价值涨的只有内存。高基数的来源基本都在表里那一列处置是把入参裁到真正影响结果的那几个字段。这一步做完再谈 TTL。命中率高但脏读仍在。别调 TTL去数两个数写总次数、失效调用总次数。两者差不为零就是通知丢了TTL 只能给上限治不了根。跨进程、跨语言、经过消息队列的失效路径最容易丢同步调用里抛异常被吞掉也会丢。修法是把「写」和「失效」放进同一条路径同一个事务、同一套重试而不是在缓存侧加参数。内存持续上涨。查三个量key 数量曲线、平均 TTL、只用一次的 key 占比。最典型的组合是「版本号方案 长 TTL」每次发版换一批 key旧 key 要等 TTL 才消失key 数量随版本线性涨。处置是给只读类结果单独一档短 TTL或者设一个最大条目数。这里还要记住上一章那个细节TTL 到期的 key 不一定立刻从内存里消失主动删除是随机抽样的所以会出现「过期了、内存还没降」那不是泄漏。失效后短暂回源尖峰。查失效是不是成批发生的。批量导入、定时同步会在同一秒作废大量 key接下来的读全部回源。两个处置TTL 加随机抖动把自然过期摊开或者对「旧值还能用」的结果走 stale-while-revalidate先返回旧值、后台刷新。协议层早就把这两件事拆开了HTTP 缓存语义里「新鲜」和「正确」从来是两个概念。RFC 9111 把 fresh 定义成「age响应已经放了多久没有超过 freshness lifetime新鲜期从生成到过期的时长」stale 就是超过了——纯粹的时间判断跟内容对不对无关。同一份规范还规定缓存不得生成 stale 响应除非客户端或源站显式允许比如请求里的 max-stale或者 RFC 5861 那类扩展指令。RFC 5861 定义的 stale-while-revalidate 就是那个「显式允许」响应变 stale 之后还能在指定秒数内继续给旧值同时不阻塞地做一次校验窗口过了还没校验成功就不该再给旧值。文档里的例子是max-age600, stale-while-revalidate30——600 秒新鲜外加 30 秒宽限。这段规范值得抄进设计文档因为它说明了一件事「可以给旧值」是一个需要显式声明、并且要写清窗口长度的决定。你在应用层给工具结果加一个 TTL等于默认发了一张 stale 许可——窗口长度等于 TTL没有后台刷新也没有「窗口过了就必须阻塞」的约束。这两件事本来是一对你只拿了前一半。什么时候该停手三个判断按顺序用。如果「脏读次数 × 单次错的代价」已经大于「多出来的回源次数 × 单次回源成本」停止调 TTL去修失效通道——继续压只会把钱花在一个没被修好的通道上。如果失效通道短期修不好跨团队、跨网络、没有 ack把这类结果移出缓存改成每次带版本号回源。回源贵一点但错不了而且成本是线性的、可预算的。如果只能改一个参数把 TTL 按「你能忍的最长脏窗口」设不按「命中率最高」设。前者是可解释的承诺后者只是一个数字。Agent 上线排障清单本章那张「现象 → 先查什么 → 处置」的表有一份可打印版本另外附了失效通道要埋的几个字段写次数、失效调用次数、通知延迟分位。放在资料包里扫码即可获取附表 A本文引用事实与出处对照表事实出处本文位置过期键有两种删除方式客户端访问时被动删除以及周期性随机抽样删除已过期的键Redis 官方文档《EXPIRE》Redis 发布https://redis.io/docs/latest/commands/expire/第 3 章末、第 6 章「内存持续上涨」过期精度2.4 的误差为零到一秒2.6 起误差为零到一毫秒Redis 官方文档《EXPIRE》「Expire accuracy」小节Redis 发布https://redis.io/docs/latest/commands/expire/第 3 章末SET 的 EX 与 PX 分别以秒、毫秒设置过期时间KEEPTTL 保留原有的存活时间诸过期选项互斥Redis 官方文档《SET》Redis 发布https://redis.io/docs/latest/commands/set/第 4 章「三个写法细节」GETEX 在取值的同时可以设置过期时间属于写命令PERSIST 移除存活时间Redis 官方文档《GETEX》Redis 发布https://redis.io/docs/latest/commands/getex/第 4 章「三个写法细节」fresh 指 age 未超过 freshness lifetimestale 指已超过freshness lifetime 是生成到过期之间的时长IETF RFC 9111《HTTP Caching》第 4.2 节IETF 发布https://www.rfc-editor.org/rfc/rfc9111.html第 2 章、第 6 章「协议层」缓存不得生成 stale 响应除非断开连接或客户端源站显式允许含 RFC 5861 的扩展指令IETF RFC 9111 第 4.2.4 节IETF 发布https://www.rfc-editor.org/rfc/rfc9111.html第 6 章「协议层」stale-while-revalidate 允许在变 stale 后继续返回旧值并后台校验窗口过后不应继续返回IETF RFC 5861《HTTP Cache-Control Extensions for Stale Content》第 3 节IETF 发布https://www.rfc-editor.org/rfc/rfc5861.html第 6 章「协议层」写在最后这篇用到的资料写这篇文章时把相关的官方文档和源码又翻了一遍顺手也整理了几份配套的东西大模型学习路线图从零基础到能自己动手做 Agent按阶段说明每一步该学什么、哪些可以先跳过《LangChain LangGraph MCP 智能体开发实战》视频课7 个模块从私有化部署、EmbeddingRAG 到 MCPAgent 全流程AI 大模型知识库在线可查Agent Skills 从入门到落地、Claude Skills 完全指南等专题按目录浏览即可640 套 AI 大模型行业报告 经典 PDF 书籍看行业落地案例和别人怎么做的时候用得上大模型零基础到精通教学视频跟着敲一遍比只读文档快得多资料是我自己整理的放在下面这个码上扫码即可获取添加时备注「AI」优先通过。资料按「先路线、再动手、最后查漏」的顺序整理好了建议先看学习路线那一份照着它挑一条适合自己当前基础的路径再往下看。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑