Write-Behind 缓存的核心是写序控制而非缓存本身,需保证顺序、防丢写、可控刷盘;低频用 sync.Map + time.AfterFunc(注意 Stop 旧 timer),高频须用 channel + select 批量/超时机制,并处理重启丢失、重复写覆盖、无事务三大陷阱。

Write-Behind 缓存的核心不是“缓存”,而是“写序控制”
Go 里没有现成的 WriteBehindCache 类型,硬套 Redis 或 Guava 的思路会掉坑里。关键不在缓存数据,而在**保证写入顺序、避免丢写、可控刷盘时机**。你真正要编排的是:写请求进队列 → 合并/去重 → 按 batch 或 timeout 触发 DB 写入 → 失败时可重试或降级。别一上来就搞 map + goroutine,那只是个带毛刺的 buffer。
用 sync.Map + time.AfterFunc 实现最简可行版(适合低频更新)
适用于配置项、用户偏好等写入不密集、允许秒级延迟的场景。重点是避免 goroutine 泛滥和 timer 泄漏。
-
sync.Map存待写 key-value,不是为了高性能读,而是为并发安全地累积变更 - 每次写入都调用
time.AfterFunc启动一个 1s 后执行的刷盘任务,但必须先Stop()上次未触发的 timer —— 否则 100 次写就启 100 个 timer,全在 1s 后扎堆执行 - 刷盘函数里要
Range遍历sync.Map,写完立刻Delete对应 key,不能只清空 map —— 否则新写入会被覆盖 - DB 写失败时别 panic,记日志 + 保留该 key 到下一轮;若连续失败,需加退避(比如指数增长 delay)
var (
pending = sync.Map{}
flushTimer *time.Timer
)
func WriteBehind(key, value string) {
pending.Store(key, value)
if flushTimer != nil {
flushTimer.Stop()
}
flushTimer = time.AfterFunc(time.Second, flushToDB)
}
高吞吐场景必须用 chan + select 控制批量与超时
每秒写几百次以上时,timer 方案延迟抖动大、合并粒度不可控。真实生产要用带缓冲 channel 和 select 超时的 worker 模式。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- 定义
type writeOp struct{ key, value string },往chan writeOp发送写请求 - worker goroutine 用
select等待:要么收满 10 条,要么等 50ms,哪个先到就发 batch —— 这两个参数得压测调优,太小吞吐上不去,太大延迟升高 - DB 批量写用
INSERT ... ON DUPLICATE KEY UPDATE(MySQL)或UPSERT(PostgreSQL),别 for-range 单条 exec,否则并发写反而变慢 - channel 缓冲区大小设为 1024 足够,再大容易 OOM;
len(ch)可监控积压,超阈值要告警或拒绝新写入
最容易被忽略的三个点
不是语法问题,而是架构层面的静默陷阱:
立即学习“go语言免费学习笔记(深入)”;
- 应用重启时,内存里的 pending 写会丢失 —— 必须配合本地磁盘暂存(如 boltdb 或甚至只是
os.WriteFile临时文件),重启后优先回放 - 同一个 key 多次快速写,
sync.Map或 channel 都不会自动去重,得自己用map[string]struct{lastVal string; ts int64}做“最终值”收敛,否则 DB 可能写入过期值 - 事务语义不存在 —— Write-Behind 天然不支持 ACID。如果某次批量写中部分成功部分失败,没法回滚已成功的行,只能靠幂等写(如带 version 字段)或补偿任务兜底

















