概念定义
本地缓存让每个节点都快,让整个集群不一致。一个节点写入的瞬间,其余节点都握着过时的真相。广播失效就是全网修剪这些真相的协同机制。
工作机制
写方在总线上发布失效事件——键、命名空间、版本;所有订阅节点删除匹配条目并记录版本,拒绝重放与迟到消息。Redis 客户端缓存把同一流程形式化为服务端跟踪键失效。
使用场景
整个集群有强新鲜度要求又不想集中缓存时使用。每个失效带版本、监控订阅者延迟、并以 TTL 兜底投递失败时的陈旧上界。
常见误区
发布订阅只保证最终清除——写与投递之间的读取照样拿到陈旧值。经典竞态是“读-回填”:写前开始的查询在失效后完成并把旧值重新塞回缓存——用版本化写或读穿串行化防守。至少一次投递要求幂等处理;无投递监控的 fire-and-forget 会让部分节点静默陈旧数小时。很多场景里,短 TTL 接受陈旧窗口反而是更便宜的选择。
代码示例
// SSG build cache: invalidate prerender output when page hashes change
import { createHash } from 'node:crypto';
import { readFile, writeFile, mkdir, readdir } from 'node:fs/promises';
async function buildIncremental(pagesDir, cacheDir) {
await mkdir(cacheDir, { recursive: true });
const pages = (await readdir(pagesDir)).filter((f) => f.endsWith('.md'));
let cacheMeta = {};
try {
cacheMeta = JSON.parse(await readFile(cacheDir + '/manifest.json', 'utf8'));
} catch {
cacheMeta = {};
}
const manifest = {};
let rebuilt = 0;
for (const file of pages) {
const raw = await readFile(pagesDir + '/' + file, 'utf8');
const hash = createHash('sha256').update(raw).digest('hex').slice(0, 16);
if (cacheMeta[file] === hash) {
manifest[file] = hash; // 静态内容未变,复用缓存产物
continue;
}
const html = '<article>' + raw.replace(/^# (.*)$/m, '<h1>$1</h1>') + '</article>';
await writeFile(cacheDir + '/' + file.replace(/\.md$/, '.html'), html);
manifest[file] = hash;
rebuilt += 1;
}
await writeFile(cacheDir + '/manifest.json', JSON.stringify(manifest, null, 2));
return { total: pages.length, rebuilt, cached: pages.length - rebuilt };
}
const stats = await buildIncremental('content/pages', 'dist/cache');
console.log('build finished:', stats);
import { createClient } from 'redis';
const redis = createClient({ url: process.env.REDIS_URL ?? 'redis://127.0.0.1:6379' });
redis.on('error', (err) => console.error('redis:', err.message));
await redis.connect();
const TTL_SECONDS = 300;
async function cached(key, loader) {
const hit = await redis.get(key);
if (hit !== null) {
await redis.incr('cache:stats:hits');
return JSON.parse(hit);
}
const fresh = await loader();
await redis.set(key, JSON.stringify(fresh), { EX: TTL_SECONDS, NX: true });
await redis.incr('cache:stats:misses');
return fresh;
}
async function invalidate(prefix) {
const pattern = prefix + ':*';
for await (const key of redis.scanIterator({ MATCH: pattern, COUNT: 200 })) {
await redis.del(key);
}
}
const product = await cached('product:42', () => fetchProduct(42));
await invalidate('product');
await redis.quit();