← 中文资源

简体中文版本

LRU 缓存 TTL 与旧值再验证

把 LRU 淘汰、TTL 过期与 stale-while-revalidate 组合:热键常驻,过期键先回旧值再后台刷新,并发刷新自动合并。

阅读英文原文

示例说明

缓存的三层语义各司其职:max 限制容量按最近使用淘汰冷键;ttl 决定数据新鲜度上限;staleTtl 允许过期但未超期的旧值继续服务,同时触发后台刷新——用户拿到即时响应,数据在几秒内收敛到最新。单飞表合并并发的刷新请求,防止过期瞬间击穿到下游。

技术要点

  • max 与 ttl 分别约束容量与新鲜度
  • staleTtl 窗口内旧值可用并触发刷新
  • inflight 表合并并发刷新请求

代码解读

lru-cache 的 fetchMethod 内建单飞语义:同键并发读取只会有一次真实请求;getRate 直接利用该能力。revalidate 演示手写单飞的通用形态:任务入表后同键请求全部复用同一 Promise,finally 确保无论成败都清理表项,防止失败的请求被永久缓存。

使用建议

stale 窗口要小于下游对旧数据的容忍上限。写操作后应主动 delete 相关键让下次读走回源。监控命中率与刷新失败率,刷新连续失败说明下游故障需要熔断。

代码示例

import { LRUCache } from 'lru-cache';

const cache = new LRUCache({
  max: 500,
  ttl: 60_000,
  staleTtl: 300_000,
  allowStale: true,
  fetchMethod: async (key) => {
    const res = await fetch('https://api.example.com/rates/' + key);
    if (!res.ok) throw new Error('upstream ' + res.status);
    return res.json();
  },
});

const inflight = new Map();

async function getRate(currency) {
  const value = await cache.fetch(currency);
  return value;
}

async function revalidate(key, loader) {
  if (inflight.has(key)) return inflight.get(key);

  const task = loader()
    .then((fresh) => { cache.set(key, fresh); return fresh; })
    .finally(() => { inflight.delete(key); });

  inflight.set(key, task);
  return task;
}

const usd = await getRate('usd');
const again = await getRate('usd');
console.log('rate:', usd.rate, 'second read:', again.rate);