跳转到内容

输入功能名称或 API · Enter 打开首项 · Esc 关闭

使用 Redis 共享查询结果

两个服务实例读取同一个用户资料时,可以通过 store/redis 复用查询结果,减少重复访问数据库或 API。本指南按创建存储、创建共享缓存、定义查询、读取结果和失效操作介绍 API 的使用方式,并在文末提供完整程序。

需要 Go 1.26+、当前 soroi 仓库和已安装的 redis-server。核心库只要求 Go 1.22+;store/redis 是独立模块,其 go-redis 依赖要求 Go 1.26+。

以下使用本地源码接入,不假定 Redis 适配器已有可安装的发布版本。在 soroi 仓库根目录执行,创建独立的临时示例模块:

终端窗口
SOROI_SOURCE_DIR="$(pwd)"
SOROI_DEMO_DIR="$(mktemp -d)"
cd "$SOROI_DEMO_DIR"
go mod init example.com/soroi-redis-demo
go mod edit -replace="github.com/Waterkyuu/soroi=$SOROI_SOURCE_DIR"
go mod edit -replace="github.com/Waterkyuu/soroi/store/redis=$SOROI_SOURCE_DIR/store/redis"
go get github.com/Waterkyuu/soroi/store/redis@v0.0.0

两个 replace 分别指向核心库和 Redis 适配器的本地源码。接入已有项目时,在项目的 go.mod 中配置这两个路径并添加依赖,无需重新执行 go mod init。

另外打开一个终端,启动临时 Redis,保持该终端运行:

终端窗口
redis-server --bind 127.0.0.1 --port 16379 --save '' --appendonly no

程序连接 127.0.0.1:16379。Redis 连接必须指向可写主节点,允许 EVAL 以及脚本内的 GET、SET(含 PX)和 DEL;读取也可能初始化缓存元数据。

可运行的最小示例使用两个 Client:各自保存独立的本地缓存,共用一个 Redis 存储。SharedCache 保存共享存储规则,Definition.Shared 把规则接到查询上。创建这两个对象时都不会加载数据。

保持上面的临时 Redis 运行,在仓库根目录执行:

终端窗口
REDIS_ADDR=127.0.0.1:16379 go -C examples/redis run ./shared_cache_minimal

预期输出:

source: loading user 7
A first: Alice, shared hits: 0
B first: Alice, shared hits: 1
A again: Alice, shared hits: 1
  1. A 第一次调用 definition.Fetch(ctx, a),本地缓存和共享存储都没有新鲜结果。soroi 执行数据源回调,将 Alice 编码为 JSON 写入 Redis,并在 A 的本地缓存中保留 Alice。
  2. B 第一次调用 definition.Fetch(ctx, b),本地没有结果,便从 Redis 读取并解码为一个新的 User,再保存在 B 的本地缓存中。数据源回调不会执行。
  3. A 再次调用 definition.Fetch(ctx, a),直接返回本地的新鲜结果。共享命中次数仍为一,因为本次没有访问 Redis。

Definition.Key 标识各个 Client 内的缓存项;SharedCacheOptions.Key 与适配器的 Prefix 一起标识 Redis 中的记录,本例的两个 key 都使用 user:7。A、B 放在同一个进程中是为了展示执行顺序。实际的不同服务进程只需配置相同的 Redis 命名空间、共享 key、兼容的 Format 和结果类型,就能复用记录,无需共享 Go 对象。

示例在读取前只清理专用 soroi-minimal-demo 共享 key,因此重复运行会得到相同输出。StaleTime 决定 Client 何时把结果视为新鲜,shared 的 TTL 限制共享结果的新鲜度和保留时间;这里都设为一分钟。Redis 适配器的失效元数据可以比结果数据保留更久。

下面按 API 的使用顺序说明:创建 Redis 存储、创建共享缓存、定义查询、读取结果,再处理失效。示例的结果类型是 User,表示用户资料:

// User contains the profile fields shared between service instances.
type User struct {
// Name is the user's display name.
Name string
}

User 放在包级。下面的初始化和调用片段依次放在同一个 main 函数中,共用前面创建的变量;它们不是独立程序。包含 import 和全部初始化步骤的可运行代码见文末“完整示例与相关文档”。

使用 redistore.New(remote, options) 创建 Redis 存储适配器。remote 是应用创建的 go-redis 客户端;返回的 store 实现 SharedStore,供后续共享缓存执行 Redis 读写。Prefix 为记录增加应用命名空间,本例使用 soroi-quickstart。

下面先创建最长运行 10 秒的示例 context 和 Redis 连接,再创建适配器。Ping 用于验证示例环境可连接;redistore.New 本身只校验配置,不访问服务器。

ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// The application owns the Redis connection and closes it at shutdown.
remote := redis.NewClient(&redis.Options{
Addr: "127.0.0.1:16379", ContextTimeoutEnabled: true,
DialTimeout: time.Second, ReadTimeout: time.Second,
WriteTimeout: time.Second, MaxRetries: -1,
})
defer func() { _ = remote.Close() }()
if err := remote.Ping(ctx).Err(); err != nil {
panic(err)
}
// Store adapts the borrowed connection to soroi operations in this namespace.
store, err := redistore.New(remote, redistore.Options{Prefix: "soroi-quickstart"})
if err != nil {
panic(err)
}

在业务项目中,Redis 连接和 store 可以在应用启动时创建,供多个查询复用。Redis 连接由应用负责关闭;不要在每次读取时重新建立连接。

使用 soroi.NewSharedCache(store, options) 为一份查询结果创建共享缓存配置。函数从 SharedCacheOptions[V] 推断保存和读取的结果类型;本例使用 SharedCacheOptions[User],其中 User 是上面定义的类型。store 使用前一步创建的 Redis 存储。

本例要共享“租户 42 的用户 7 的资料”:共享 key 为 tenant:42:user:7:v1,按默认 JSON 编码,格式标识为 user.json.v1,从数据源加载完成起最多保留 5 分钟,每次共享存储操作最多等待 200 毫秒。

// Bind one User record's identity and caching rules to the Redis adapter.
// Construction validates configuration without reading or writing Redis.
shared, err := soroi.NewSharedCache(store, soroi.SharedCacheOptions[User]{
// Key identifies this tenant's user profile across service instances.
Key: "tenant:42:user:7:v1",
// Format must match the schema and encoding expected by every reader.
Format: "user.json.v1",
// TTL caps shared freshness and retention from source completion.
TTL: 5 * time.Minute,
// Timeout bounds each store call, not the source Fetcher.
Timeout: 200 * time.Millisecond,
})
if err != nil {
panic(err)
}

返回的 shared 类型为 *soroi.SharedCache[User],保存这份数据的存储和复用规则。创建时只校验并保存配置,不会加载用户资料;下一节通过 Definition.Shared 将它连接到查询。

查询用户 8 时,应创建使用 tenant:42:user:8:v1 等不同 key 的配置,可以继续复用同一个 store。key 中的冒号和各段文字只是应用的命名约定。Format 是读取方匹配的数据结构与编码版本标识,不选择编码器;本例未设置 Codec,所以使用默认 JSON。无效的 key、Format、TTL 或 Timeout 等配置会返回错误,具体约束见“配置说明”。

调用 soroi.QueryDefinition(key, fetcher) 定义本地查询身份和数据源读取函数。加载函数返回 User,因此函数推断结果类型为 User,返回 (soroi.Definition[User], error)。先处理构造错误,再将前一步返回的 shared 赋给定义的 Shared 字段,其类型必须为 *soroi.SharedCache[User]。

下面的 source 是模拟数据库的并发安全变量,calls 统计真实回源次数。此处只定义 Fetcher 回调;创建查询定义时不会执行它。业务接入时将回调替换为数据库或 API 读取,并使用传入的 context。

// A concurrency-safe in-memory source stands in for a database or API.
var source atomic.Value
source.Store(User{Name: "Alice"})
var calls atomic.Int64
// Definition connects the shared rules to a local query identity and source.
definition, err := soroi.QueryDefinition("tenant:42:user:7", func(context.Context) (User, error) {
calls.Add(1)
return source.Load().(User), nil
})
if err != nil {
panic(err)
}
definition.Shared = shared

Definition.Key 标识当前 Client 内的查询,SharedCacheOptions.Key 标识跨实例共享的记录;两者分别配置,不会自动转换。相同本地 key 应使用一致的结果类型和加载规则。

使用 definition.Fetch(ctx, client) 读取结果,它等待并返回 User 或错误。Client 保存本地查询状态;业务中通常在应用内复用同一个 Client,请求传入自己的 context。下面创建 A、B 两个 Client,模拟两个独立服务实例。

StaleTime: time.Minute 表示数据源结果在 1 分钟内可作为新鲜数据复用。A 没有可用记录时执行 Fetcher 并写入 Redis;B 随后可以复用 Redis 记录。最后用 shared.Stats().Hits 查看共享命中次数。

// Both Clients use the same shared configuration and keep separate local results.
a := soroi.NewClient(soroi.Options{StaleTime: time.Minute})
b := soroi.NewClient(soroi.Options{StaleTime: time.Minute})
defer a.Close()
defer b.Close()
first, err := definition.Fetch(ctx, a)
if err != nil {
panic(err)
}
fmt.Printf("A: %s, source calls: %d\n", first.Name, calls.Load())
second, err := definition.Fetch(ctx, b)
if err != nil {
panic(err)
}
fmt.Printf("B: %s, source calls: %d\n", second.Name, calls.Load())
fmt.Printf("shared hits: %d\n", shared.Stats().Hits)

读取顺序为本地新鲜结果、Redis 新鲜记录、Fetcher。B 的共享命中不会再次执行 Fetcher;之后的新鲜读取直接使用各自 Client 的本地结果。文末完整示例包含初始记录重置,便于重复验证输出。

应用退出时先关闭 Client,再关闭 Redis 连接。Client.Close() 不关闭注入的连接,也不删除 Redis 数据。不同进程复用共享记录时,应使用一致的 Redis 数据集、命名空间、共享 key、Format、Codec 和 EnableTags 配置。

先完成数据库或 API 写入,再调用 definition.InvalidateShared。该方法先使 Redis 记录失效,再使指定 Client 中的查询失效;随后读取会重新访问数据源。

将下面片段放在文末完整示例的 main 函数末尾、最后一条输出之后。它复用已有的 source、definition、ctx、a 和 calls,用修改内存数据模拟一次成功的业务写入:

// Simulate a successful database or API write before invalidating the cache.
source.Store(User{Name: "Bob"})
if err := definition.InvalidateShared(ctx, a, soroi.InvalidateOptions{
Refetch: soroi.RefetchNone,
}); err != nil {
panic(err)
}
updated, err := definition.Fetch(ctx, a)
if err != nil {
panic(err)
}
fmt.Printf("A refreshed: %s, source calls: %d\n", updated.Name, calls.Load())

新增输出为:

A refreshed: Bob, source calls: 2

RefetchNone 只标记失效,不自动启动刷新;这里由紧接着的 Fetch 发起读取。省略配置或传入空配置时,已观察且启用的查询可以按默认规则刷新,但 InvalidateShared 本身不等待刷新完成。

此时 B 仍可能返回本地的 Alice,直到新鲜期结束或本地失效。本例没有启用跨实例通知。若需要其他实例及时处理失效,按共享失效通知与标签配置 EnableNotifications 和 WatchShared;关联多个共享 key 时再启用远端标签。

如果只需要让 B 重新读取数据,在上一个片段之后插入以下代码。它复用已有的 b 和查询定义:

if err := b.Invalidate(definition.Key, soroi.InvalidateOptions{
Refetch: soroi.RefetchNone,
}); err != nil {
panic(err)
}
refreshed, err := definition.Fetch(ctx, b)
if err != nil {
panic(err)
}
fmt.Printf("B refreshed: %s, source calls: %d\n", refreshed.Name, calls.Load())

新增输出为:

B refreshed: Bob, source calls: 3

本地失效后新启动的读取会绕过 Redis 中的数据并回源,因此调用次数增加。这种操作不会主动通知其他 Client。

选择失效 API 时,注意作用范围:

API 作用
definition.InvalidateShared(ctx, client, options...) 先使共享记录失效,再使指定 Client 的查询失效;启用通知后其他 watcher 可异步收到事件
shared.Invalidate(ctx) 只执行共享失效,不直接操作任何 Client;启用通知后 watcher 仍可异步处理
client.Invalidate(key) 只使当前 Client 的查询失效;之后新启动的加载绕过共享数据
字段 用途 必填或默认值 使用约束
Definition.Key 当前 Client 内的查询身份 必填 使用受支持的可比较值,包含影响结果的租户、权限和参数;相同 key 使用一致的结果类型和加载配置
Definition.Shared 将查询连接到共享缓存 默认 nil,只使用本地缓存 SharedCache[V] 的 V 与查询结果类型一致
SharedCacheOptions.Key 跨实例共享记录的身份 必填非空字符串 不会从本地 key 自动生成;示例为 tenant:42:user:7:v1
SharedCacheOptions.Format 结果的数据结构和编码版本 必填非空字符串 示例为 user.json.v1;不兼容升级应同时使用新的共享 key
SharedCacheOptions.Codec 对结果编码和解码 默认 JSONCodec[V] JSON 只保留可序列化字段;自定义 Codec[V] 的方法必须支持并发调用

Key 决定“是不是同一份数据”,Format 决定“如何解释这份数据”。两个不兼容的调用方共用一个 key,即使 Format 不同,也会互相替换记录并反复回源。不要把 Format 校验当作数据迁移。

默认 JSON 支持可编码的零值、空值和 nil;缓存的 nil 仍然是有效结果。未导出字段、函数、channel 和指针身份不会被保留。共享 key 不应包含密钥,也不应依赖指针地址或任意值的 fmt.Sprint 输出。

字段 用途 必填或默认值 使用约束
Options.StaleTime 允许复用数据源结果的新鲜时长 默认 0,立即 stale 要复用共享记录,需为 Client 或查询消费者配置正值;示例为 1 分钟
SharedCacheOptions.TTL 共享结果从数据源加载完成起的最长新鲜期和存储时长 必填正时长 示例为 5 分钟;命中不续期
SharedCacheOptions.Timeout 每次共享存储调用的超时 0 使用 1 秒;负值无效 示例为 200 毫秒;不控制 Fetcher 的超时

例如,A 在 12:00:00 从数据源读取 Alice。B 在 12:00:30 命中 Redis,StaleTime 为 1 分钟,则 B 只新鲜到 12:01:00,不会从命中时间重新计时。即使这条 Redis 记录的 TTL 还有剩余,此后也不能将它作为新鲜结果复用。如果 StaleTime 为 10 分钟而共享 TTL 为 5 分钟,新鲜期仍在 12:05:00 截止。

GCTime 和容量淘汰只管理本地缓存,不删除 Redis 记录。MaxAge 从本地安装结果时计时,也可能提前删除本地数据;它不是从数据源读取起计算的数据年龄保证。详见缓存与生命周期。

Timeout 分别约束每次存储调用,不是整个查询的总耗时。请求 context 和查询的超时策略仍控制加载生命周期。为 go-redis 同时配置 context、连接、读写和重试预算;示例启用了 ContextTimeoutEnabled,并通过 MaxRetries: -1 禁用驱动重试。

字段 用途 必填或默认值 使用约束
redistore.Options.Prefix 隔离应用或环境的 Redis 命名空间 空值使用 soroi 不允许 { 或 };生产项目使用自己的命名空间
redistore.Options.EnableNotifications 发布失效事件并允许 watcher 订阅 默认 false 需要额外的 Pub/Sub 权限;所有发布方都应启用
redistore.Options.EnableTags 启用跨 key 的远端标签失效 默认 false 使用独立的数据布局;一个命名空间集中在一个 Cluster slot
SharedCacheOptions.ShareTags 将 Definition.Tags 登记到远端 默认 false 要求存储启用标签能力;仍会遵守同一共享记录上已登记的标签

通知和标签的接入示例、权限、watcher 清理及 Cluster 取舍见共享失效通知与标签。

Fetch 仍以数据源结果为准:Redis 读取失败会回源,编码或共享写入失败不会把成功的数据源读取变成查询错误。数据源本身失败则返回查询错误,并按配置处理重试和本地旧数据。调用方取消或查询超时仍可终止查询。

显式失效不采用静默降级:InvalidateShared 的远端操作失败时返回错误,并保留本地状态。业务写入已成功时,应单独处理缓存同步失败,不能因此盲目重试业务写入。传输超时可能意味着远端操作已经执行;可以重试失效操作,但已提交的远端失效不会因后续本地处理失败而回滚。

现象 检查原因 处理方式
go run 报找不到适配器或要求更新 Go 本地替换路径错误,或 Go 版本不足 检查两个 replace 是否指向当前源码,使用 Go 1.26+
示例的 Ping 报错 Redis 未启动、地址或权限错误 确认服务监听 127.0.0.1:16379;该检查是演示的启动检查,不是 soroi 强制要求
B 再次回源,ReadErrors 增加 Redis 连接、ACL 或操作超时 查看 shared.Stats().LastError,再检查连接选项、可写主节点和脚本权限
Hits 为 0,但没有读错误 StaleTime 为 0、记录已过期,或始终命中本地缓存 配置正新鲜期,用新的 Client 验证共享命中;本地命中不会增加共享 Hits
DecodeErrors 增加并回源 Format 不匹配、记录损坏或 Codec 解码失败 从 LastError 查看失败阶段或记录的实际格式;对齐类型、Format 和 Codec,不兼容结果改用新 key
数据源读取成功,但 EncodeErrors 或 WriteErrors 增加 结果不能编码,或 Redis 写入失败 从 LastError 查看原始 Codec 或存储错误;本地结果仍可用
A 失效后 B 仍读到旧值 B 的本地结果仍新鲜,且未启用 watcher 显式使 B 本地失效,或配置 WatchShared
构造配置时报错 key、Format 为空,TTL 非正数,或缺少标签能力 按配置表检查;无效共享配置返回 ErrInvalidSharedCache,缺少能力返回 ErrSharedCapability

在文末完整示例的 main 函数末尾加入以下片段,读取已有 shared 的统计:

stats := shared.Stats()
fmt.Printf("hits=%d misses=%d read_errors=%d write_errors=%d\n",
stats.Hits, stats.Misses, stats.ReadErrors, stats.WriteErrors)
fmt.Printf("last_error=%v\n", stats.LastError)

完整示例成功运行时的新增输出:

hits=1 misses=1 read_errors=0 write_errors=0
last_error=<nil>

shared.Stats() 只累计这个 SharedCache 对象参与的操作,不是 Redis 服务端或所有进程的全局指标。Bypasses 表示强制刷新或失效后绕过共享数据的尝试;WriteConflicts 表示加载期间发生失效、共享写入被拒绝。Client 自身的统计继续描述本地缓存和加载。

SharedStats.LastError 保留最近一次被计数的共享缓存失败。错误信息包含操作(read、decode、encode、write 或 invalidate)、逻辑共享 Key 和预期 Format。Codec 与封套处理失败还包含具体阶段;格式不匹配会报告实际存储格式,封套时间戳非法或存储返回空版本也有明确原因。soroi 不主动附加缓存载荷;底层错误仍保留自身的诊断详情。错误文本用于诊断,不是稳定的解析接口。

底层存储、Codec 和 JSON 错误保留在包装链中。使用 errors.Is(stats.LastError, context.DeadlineExceeded) 判断存储操作超时,或通过 errors.As 检查驱动、Codec 的具体错误类型;使用这些函数时需导入标准库 errors 包。调用 LastError.Error 前应检查是否为 nil。自定义错误在被保留期间必须保持不可变。

诊断与对应的现有错误计数在同一同步边界内记录。首次计数失败前 LastError 为 nil;成功读取、写入、本地命中和失效不会清除它,后续计数失败会替换它,但不会改变先前返回的快照。仅保留一个错误:如果解码失败,随后回源结果又编码失败,两项计数都会增加,LastError 则描述编码失败。并发失败按统计记录顺序确定先后,不按操作启动时间排序。它是诊断历史,不表示当前健康状态,也不是逐请求错误日志。

记录缺失、过期、新鲜期为零和条件写入冲突不会设置 LastError。数据源失败仍作为查询错误返回,不替换共享缓存诊断。允许继续回源的存储超时会被保留;调用方取消仍按原有规则终止查询。存储调用前被拒绝的操作,以及在错误计数前因查询取消而终止的读取,不产生诊断。显式共享 key 失效失败会保留诊断,同时继续向调用方返回错误。直接存储操作、Client 标签失效和 watcher 失败不属于该 SharedCache 对象的统计范围;watcher 失败仍通过 SharedWatchStats.LastError 查看。

  • 读取顺序为本地新鲜结果、Redis 新鲜记录、数据源。新启动的强制刷新或失效刷新跳过 Redis 数据,但仍读取版本以约束后续写入。如果同 key 已经在加载,刷新仍加入现有加载,不会重启或升级它。刷新失败后的新加载继续绕过共享数据,直到成功加载或本地写入替换失效状态。
  • 共享读取保留原始数据源完成时间。命中不延长 TTL;共享新鲜期结束不等于立即删除本地旧数据。跨机器应合理同步时钟,时间晚于读取方当前时间的记录按未命中处理。
  • 某次加载取得共享版本后,如果记录在写回前被失效,这次加载不能把旧结果重新写回 Redis。被拒绝的写入增加 WriteConflicts;成功的数据源结果仍可返回,但本地结果保持 stale。这个保护不替应用排序数据库事务,也不解决同一版本内多个加载的完成顺序。
  • 同 key 请求合并只发生在单个 Client 内,没有跨实例的分布式锁。多个实例同时未命中时,可能同时回源。共享缓存也不保存查询错误、订阅或持久化查询状态。
  • Set、Update 只修改本地数据,不自动写入 Redis。Get、Snapshot 不读取 Redis;Remove、Clear、GC 和 Client.Close() 不删除共享记录或发布失效事件。
  • 默认 Redis 布局为每个共享 key 保存一条会过期的数据 key 和一条不设置 TTL 的版本元数据 key。即使数据过期,元数据仍占空间,应按查询身份数量规划容量。丢失元数据后,下一次读取会创建新版本并丢弃孤立数据。
  • 适配器使用原子 Lua 脚本,默认布局把同一记录的物理 key 放在同一个 Cluster slot。测试使用单机 Redis,不覆盖真实 Cluster 或 Sentinel 故障切换。旧快照恢复或故障切换丢失已确认写入时,不提供超出 Redis 本身的持久性保证。
  • 存储和 Codec 调用在 Client 锁外执行;Executor 的一个槽覆盖整次加载。共享缓存故障本身不触发查询重试,数据源错误仍遵守查询重试策略。取消是协作式的,驱动和 Fetcher 都需要响应 context。

在安装步骤创建的临时模块目录($SOROI_DEMO_DIR)中,将以下完整程序保存为 main.go。它组合了前面的创建和读取操作,并只重置示例记录,以保证重复运行时输出一致。

package main
import (
"context"
"fmt"
"sync/atomic"
"time"
"github.com/Waterkyuu/soroi"
redistore "github.com/Waterkyuu/soroi/store/redis"
"github.com/redis/go-redis/v9"
)
// User contains the profile fields shared between service instances.
type User struct {
// Name is the user's display name.
Name string
}
// main demonstrates a source read followed by reuse in a separate local Client.
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// The application owns the Redis connection and closes it at shutdown.
remote := redis.NewClient(&redis.Options{
Addr: "127.0.0.1:16379", ContextTimeoutEnabled: true,
DialTimeout: time.Second, ReadTimeout: time.Second,
WriteTimeout: time.Second, MaxRetries: -1,
})
defer func() { _ = remote.Close() }()
if err := remote.Ping(ctx).Err(); err != nil {
panic(err)
}
// Store adapts the borrowed connection to soroi operations in this namespace.
store, err := redistore.New(remote, redistore.Options{Prefix: "soroi-quickstart"})
if err != nil {
panic(err)
}
// Bind one User record's identity and caching rules to the Redis adapter.
// Construction validates configuration without reading or writing Redis.
shared, err := soroi.NewSharedCache(store, soroi.SharedCacheOptions[User]{
// Key identifies this tenant's user profile across service instances.
Key: "tenant:42:user:7:v1",
// Format must match the schema and encoding expected by every reader.
Format: "user.json.v1",
// TTL caps shared freshness and retention from source completion.
TTL: 5 * time.Minute,
// Timeout bounds each store call, not the source Fetcher.
Timeout: 200 * time.Millisecond,
})
if err != nil {
panic(err)
}
// Reset only this demonstration record so repeated runs have the same output.
if err := shared.Invalidate(ctx); err != nil {
panic(err)
}
// A concurrency-safe in-memory source stands in for a database or API.
var source atomic.Value
source.Store(User{Name: "Alice"})
var calls atomic.Int64
// Definition connects the shared rules to a local query identity and source.
definition, err := soroi.QueryDefinition("tenant:42:user:7", func(context.Context) (User, error) {
calls.Add(1)
return source.Load().(User), nil
})
if err != nil {
panic(err)
}
definition.Shared = shared
// Both Clients use the same shared configuration and keep separate local results.
a := soroi.NewClient(soroi.Options{StaleTime: time.Minute})
b := soroi.NewClient(soroi.Options{StaleTime: time.Minute})
defer a.Close()
defer b.Close()
first, err := definition.Fetch(ctx, a)
if err != nil {
panic(err)
}
fmt.Printf("A: %s, source calls: %d\n", first.Name, calls.Load())
second, err := definition.Fetch(ctx, b)
if err != nil {
panic(err)
}
fmt.Printf("B: %s, source calls: %d\n", second.Name, calls.Load())
fmt.Printf("shared hits: %d\n", shared.Stats().Hits)
}

在保存 main.go 的目录运行:

终端窗口
go run .

预期输出:

A: Alice, source calls: 1
B: Alice, source calls: 1
shared hits: 1

A 回源后,B 复用了 Redis 结果,所以数据源调用次数仍为 1。程序只重置 soroi-quickstart 命名空间中的示例记录;业务启动时不应执行这项重置,也不要用该命名空间保存生产数据。示例结束后,在 Redis 终端按 Ctrl+C 停止临时服务。

  • 完整 Redis 示例与运行说明:运行 shared_cache 验证共享读取、显式失效和 Redis 故障回源;运行 shared_invalidation 验证标签及跨实例通知。
  • 共享失效通知与标签:让其他实例及时标记本地结果 stale,并对详情、列表等多个查询统一失效。
  • 缓存与生命周期:配置本地新鲜期、数据保留与资源清理。
  • 失效与刷新:选择自动刷新策略并等待本地刷新完成。
  • SharedStore 契约:实现自定义存储时所需的原子读写、版本校验、context 和数据所有权要求。