bigywater.tech
Django · 97

缓存失效时间与并发请求

缓存失效瞬间,请求会怎样?get_or_set 能保证只算一次吗?怎么让用户感觉不到回源的等待?这篇用线程并发,把这几个问题都数出来。

20 次
20 个线程并发 get_or_set 同一个不存在的 key,compute 被执行 20 次
205 → 0.3 ms
阻塞式回源 vs stale-while-revalidate 的 p99(计算 200 ms、TTL 1 秒)
lost update
缓存里的读-改-写不是原子的:并发写入会被覆盖

get_or_set 与并发

实测 · 本机执行输出
compute 被执行了 20 次;返回了 1 个不同的值: ['result-2']  缓存里最终留下: result-2

各线程各自 get 不到、各自计算、最后 add 只有第一个成功——所以结果一致,但计算重复了。

用 add 做互斥

实测 · 本机执行输出
compute 被执行了 1 次;所有线程拿到同一个值: True ;总耗时 110 ms

同第 96 篇的击穿防护,锁要带过期时间。

过期瞬间的等待

实测 · 本机执行输出
  阻塞式回源                    请求 537 次;计算 16 次;p50 0.02 ms;p99 205.1 ms;最慢 205 ms
  stale-while-revalidate   请求 635 次;计算 7 次;p50 0.02 ms;p99 0.3 ms;最慢 205 ms

stale-while-revalidate 的做法:缓存里的值带一个「软过期时间」,硬 TTL 比它长得多;读到过期(软)的值时先原样返回,同时让一个线程去刷新。本文的实现是单进程内的简化版(用一个标志位防止重复刷新),多进程要用缓存里的锁。

TTL 与竞态

实测 · 本机执行输出
set 后 1.2s 再 set(2s),再过 1.2s(距第一次 2.4s): a2 (第二次 set 重置了 TTL)
读-改-写竞态:另一个线程在中间写入了 v1-from-other,最终 = v0+writer  ← 覆盖了对方的写入(lost update)

要原子地累加用 cache.incr;要保证「只有一个人在改」需要锁。

分步演示:失效与并发

缓存失效与并发
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 / Python 3.14.7 / Django 6.1.1(默认用 SQLite,其它数据库会在文中注明),用 django.test.Client 或真实的本机服务器进程实测;计时用 time.perf_counter,数字每次运行会有小幅抖动。LocMemCache + 线程;compute 用 sleep 模拟耗时
    • 没有验证:Redis 后端下的 get_or_set、多进程 SWR、cache.incr 在各后端的原子性、Redis 的 SET NX EX 锁的可靠性边界
    京ICP备2026067502号-1京公网安备11010502063487号