• CasCache插件-为Go开发者解决缓存一致性问题而生
  • 发布于 2天前
  • 78 热度
    0 评论
一.引言
你是否也遇到过这样的场景:明明已经更新了数据,但缓存里却还在返回旧值?更糟糕的是,当你在多个副本(multi-replica)之间做缓存同步时,各种 race condition 和 stale 数据层出不穷,简直让人抓狂。今天,我们要介绍一个专治“缓存 stale 病”的 Go 工具 —— CasCache。它不仅支持 Compare-And-Set(CAS)语义的缓存操作,还通过“generation-guarded writes”和“read-time validation”机制,确保一旦你失效了一个缓存键,就再也看不到它的旧值。听起来是不是很爽?那我们就来深入聊聊这个神器!

二.为什么需要 CasCache?
在正式介绍 CasCache 之前,我们先来看看传统缓存的几个“坑”:
缓存模式 你做了什么 出现的问题
TTL-only 设置 user:42 缓存 5 分钟 写入后读者可能看到最多 5 分钟的旧数据,缩短 TTL 又会增加 DB 压力
Delete then Set 先 DEL user:42 再 SET user:42 两个操作之间存在 race,可能导致旧数据重新写入缓存
Write-through 先写 DB 再更新缓存 并发读取时仍可能读到旧数据,除非完美协调缓存失效
Value with version 值中带版本号 {version, payload} 读者仍需获取当前版本,协调版本仍是难题
这些问题的核心在于:缓存的读写一致性难以保障。而 CasCache 的设计初衷,就是解决这些痛点,提供一个读写安全、一致性高、支持多副本部署的缓存解决方案。

三.CasCache 的核心特性
✅ 无 stale 读
每个缓存键都有一个 generation(代号)。当你调用 Invalidate(key) 时,CasCache 会将该键的 generation 增加。读取时会验证缓存中的 generation 是否与当前一致,不一致则丢弃旧值,视为未命中。
✅ 安全的 CAS 写操作
CasCache 支持带 generation 检查的写入操作。写入前先 snapshot generation,写入时若 generation 已变,写入将失败,防止旧数据写回缓存。
✅ 故障容错
如果 generation 存储不可用,CasCache 会自动降级:读操作视为未命中,写操作跳过,绝不返回 stale 数据。
✅ 可插拔设计
缓存提供者(Providers):支持 Ristretto、BigCache、Redis 等
编解码器(Codecs):支持 JSON、CBOR、Msgpack、Proto 等
Wire format 零拷贝:payload 解码高效,性能优秀
✅ 批量缓存安全机制
批量读取时,每个成员都会进行 generation 校验。只要有一个成员过期,整个批量缓存都会被丢弃,避免部分 stale 数据污染结果。
✅ 多副本安全(Multi-replica)
通过共享 generation 存储(如 RedisGenStore),所有副本都能看到一致的 generation,确保缓存行为在多副本环境中保持一致。

四.CasCache 的使用场景
✅ 推荐使用场景
.数据更新后必须立即生效(如用户资料、商品详情、权限配置、价格、开关等)
.多副本部署,缓存失效协调困难
.希望在异常情况下,缓存行为可预测(要么返回新鲜数据,要么未命中,绝不返回“可能旧”的数据)
❌ 不推荐使用场景
.允许一定 stale(如首页 Feed、监控面板等),TTL 缓存即可满足
.写热点 key(每次读都触发写),缓存意义不大

.需要 dogpile 预防机制(single-flight),CasCache 不内置,需自行添加


五.快速上手 CasCache

1️⃣ 初始化缓存

import (
 "context"
 "time"

 "github.com/unkn0wn-root/cascache"
 "github.com/unkn0wn-root/cascache/codec"
 rp "github.com/unkn0wn-root/cascache/provider/ristretto"
)

type User struct{ ID, Name string }

func newUserCache() (cascache.CAS[User], error) {
 rist, err := rp.New(rp.Config{
  NumCounters: 1_000_000,
  MaxCost:     64 << 20, // 64 MiB
  BufferItems: 64,
  Metrics:     false,
 })
 if err != nil { return nil, err }

 return cascache.New[User](cascache.Options[User]{
  Namespace:  "user",
  Provider:   rist,
  Codec:      codec.JSON[User]{},
  DefaultTTL: 5 * time.Minute,
  BulkTTL:    5 * time.Minute,
  // GenStore: nil -> 本地 generation(单进程)
 })
}
2️⃣ 安全读取单个 key
type UserRepo struct {
 Cache cascache.CAS[User]
 // db handle...
}

func (r *UserRepo) GetByID(ctx context.Context, id string) (User, error) {
 if u, ok, _ := r.Cache.Get(ctx, id); ok {
  return u, nil
 }

 // CAS snapshot BEFORE reading DB
 obs := r.Cache.SnapshotGen(id)

 u, err := r.dbSelectUser(ctx, id) // your DB load
 if err != nil { return User{}, err }

 // Conditionally cache only if generation didn't move
 _ = r.Cache.SetWithGen(ctx, id, u, obs, 0)
 return u, nil
}
3️⃣ 写入时失效 key
func (r *UserRepo) UpdateName(ctx context.Context, id, name string) error {
 if err := r.dbUpdateName(ctx, id, name); err != nil { return err }
 _ = r.Cache.Invalidate(ctx, id) // bump gen + clear single
 return nil
}
4️⃣ 批量读写(可选)
func (r *UserRepo) GetMany(ctx context.Context, ids []string) (map[string]User, error) {
 values, missing, _ := r.Cache.GetBulk(ctx, ids)
 if len(missing) == 0 {
  return values, nil
 }

 // Snapshot *before* DB read
 obs := r.Cache.SnapshotGens(missing)

 // Load missing from DB in one shot
 loaded, err := r.dbSelectUsers(ctx, missing)
 if err != nil { return nil, err }
 // 堆代码 duidaima.com
 // Index for SetBulkWithGens
 items := make(map[string]User, len(loaded))
 for _, u := range loaded { items[u.ID] = u }

 // Conditionally write bulk (or it will seed singles if any gen moved)
 _ = r.Cache.SetBulkWithGens(ctx, items, obs, 0)

 // Merge and return
 for k, v := range items { values[k] = v }
 return values, nil
}
六.多副本部署:使用 RedisGenStore
在多副本部署中,每个副本都应使用共享的 generation 存储。CasCache 提供了 RedisGenStore 实现,确保所有副本看到一致的 generation。
import (
 rgs "github.com/unkn0wn-root/cascache/genstore/redis"
)

func newRedisGenStore(client *redis.Client) cascache.GenStore {
 return rgs.New(client, rgs.Options{
  Namespace: "genstore",
 })
}
初始化缓存时传入:
return cascache.New[User](cascache.Options[User]{
 // ...
 GenStore: newRedisGenStore(redisClient),
})


总结

CasCache 是一个专为解决缓存 stale 问题而设计的 Go 缓存库。它通过 generation-guarded writes 和 read-time validation,确保一旦你失效了一个 key,就不会再读到旧值。结合插件化设计、批量缓存校验和多副本安全机制,CasCache 成为了现代高并发、多副本服务架构中缓存层的不二之选。如果你也在为缓存一致性头疼,不妨试试 CasCache,给你的缓存系统来一次“无 stale”的升级体验!
📌 GitHub 地址:https://github.com/unkn0wn-root/cascache
📌 适用场景:需要强一致缓存、多副本部署、缓存失效频繁的业务系统
用户评论