没设 TTL 的键会造出什么
一句话总结
到期时间到来时不会立即发生。接近时删除(延迟删除),后台会抽取样本删除(主动到期)。
为什么需要这个?
Redis实例的内存经常会不断增长,但找不到原因的情况经常发生。一般来说,答案只有一条——混合了没有设置TTL的键。
问题是它看不见。现金代码大部分是SETEX使用,在某个地方SET只在用,SET key val更新时会销毁原有的TTL。第二个特别巧妙——SET删除现有TTL。要保持TTL的话KEEPTTL需要选项。
怎么行动
从到期机制开始,必须准确了解。Redis同时写两种方式。延迟删除在访问键时才会检查到期并删除。主动到期在后台随机抽取带有TTL的键作为样本,删除已到期的键。所以即使到期时间已经过去,但没有人访问的键也会在内存中保留一段时间。DBSIZE比预想的要大的原因就是这个。
达到maxmemory时,排斥政策会启动。选项有八种,但实际工作中重要的只有三个。
noeviction是基本值,如果达到限制值,就会拒绝使用。在使用缓存时,如果设置为这个值,有一天突然所有使用都失败。
allkeys-lru或者allkeys-lfu将所有的密钥视为驱逐对象。如果是纯粹的缓存用途的话,就是这个。作者的建议是allkeys-lfu这是因为比起最近性,频率更适合现金命中率。
volatile-lru只提取设置了TTL的键。在缓存和永久数据混在一起的实例时写入。但是,在这个设置中,如果在缓存键中省略TTL,该键将永远不会被提取,而消耗内存。
现金崩溃的三种方式
TTL设计是为了防止这三个。虽然名字相似,会让人混淆,但原因和对策是 不一样。
| 名字 | 怎么回事 | 怎么阻止 |
|---|---|---|
| Stampede | 人气限制量到期时,数千个请求同时发送到数据库 | 跳跃、锁定、提前重新计算 |
| 穿透(penetration) | 继续查询没有的键 — 缓存中什么都没有,每次DB | 没有也短时间缓存,bloom过滤器 |
| 事态(avalanche) | 很多键一次性过期或快取服务器下线 | 震动、TTL分散、多层快取 |
Stampfed通过锁定来阻止。只有第一个请求才会进入数据库,其余的等待。
def get_or_load(key, loader, ttl=300):
v = r.get(key)
if v is not None:
return v
# SET NX 로 잠금 하나만 얻는다. 얻지 못한 요청은 잠깐 기다렸다 다시 읽는다.
if r.set(f"lock:{key}", "1", nx=True, ex=10):
try:
v = loader()
r.set(key, v, ex=ttl + random.randint(-30, 30)) # 지터
return v
finally:
r.delete(f"lock:{key}")
time.sleep(0.05)
return r.get(key) or loader() # 그래도 없으면 어쩔 수 없이 직접
更好的方法是提前重新计算。TTL结束之前,如果概率性地提前更新的话 没有到期瞬间本身消失。剩余TTL越短,提高更新概率的方式。 (probabilistic early expiration)被广泛使用。
穿透也会缓存“没有”。但是只有在需要很短的时间(30~60秒)后才会真正产生数据时 很快就会反映出来。
卡西和原件不一致的瞬间
如果有写入,就需要无效化,无效化有顺序问题。
❌ 캐시를 먼저 지우고 DB 를 쓴다
1. DEL cache 2. (다른 요청이 옛 값을 읽어 캐시에 다시 넣는다)
3. UPDATE db → 캐시에 옛 값이 영원히 남는다
✅ DB 를 먼저 쓰고 캐시를 지운다 (cache-aside)
1. UPDATE db 2. DEL cache
→ 사이에 읽은 요청은 옛 값을 보지만, 곧 사라진다
即便如此,也很罕见地会出错。要完全阻止的话,请延迟双重删除(使用后删除后, 几百毫秒后再删除一次)或写入更改日志(CDC)订阅。大多数 在服务中,较短的TTL是更便宜的答案——即使不符,也只比TTL不符。
在现场相遇的样子
在TTL中加入震动也很重要。发布后立即填充缓存会使TTL完全相同,一小时后全部同时过期。那一刻就是Stampede。ttl = base + random(-spread, +spread)一句话说来,到期时间会分散在时间轴上。
还有需要感谢。SCAN巡回TTL定期运行计数-1人的键(无到期)的脚本,当新来的代码遗漏了TTL时,会立即纠正。
下次实习要做的事情
挂上TTL确认后,SET再现炸毁这个TTL的陷阱,KEEPTTL阻止,改变驱逐政策,确认实际驱逐了基数,最后制作找到没有TTL的基数的审计脚本。