缓存模式 | 你做了什么 | 出现的问题 |
---|---|---|
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} | 读者仍需获取当前版本,协调版本仍是难题 |
.需要 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
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”的升级体验!