概念概述
写回缓存(Write-Behind Caching)指的是:写回缓存先更新缓存即确认写入,并异步刷写到后端存储,以持久性换取延迟,适用于计数与遥测等场景。
从更严格的定义看,Write-behind caching, also called write-back, updates the cache immediately and defers persisting to the underlying store by queueing the change and applying it later. 该概念在工程实践中的价值,在于它能帮助团队用统一的语言描述一个具体的质量属性或设计约束,从而在评审与协作中减少歧义。
为什么需要它
It is chosen when write latency matters more than strict durability, such as counters, telemetry, and session touch updates. 它与 Cache Partitioning via Consistent Hashing、Edge Caching at the CDN、Request Deduplication 等概念经常一起出现,用于解决相近的工程问题。理解它的适用场景,有助于在多种备选方案之间做出有依据的取舍,而不是凭直觉选择技术方案。
常见误解
- write-behind guarantees the write reaches the database eventually under all failures
- it is a drop-in replacement for a synchronous commit
需要避免的反模式
- using write-behind for financial records that must survive a crash
- queueing flushes in memory only, without persistence
实践要点
- The client sees low latency because the write returns once the cache is updated, while a background process drains the pending changes to the durable store.
- If the process or node crashes before the flush, the queued changes can be lost, so the deferred queue must itself be durable for data that cannot be dropped.
- Ordering and conflict matter because later writes to the same key must not be overwritten by older queued writes, which requires per-key sequencing.
参考来源
https://learn.microsoft.com/en-us/azure/architecture/patterns/cache-aside
代码示例
interface CacheEntry<T> {
value: T;
expiresAt: number;
}
function Cached<T>(ttlMs: number) {
return function (
target: object,
propertyKey: string,
descriptor: TypedPropertyDescriptor<(...args: never[]) => Promise<T>>,
) {
const original = descriptor.value!;
const store = new Map<string, CacheEntry<T>>();
descriptor.value = async function (this: unknown, ...args: never[]) {
const key = propertyKey + ':' + JSON.stringify(args);
const hit = store.get(key);
const now = Date.now();
if (hit && hit.expiresAt > now) {
(this as { __cacheHits?: number }).__cacheHits ??= 0;
return hit.value;
}
const value = await original.apply(this, args);
store.set(key, { value, expiresAt: now + ttlMs });
return value;
};
return descriptor;
};
}
class ExchangeRates {
private hits = 0;
@Cached(30_000)
async ratesFor(base: string): Promise<Record<string, number>> {
const res = await fetch('https://api.example.com/rates?base=' + base);
return res.json();
}
@Cached(300_000)
async currencies(): Promise<string[]> {
return ['USD', 'EUR', 'JPY'];
}
}
const rates = new ExchangeRates();
const first = await rates.ratesFor('USD');
const second = await rates.ratesFor('USD');
console.log('cached identity:', first === second);