概念概述
一致性哈希的缓存分区(Cache Partitioning via Consistent Hashing)指的是:缓存分区用一致性哈希把键分散到多个节点,新增或移除节点时只迁移一小部分键而非全部重排。
从更严格的定义看,Cache partitioning is the distribution of cache entries across multiple nodes by mapping each key to a node, typically using consistent hashing so topology changes relocate a minimal set of keys. 该概念在工程实践中的价值,在于它能帮助团队用统一的语言描述一个具体的质量属性或设计约束,从而在评审与协作中减少歧义。
为什么需要它
It is used to scale a cache beyond one machine’s memory and to keep hot data spread evenly. 它与 Edge Caching at the CDN、Request Deduplication、Cache-Control Directives 等概念经常一起出现,用于解决相近的工程问题。理解它的适用场景,有助于在多种备选方案之间做出有依据的取舍,而不是凭直觉选择技术方案。
常见误解
- modulo hashing distributes keys just as well when nodes change
- hot keys are automatically spread across partitions
需要避免的反模式
- resizing the node count with modulo hashing and invalidating most keys
- routing a single popular key to one node and concentrating load
实践要点
- Modulo hashing maps a key to node by the remainder of a hash, so changing the node count remaps nearly every key and triggers a mass invalidation.
- Consistent hashing places keys and nodes on a ring so adding or removing a node affects only the adjacent range, which keeps most cached data valid across scaling events.
- Partitioning distributes keys but not load, so a single very hot key still lands on one node and needs replication or client-side caching to spread the pressure.
参考来源
https://en.wikipedia.org/wiki/Consistent_hashing
代码示例
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);
// 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);