Skip to content

缓存

IDistributedCache 统一读写入口,在不改业务调用方式的前提下接入缓存能力。

适用场景

  • Chat 应用里有回复结果、验证码、会话状态这类“读多写少”的数据。
  • 业务代码不想直接依赖某个具体缓存 SDK。
  • 需要给缓存项补上统一的过期策略,而不是在各处散落失效逻辑。
  • 应用已经接入依赖注入,希望缓存和其他基础设施一样通过容器解析。

如果这些问题已经出现,真正要统一的不是”某个缓存产品的用法”,而是”业务代码如何读写缓存”这一层接口。

核心问题

soulsoft_extensions_caching 当前提供两层能力:

  • IDistributedCache:统一定义 getsetrefreshremove 这组基础操作。
  • MemoryDistributedCache:基于内存的默认实现,可直接使用,也可通过 DI 注册成 IDistributedCache

这层抽象的价值不在于“少写几个方法名”,而在于把业务代码和具体实现解耦。

这张图只表达一条主线:业务代码依赖接口,当前内置实现是 MemoryDistributedCache,如果后续需要接入别的存储,应继续复用 IDistributedCache 这一层。

信息

当前源码内置的只有 MemoryDistributedCacheaddDistributedMemoryCache()。文档中的扩展能力只描述接口层已经预留的替换点,不把未提供的具体注册 API 写成现状。

相关依赖

包名作用什么时候需要
soulsoft_extensions_caching提供 IDistributedCacheMemoryDistributedCacheDistributedCacheEntryOptions 等核心类型始终需要
soulsoft_extensions_options提供 Options.create() 创建默认配置对象直接创建 MemoryDistributedCache 时需要

快速开始

先引入 soulsoft_extensions_cachingsoulsoft_extensions_options,按“创建实例 → 写入 → 读取 → 删除”这条最小路径走一遍。

cangjie
import soulsoft_extensions_caching.*
import soulsoft_extensions_options.*

main(): Int64 {
    // 创建缓存实例,使用默认配置
    let cache = MemoryDistributedCache(Options.create(DistributedCacheOptions()))

    cache.setString("chat:last-reply", "hello")
    println(cache.getString("chat:last-reply") ?? "None") // 键存在,输出 hello

    cache.remove("chat:last-reply")
    println(cache.getString("chat:last-reply") ?? "None") // 键已删除,返回 None
    return 0
}

删除后再次读取会返回 None

角色职责常用入口
MemoryDistributedCache当前默认实现MemoryDistributedCache(Options.create(...))
IDistributedCache统一缓存接口get() / set() / refresh() / remove()
DistributedCacheOptions全局缓存配置expirationScanFrequency

信息

本页仓颉示例已在 E:\gitcode\demo 中通过 cjpm build 验证。

核心概念

统一接口

IDistributedCache 把缓存读写收敛成两组方法:

数据形式读取写入
字节数组get(key)set(key, value)
字符串getString(key)setString(key, value)

其中:

  • getString() 内部基于 get() 读取字节数组后再做 UTF-8 转换。
  • setString() 内部会把字符串转成字节数组,再调用 set()

这意味着如果业务本身就是字符串场景,优先直接使用 getString() / setString();如果要缓存原始二进制载荷,再使用 get() / set()

过期判断

缓存项是否过期由 DistributedCacheEntryOptions 决定。当前支持三种条件:

字段含义适合场景
slidingExpiration距离上次访问超过指定时长后过期会话状态、在线状态
absoluteExpiration到达指定 UTC 时间点后过期某个固定时间必须失效的数据
absoluteExpirationRelativeToNow距离创建时间超过指定时长后过期验证码、临时 token

任一条件满足,缓存项都会被判定为过期。

这样设计的原因是:很多业务既需要“空闲太久就失效”,也需要“最长只能活多久”。把三种条件拆开后,可以单独使用,也可以组合使用。

刷新与过期清理

refresh(key) 有两个行为:

  • 如果条目仍然有效,更新最近访问时间。
  • 如果条目已经过期,直接删除该条目。

get(key) 会先调用 refresh(key),再返回缓存值。因此读取本身也会更新滑动过期窗口;这就是滑动过期能够“持续访问就续期”的原因。

除了访问时的被动清理,MemoryDistributedCache 还会按 DistributedCacheOptions.expirationScanFrequency 触发后台扫描,异步移除已经过期但长期未被访问的条目。默认扫描间隔是 Duration.minute

常见用法

读写字符串和字节数组

先看方法名:字符串场景直接用 setString() / getString(),二进制场景用 set() / get()

cangjie
import soulsoft_extensions_caching.*
import soulsoft_extensions_options.*

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

// 字符串场景直接使用 setString / getString
cache.setString("chat:user", "alice")
println(cache.getString("chat:user") ?? "None")

// 二进制场景:写入前手动转换,读取后手动还原
cache.set("chat:payload", "hello".toArray())
if (let Some(bytes) <- cache.get("chat:payload")) {
    println(String.fromUtf8(bytes))
}
// 键不存在时 get() 和 getString() 都返回 None,不会抛异常

如果键不存在,两种读取方式都会返回 None,不会抛异常。

配置过期策略

先看 DistributedCacheEntryOptions(...) 里的三个字段,它们分别对应三种过期条件。

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

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

// 相对过期:从创建时刻起算,5 分钟后失效
cache.setString(
    "chat:code",
    "9527",
    DistributedCacheEntryOptions(
        absoluteExpirationRelativeToNow: Some(Duration.minute * 5)
    )
)

// 绝对时刻过期:盯住固定时间点,适合每日重置等场景
cache.setString(
    "chat:daily-summary",
    "ready",
    DistributedCacheEntryOptions(
        absoluteExpiration: Some(DateTime.nowUTC() + Duration.hour)
    )
)

// 滑动过期:每次访问都会重置窗口
cache.setString(
    "chat:session",
    "alice",
    DistributedCacheEntryOptions(
        slidingExpiration: Some(Duration.minute * 30)
    )
)

这三段代码分别演示了相对过期、绝对时刻过期和滑动过期。区别要点只有一个:相对过期从创建时刻起算,绝对过期盯住固定时间点,滑动过期盯住“最近一次访问”。

组合滑动过期和绝对过期

如果既希望“持续访问就续期”,又希望“最长只能活一段时间”,就把两个条件放在同一个 DistributedCacheEntryOptions 里。

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

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

// 两个条件同时生效:空闲 5 分钟或最长存活 10 分钟,任一先满足即过期
cache.setString(
    "chat:challenge",
    "value",
    DistributedCacheEntryOptions(
        slidingExpiration: Some(Duration.minute * 5),
        absoluteExpirationRelativeToNow: Some(Duration.minute * 10)
    )
)

两个条件中任一先满足,条目就会过期。

与依赖注入集成

实际项目里更常见的做法是把缓存注册进 ServiceCollection,让业务服务直接依赖 IDistributedCache。先看 CachedChatClient 的构造函数和 getReply 方法,再看底部注册代码。

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

class CachedChatClient {
    private let cache: IDistributedCache // 依赖接口,不依赖 MemoryDistributedCache

    public init(cache: IDistributedCache) {
        this.cache = cache // 构造函数注入,由 DI 容器自动解析
    }

    public func getReply(prompt: String): String {
        let key = "chat:reply:${prompt}"
        if (let Some(reply) <- cache.getString(key)) {
            return reply // 缓存命中,直接返回
        }

        // 缓存未命中:计算新值并写入缓存,同时配置过期策略
        let reply = "ok"
        cache.setString(
            key,
            reply,
            DistributedCacheEntryOptions(
                absoluteExpirationRelativeToNow: Some(Duration.minute * 5)
            )
        )
        return reply
    }
}

// 注册阶段:装配 IDistributedCache 和业务服务
let services = ServiceCollection()

services.addDistributedMemoryCache { options => // IDistributedCache 注册为单例
    options.expirationScanFrequency = Duration.minute * 30
}
services.addTransient<CachedChatClient, CachedChatClient>()

let provider = services.build()
let cache = provider.getOrThrow<IDistributedCache>() // 从容器解析同一个缓存实例
cache.setString("chat:settings", "loaded")

let client = provider.getOrThrow<CachedChatClient>() // DI 自动注入 IDistributedCache
println(client.getReply("hello"))

addDistributedMemoryCache()IDistributedCache 注册进容器,CachedChatClient 只依赖接口。

这样组织的原因是:缓存实现和业务服务的装配关系统一留在启动阶段,业务代码只关心“我能不能读写缓存”,不关心实例是怎么创建的。当前注册方式使用单例生命周期,因此同一个根容器解析到的是同一个缓存实例。

最佳实践

  • 业务服务优先依赖 IDistributedCache,不要把 MemoryDistributedCache 直接写进构造函数。这样做的目的是把替换空间留在注册阶段,而不是把具体实现散落到业务层。
  • 字符串场景优先使用 getString() / setString()。只有在需要原始二进制载荷时,再退回 get() / set(),这样调用点更容易读。
  • 只在写入时配置过期策略,不要把”多久失效”散落到读取逻辑里。过期规则跟缓存项本身绑定,排错时才容易定位。
  • expirationScanFrequency 只影响后台扫描节奏,不改变单次 get() / refresh() 对过期条目的即时判断。也就是说,调大扫描间隔不会让过期数据在读取时继续命中。

推荐组织方式:业务服务通过构造函数接收 IDistributedCache,写入时携带过期策略。

cangjie
class ChatReplyService {
    private let cache: IDistributedCache

    public init(cache: IDistributedCache) {
        this.cache = cache
    }

    public func getOrCompute(key: String): String {
        if (let Some(hit) <- cache.getString(key)) {
            return hit
        }
        let value = “computed
        // 过期策略只在写入时指定,不在读取路径中
        cache.setString(
            key,
            value,
            DistributedCacheEntryOptions(
                absoluteExpirationRelativeToNow: Some(Duration.minute * 5)
            )
        )
        return value
    }
}

限制与边界

  • 当前内置实现只有 MemoryDistributedCache,数据保存在当前进程内。
  • addDistributedMemoryCache() 也是当前唯一提供的容器注册入口。
  • 文档能确认的扩展点是“你可以自己实现 IDistributedCache 并按 IDistributedCache 注册进容器”;但具体外部缓存产品的实现和注册 API,并不属于当前源码已提供的现状。
  • getString() 依赖 UTF-8 转换。如果缓存的是非文本二进制数据,应使用 get() 读取原始字节数组。

警告

MemoryDistributedCache 适合本地开发、单进程应用或接口抽象层验证。它不负责跨进程、跨节点共享数据,因此不能把“内存实现”直接等同于“多实例共享缓存”。

常见问题

getString() 返回 None 是什么情况?

常见只有三种情况:键从未写入、条目已被删除、条目已经过期。当前实现对这三种情况都返回 None,不会区分错误类型。

refresh() 会不会把绝对过期时间一起延后?

不会。refresh() 只更新最近访问时间,因此只对 slidingExpiration 有意义。绝对过期时间和相对创建时间过期都不会因为 refresh() 而被推迟。

什么时候需要调整 expirationScanFrequency

只有当你明确在权衡“后台扫描频率”和“清理滞后成本”时才需要调整。默认值已经是 Duration.minute,多数场景可以直接使用默认配置。

怎么接入自己的缓存实现?

先实现 IDistributedCache 的四个基础方法:getsetrefreshremove,再按 IDistributedCache 注册到容器里。这样业务层仍然只依赖统一接口,不需要跟着你的实现一起改构造函数和调用方式。

相关专题

  • 依赖注入addDistributedMemoryCache()IDistributedCache 注册为 Singleton
  • 选项DistributedCacheOptionsDistributedCacheEntryOptions 通过选项模式管理配置
  • 配置管理 — 缓存策略可通过 IConfiguration 绑定