XiHan Framework 处于预览阶段

查看获取方式
跳转到内容

XiHan.Framework.Caching.Redis ​

caching.distributed@1 的 Redis Provider,基于 StackExchange.Redis:装配后共享缓存、ICacheStore、分布式锁、IDistributedCache 与 Idempotency 阶段中间件改由 Redis 承担,适用于多实例部署。

项内容
包 IDXiHan.Framework.Caching.Redis
环层Ring 3(实现)
能力提供 caching.distributed@1(role: provider);依赖 kernel.hosting@1
特性入口RedisCachingFeature(清单 ID xihan.caching.redis)
配置节XiHan:Caching:Redis、XiHan:Caching:Idempotency;可选绑定 cache(类型 cache:distributed)
依赖XiHan.Framework.Caching.Abstractions;StackExchange.Redis
AOT不兼容(IsAotCompatible=false,StackExchange.Redis 未声明 AOT 兼容)
稳定性Preview

概述 ​

RedisCacheStore 实现 ICacheStore、ICacheStoreInspector 与 ICacheChangeFeed,特性用它注册:

  • ISharedCache:SerializingCache + RedisCacheStore,值经 ICacheSerializer 序列化。
  • ICacheStore、IDistributedLock(StoreDistributedLock)、IDistributedCache(CacheStoreDistributedCache),以及 Idempotency 阶段的 IdempotencyMiddleware。

进程内的 ICache 不由本包提供,仍是 Caching.InMemory(或 Caching.Hybrid)。被本包顶替后,内存实现不再注册共享缓存、锁与幂等中间件。

存储的编码与命令:

  • 键加前缀 KeyPrefix(默认 xihan:)。
  • 值前有 1 字节头:0 表示绝对过期;1 表示滑动过期,后跟 8 字节窗口毫秒数。滑动条目在读取时用 EXPIRE 延长。
  • 不存在才写用 SET NX PX;比较删用 Lua 脚本跳过头部比较载荷后 DEL,在服务端原子执行。
  • 批量读用一次 MGET(命中的滑动条目再用一个批次统一延长);批量写与删把全部命令放进一个批次,一次往返。

连接在第一次解析 IConnectionMultiplexer 时建立,StartAsync 触发并核对,连不上时启动失败;停止时关闭连接。

何时使用 ​

  • 多实例部署需要共享缓存、跨实例互斥的锁与幂等去重。
  • 配合 Caching.Hybrid:Redis 实现了变更通知,两级缓存的 L1 在别的实例写入后立即失效。
  • 单实例、开发与测试用 Caching.InMemory 即可;后端不是 Redis、或要用 .NET 以外的语言实现存储时用 Caching.Remote。

安装与启用 ​

bash
dotnet add package XiHan.Framework.Caching.Redis --prerelease

当前是预览版,安装要带 --prerelease;从源码或本地包源引用见快速上手。

csharp
using XiHan.Framework.Caching.Redis;
using XiHan.Framework.Hosting.AspNetCore;
using XiHan.Framework.Profiles.Standard;

var builder = WebApplication.CreateBuilder(args);

// 内存实现继续提供 caching.local,Redis 顶替 caching.distributed
builder.AddXiHan(xihan => xihan.UseStandardProfile().UseFeature<RedisCachingFeature>());

配置 ​

键类型缺省说明
XiHan:Caching:Redis:Configuration字符串—StackExchange.Redis 连接字符串;有绑定 cache 时被绑定的主值覆盖,两者都没有时装配失败;绑定的属性 username / password 覆盖其中的 user / password(连接时按客户端选项设置,不改写字符串,密码里有逗号也不会拆错)
XiHan:Caching:Redis:KeyPrefix字符串xihan:键前缀;变更频道为 <KeyPrefix>__changes
XiHan:Caching:Redis:DefaultTimeToLiveSeconds正数300条目没有指定过期时的存活时间
XiHan:Caching:Redis:PublishChanges布尔true写入与删除后是否向变更频道发布;关掉后本实例的同步通知不受影响
XiHan:Caching:Redis:HealthProbeIntervalSeconds正数15健康项的重探间隔,也是单次探测的超时
json
{
  "XiHan": {
    "Caching": {
      "Redis": {
        "Configuration": "localhost:6379",
        "KeyPrefix": "orders:",
        "DefaultTimeToLiveSeconds": 600
      }
    }
  }
}

值的序列化器按「已登记的 ICacheSerializer → 已登记的 JsonSerializerOptions(须带 TypeInfoResolver)→ 反射版 JsonCacheSerializer」的顺序选,在 AddXiHan 装配时决定,所以要在 AddXiHan 之前登记,见序列化器。幂等选项见 Caching.Abstractions。

主要类型 ​

类型说明
RedisCachingFeature特性入口(IHostedFeature);常量 ConnectionBinding(cache)、HealthName(caching.redis.connection)
RedisCachingOptionsConfiguration、KeyPrefix、DefaultTimeToLive、PublishChanges、HealthProbeInterval;FromConfiguration(configuration, binding)
RedisCacheStore字节存储,实现 ICacheStore、ICacheStoreInspector、ICacheChangeFeed、IDisposable

使用示例 ​

订阅存储的变更,在别的实例写入或删除时清掉自己维护的本地副本(两级缓存就是这样做的):

csharp
using System.Collections.Concurrent;
using XiHan.Framework.Caching.Abstractions.Caches;
using XiHan.Framework.Caching.Abstractions.Stores;

public sealed class LocalMirror : IDisposable
{
    private readonly ConcurrentDictionary<string, string> _copies = new(StringComparer.Ordinal);
    private readonly IDisposable? _subscription;

    public LocalMirror(ISharedCache cache)
    {
        // 只有实现了变更通知的存储(Redis)才订阅得到
        _subscription = cache.Store is ICacheChangeFeed feed ? feed.Subscribe(OnChanged) : null;
    }

    public bool InvalidatesAcrossInstances => _subscription is not null;

    // 处理器在写入的调用里同步执行,只做内存操作
    private void OnChanged(CacheChange change)
    {
        if (change.Prefix is { } prefix)
        {
            foreach (var key in _copies.Keys.Where(k => k.StartsWith(prefix, StringComparison.Ordinal)))
            {
                _copies.TryRemove(key, out _);
            }

            return;
        }

        foreach (var key in change.Keys)
        {
            _copies.TryRemove(key.Value, out _);
        }
    }

    public void Dispose() => _subscription?.Dispose();
}

行为与限制 ​

  • 连接或超时失败抛出携带 CACHING_UNAVAILABLE_001 的 FrameworkException,不当成未命中。
  • 键管理 ICacheStoreInspector 用单个端点的 SCAN:续页标记是游标,CacheKeyQuery.Limit 只是提示值,同一个键可能出现在两页;前缀里的 glob 字符被转义,只按前缀匹配。读元数据把 PTTL、STRLEN、GETRANGE 放进一个批次,不延长滑动过期;按前缀删除逐页 SCAN 后 DEL。
  • 变更通知:每次写入与删除(真的删到了才算,按前缀删除总是)后同步通知本实例的订阅者,再发后不等地向 <KeyPrefix>__changes 发布;消息带来源标识,不回送给发出者;第一次有人订阅时才订阅频道,释放存储即退订。发布失败不让写入失败,断线期间的通知会丢失。
  • 多个应用共用一个 Redis 时用不同的 KeyPrefix 隔开键与变更频道。
  • 测试:一致性套件需要真实 Redis,设置环境变量 XIHAN_TEST_REDIS(如 localhost:6379)后运行,未设置时跳过。
健康项状态
caching.redis.connection启动时同步 PING 一次,之后按 HealthProbeIntervalSeconds 在后台重探并缓存结果;PING 失败或超时时 Degraded(KERNEL_UNAVAILABLE_005,reason 只写异常类型,完整异常进日志),应用仍在服务

相关 ​

Released under The MIT License