一致性问题的本质

缓存与数据库是两份独立状态,任何双写方案都无法天然获得原子性。工程目标通常不是追求绝对同步,而是明确允许的不一致窗口,并保证异常发生后系统能够自动收敛。

优先更新数据库,再删除缓存

对大多数读多写少业务,推荐先提交数据库事务,再删除缓存。删除失败时记录事件并重试。相比先删除缓存,这种顺序更不容易让旧数据库值被并发读请求重新写回缓存。

func UpdateUser(ctx context.Context, user User) error { if err := repository.Update(ctx, user); err != nil { return err }; if err := cache.Delete(ctx, user.ID); err != nil { return retryQueue.Publish(ctx, user.ID) }; return nil }

缓存击穿与回源保护

热点 Key 失效后,大量请求可能同时访问数据库。可以使用 singleflight 合并进程内回源请求,并对跨实例热点增加短期互斥锁。锁必须设置过期时间,未拿到锁的请求应短暂等待后再次读取缓存。

  • TTL 增加随机抖动,避免一批 Key 同时过期。
  • 空值使用较短 TTL,阻挡不存在数据的重复查询。
  • 重试事件包含业务主键和版本,避免旧事件覆盖新状态。
  • 监控缓存命中率、回源 QPS、删除失败数和重试积压。

何时使用 CDC

当写入入口很多,难以保证每个调用方都正确删除缓存时,可以订阅 Binlog 或 CDC 事件统一失效缓存。它提高了治理一致性,但也引入事件延迟、重复消费和顺序处理问题,需要用版本号与幂等消费兜底。