Skip to content

缓存

缓存用于把读取频繁、计算昂贵、短时间内变化不大的数据暂存在更快的存储中,从而减少重复计算和下游访问开销。

soulsoft_extensions_caching 中,缓存能力有 4 个最重要的特性:

  • IDistributedCache 统一抽象缓存读写接口
  • 当前内置 MemoryDistributedCache 作为默认实现
  • 支持滑动过期、绝对过期、相对过期三种策略
  • 支持通过依赖注入注册缓存,实现按接口使用、按实现切换

先建立一个整体认识

这一页最重要的不是“内存缓存怎么用”,而是“业务代码应该依赖什么”。

Spire 在缓存这一层优先提供的是统一接口 IDistributedCache。 当前默认实现是内存缓存;未来会继续支持 Redis 等实现来完成同一套接口。

这意味着:

  • 业务代码只依赖 IDistributedCache
  • 当前可以先用内置内存实现快速落地
  • 将来切到 Redis 之类的实现时,通常只需要改注册方式,不需要重写业务缓存调用

这正是这套设计的价值所在:减少开发者学习、使用和适配不同缓存产品的压力,并尽量实现零成本切换缓存实现。

最小示例

如果你只是想先把缓存跑通,可以从这个例子开始:

cangjie
import soulsoft_extensions_caching.*
import soulsoft_extensions_options.*

main(): Int64 {
    // 创建内存缓存实现,并传入缓存系统选项。
    let cache = MemoryDistributedCache(Options.create(DistributedCacheOptions()))

    // 写入一个字符串缓存项。
    cache.setString("access_token", "token-value")

    println(cache.getString("access_token") ?? "None")

    // 删除缓存后再次读取会拿到 None。
    cache.remove("access_token")
    println(cache.getString("access_token") ?? "None")
    return 0
}

这个例子里有三个关键点:

  • 当前直接创建的是 MemoryDistributedCache
  • 常见字符串场景可以直接用 setString() / getString()
  • 删除后再次读取会返回 None

核心类型

类型作用
IDistributedCache缓存统一接口,定义 get / set / refresh / remove
MemoryDistributedCache当前内置的默认实现
DistributedCacheEntryOptions单个缓存项的过期策略
DistributedCacheOptions缓存系统级选项,例如过期扫描频率

最常见的使用流程只有 4 步:

  1. 获取一个 IDistributedCache 实例
  2. 调用 set()setString() 写入数据
  3. 调用 get()getString() 读取数据
  4. 需要时调用 refresh()remove()

基本读写

存取字符串

cangjie
import soulsoft_extensions_caching.*
import soulsoft_extensions_options.*

// 创建一个可直接使用的内存缓存实例。
let cache = MemoryDistributedCache(Options.create(DistributedCacheOptions()))

cache.setString("user.profile", "alice")
let value = cache.getString("user.profile")
println(value ?? "None")

存取字节数组

cangjie
import soulsoft_extensions_caching.*
import soulsoft_extensions_options.*

let cache = MemoryDistributedCache(Options.create(DistributedCacheOptions()))

// 以字节数组形式写入缓存。
cache.set("binary.data", "hello".toArray())

if (let Some(bytes) <- cache.get("binary.data")) {
    // 读取到字节后再转回字符串。
    println(String.fromUtf8(bytes))
}

刷新与删除

refresh() 的作用是刷新滑动过期计时器;remove() 用于主动删除缓存项。

cangjie
import soulsoft_extensions_caching.*
import soulsoft_extensions_options.*

let cache = MemoryDistributedCache(Options.create(DistributedCacheOptions()))

cache.setString("session.token", "abc123")
// refresh() 会刷新滑动过期计时。
cache.refresh("session.token")
// remove() 用于主动删除缓存项。
cache.remove("session.token")

为什么要面向 IDistributedCache 编程

如果你直接在业务代码里到处依赖 MemoryDistributedCache,那未来切 Redis、切多节点缓存、切其他外部缓存产品时,替换成本会快速变高。

更推荐的方式是:

  • 应用层、领域层、基础设施适配层统一依赖 IDistributedCache
  • 把“到底用内存、Redis 还是其他实现”的决定放到注册阶段

这样做有三个直接收益:

  • 业务代码更稳定,不和某个具体缓存产品绑死
  • 开发者只需要学习一套缓存调用方式
  • 后续替换实现时,适配压力主要集中在基础设施注册层,而不是扩散到业务层

过期策略

缓存项支持三种过期条件。 它们可以单独使用,也可以组合使用;只要任意一个条件满足,就会过期。

策略字段适合场景
滑动过期slidingExpiration会话、在线状态、短期热点数据
绝对过期absoluteExpiration某个固定时间点必须失效的数据
相对过期absoluteExpirationRelativeToNow从写入时刻开始计时的数据

相对过期

cangjie
import std.time.*
import soulsoft_extensions_caching.*
import soulsoft_extensions_options.*

let cache = MemoryDistributedCache(Options.create(DistributedCacheOptions()))

// 写入一个 5 分钟后过期的验证码。
cache.setString(
    "sms.code",
    "9527",
    DistributedCacheEntryOptions(
        absoluteExpirationRelativeToNow: Some(Duration.minute * 5)
    )
)

这类写法适合验证码、临时 token、短时结果缓存。

绝对过期

cangjie
import std.time.*
import soulsoft_extensions_caching.*
import soulsoft_extensions_options.*

let cache = MemoryDistributedCache(Options.create(DistributedCacheOptions()))

// 写入一个在固定时间点失效的缓存项。
cache.setString(
    "daily.report",
    "ready",
    DistributedCacheEntryOptions(
        absoluteExpiration: Some(DateTime.nowUTC() + Duration.hour)
    )
)

这类写法适合“到某个时间点必须失效”的数据。

滑动过期

cangjie
import std.time.*
import soulsoft_extensions_caching.*
import soulsoft_extensions_options.*

let cache = MemoryDistributedCache(Options.create(DistributedCacheOptions()))

// 只要持续访问,就会不断延长过期时间。
cache.setString(
    "session.user",
    "alice",
    DistributedCacheEntryOptions(
        slidingExpiration: Some(Duration.minute * 30)
    )
)

只要在窗口期内访问,滑动过期时间就会被续延。

组合策略

cangjie
import std.time.*
import soulsoft_extensions_caching.*
import soulsoft_extensions_options.*

let cache = MemoryDistributedCache(Options.create(DistributedCacheOptions()))

// 同时设置滑动过期和最长存活时间。
cache.setString(
    "login.challenge",
    "value",
    DistributedCacheEntryOptions(
        slidingExpiration: Some(Duration.minute * 5),
        absoluteExpirationRelativeToNow: Some(Duration.minute * 10)
    )
)

这个例子表示:

  • 5 分钟内不访问会失效
  • 即使一直访问,10 分钟后也一定会失效

过期清理机制

当前内置的 MemoryDistributedCache 不是简单的“只在读取时才发现过期”。 它同时有两层清理机制:

  • 访问某个 key 时,如果发现已经过期,会立即移除
  • 缓存里长期没有被访问到的过期项,会通过后台扫描异步清理

扫描频率由 DistributedCacheOptions.expirationScanFrequency 控制,默认值是 Duration.minute

cangjie
import std.time.*
import soulsoft_extensions_caching.*

// 调整后台过期扫描频率。
let options = DistributedCacheOptions()
options.expirationScanFrequency = Duration.minute * 30

一般来说:

  • 对实时性要求高,可以把扫描频率调小
  • 更关注后台开销,可以把扫描频率调大

与依赖注入集成

在实际项目里,更推荐通过 DI 使用缓存,而不是在业务代码里手动 new MemoryDistributedCache(...)

cangjie
import soulsoft_extensions_caching.*
import soulsoft_extensions_injection.*
import std.time.*

let services = ServiceCollection()

// 通过 DI 注册内存缓存,并调整扫描频率。
services.addDistributedMemoryCache { options =>
    options.expirationScanFrequency = Duration.minute * 30
}

// 从容器里解析统一的缓存接口。
let root = services.build()
let cache = root.getOrThrow<IDistributedCache>()

cache.setString("app.settings", "loaded")

这样做的关键价值是:

  • 业务侧拿到的是 IDistributedCache
  • 当前实现是 MemoryDistributedCache
  • 未来接入其他实现时,业务代码基本不用改

未来扩展:Redis 等缓存实现

当前 soulsoft_extensions_caching 内置的是 MemoryDistributedCache。 它适合:

  • 单进程应用
  • 本地开发
  • 中小规模、无需跨节点共享的数据缓存

但从设计上,这一层并不是为了把开发者永久绑定在内存缓存上。 相反,IDistributedCache 的目的就是给后续接入 Redis 等缓存产品留出统一抽象。

未来扩展时,我们会沿着这个方向演进:

  • 保持 IDistributedCache 作为统一业务接口
  • 新增 Redis 等实现来满足跨节点、分布式部署和更大规模缓存需求
  • 通过类似 addDistributedRedisCache() 的注册方式完成实现切换

对业务代码来说,理想状态是:

  • 不需要重新学习另一套缓存 API
  • 不需要在业务逻辑里到处写针对不同缓存产品的分支
  • 只调整基础设施注册,就能完成缓存实现切换

这会显著减少开发者的学习成本、使用成本和适配压力。

使用建议

  • 业务代码优先依赖 IDistributedCache,不要直接把 MemoryDistributedCache 写死在服务内部
  • 会话类数据优先考虑滑动过期,验证码和一次性令牌优先考虑相对过期
  • 对“最长有效期”明确的场景,使用组合策略更稳妥
  • 当前是内存实现,大数据量或多节点场景要提前考虑后续切换到外部缓存
  • 通过 DI 注册缓存,比手动创建实例更利于后续演进

注意事项

  • get() / getString() 在 key 不存在或已过期时会返回 None
  • refresh() 只对滑动过期有意义,不会改变绝对过期时间
  • 当前默认实现是进程内内存缓存,不天然跨节点共享
  • 后续如果切换到 Redis 等实现,推荐保持业务层继续面向 IDistributedCache