🗄️ Redis 概述
Snet.Redis 是一个高性能 Redis 客户端库,基于 StackExchange.Redis 封装,面向 .NET 8/10。它提供了统一的 API,提供 String、List、Hash、SortedSet 与通用 Key 操作(没有独立 Set API)以及发布/订阅功能。主要特性包括 TAG 前缀命名空间、对象存储的 JSON 序列化、同步/异步双模 API、连接事件监控以及基于 Lua 脚本的 通配符键删除。
| 属性 | 值 |
|---|---|
| 包名 | Snet.Redis |
| 命名空间 | Snet.Redis |
| 目标框架 | .NET 8.0 / .NET 10.0 |
| 依赖项 | StackExchange.Redis 3.4.20, Snet.Core |
| 许可证 | MIT |
安装版本基线为 26.301.1(2026-10-29 已核对 NuGet)。本文行为及第三方依赖按当前源码核对;源码 <Version>1.0.0.0</Version> 与内部 ProjectReference 是构建配置,不是安装版本;源码行为可能晚于已发布包。
▶️ 快速开始
安装包:
dotnet add package Snet.Redis
以下示例涵盖连接、字符串操作、对象序列化与断开连接:
using Snet.Redis;
var redis = RedisOperate.Instance(new RedisData.Basics
{
ConnectStr = "127.0.0.1:6379",
DataBaseID = 0,
Expiry = 86400000, // 24 小时(毫秒)
TAG = "MyApp:"
});
// 连接
var result = await redis.OnAsync();
if (!result.Status) { Console.WriteLine($"连接失败: {result.Message}"); return; }
// 字符串操作
await redis.StringSetAsync("greeting", "Hello Redis!");
var value = await redis.StringGetAsync("greeting");
Console.WriteLine(value); // "Hello Redis!"
// 对象序列化
await redis.StringSetAsync("user:1", new { Name = "John", Age = 30 });
var user = await redis.StringGetAsync<User>("user:1");
Console.WriteLine($"{user.Name}, {user.Age}");
// 断开连接
await redis.OffAsync();
// 类型声明放在顶级语句之后。
public class User
{
public string Name { get; set; } = string.Empty;
public int Age { get; set; }
}
⚙️ 安装与配置
通过 RedisData.Basics 类进行配置:
| 参数 | 类型 | 默认值 | 描述 |
|---|---|---|---|
ConnectStr |
string |
— | Redis 连接字符串(如 "127.0.0.1:6379") |
DataBaseID |
int |
0 |
Redis 数据库编号(0-15) |
Expiry |
int |
86400000 |
单值 StringSet/StringSetAsync(含泛型重载)的 TTL,单位毫秒,默认 24 小时 |
TAG |
string |
"S:" |
键前缀,用于命名空间隔离 |
连接字符串格式:
// 本地
"127.0.0.1:6379"
// 远程 + 密码
"remote-host:6379,password=xxx"
// 哨兵模式
"sentinel-host:26379,serviceName=mymaster"
// 多端点
"host1:6379,host2:6379,host3:6379"
🧠 核心概念
1. TAG 前缀系统
大多数单键操作通过 GTAG() 添加非空白的 TAG。批量 KeyDelete(IEnumerable<string>) 及其异步版本接收完整 Redis 键,不自动添加 TAG。KeyRename/KeyRenameAsync 仅给旧键添加前缀,newKey 原样使用。发布/订阅频道也原样使用传入名称,不添加 TAG。
// 当 TAG = "MyApp:"
await redis.StringSetAsync("user:1", "Alice");
// 实际 Redis 键: "MyApp:user:1"
2. JSON 序列化
对象使用 value.ToJson()(来自 Snet.Core)存储,并通过 JsonSerializer.Deserialize<T>() 读取。这允许无缝存储复杂的 .NET 对象,无需手动序列化。
await redis.StringSetAsync("user:1", new User { Name = "John", Age = 30 });
var user = await redis.StringGetAsync<User>("user:1");
3. 同步/异步双模 API
操作提供同步和异步版本,但部分参数和行为有所不同:
// 同步
redis.StringSet("key", "value");
var val = redis.StringGet("key");
// 异步
await redis.StringSetAsync("key", "value");
var val = await redis.StringGetAsync("key");
4. 单例模式
RedisOperate 内部使用 CoreUnify<T, B>.Instance(),确保每个唯一配置对应一个连接池。重用相同配置会返回同一实例。
5. 连接事件
库暴露七种连接事件,均通过 OnInfoEvent 上报:
| 事件 | 描述 |
|---|---|
ConnectionRestored |
丢失的连接重新建立时触发 |
ConnectionFailed |
连接尝试失败时触发 |
ErrorMessage |
Redis 错误消息时触发 |
ConfigurationChanged |
主/副本拓扑变更时触发 |
HashSlotMoved |
哈希槽迁移时触发 |
InternalError |
库内部错误时触发 |
ConfigurationChangedBroadcast |
广播配置变更时触发 |
redis.OnInfoEvent += (sender, e) =>
{
Console.WriteLine($"[{e.Status}] {e.Message}");
};
6. 断开连接时的返回值约定
大多数数据操作先调用 GetStatus(),若检查结果为未连接则返回下表中的值。这不是异常兜底保证:命令失败和 JSON 反序列化异常可以向外抛出。发布/订阅方法直接使用连接,没有断连默认值检查。
| 返回类型 | 默认值 |
|---|---|
bool |
false |
long |
-1 |
double |
-1(两个 SortedSetIncrement 版本) |
string? |
null |
T? |
default |
IEnumerable? |
null |
📚 API 参考
生命周期方法
| 方法 | 签名 | 描述 |
|---|---|---|
On |
OperateResult On() |
同步连接 |
OnAsync |
Task<OperateResult> OnAsync(CancellationToken token = default) |
异步连接 |
Off |
OperateResult Off(bool hardClose = false) |
同步断开连接 |
OffAsync |
Task<OperateResult> OffAsync(bool hardClose = false, CancellationToken token = default) |
异步断开连接 |
GetStatus |
OperateResult GetStatus() |
获取连接状态 |
GetStatusAsync |
Task<OperateResult> GetStatusAsync(CancellationToken token = default) |
异步获取连接状态 |
Dispose |
void Dispose() |
清理资源 |
DisposeAsync |
ValueTask DisposeAsync() |
异步清理资源 |
GetBaseObject |
OperateResult GetBaseObject() |
获取底层 ConnectionMultiplexer(通过 ResultData) |
GetBaseObjectAsync |
Task<OperateResult> GetBaseObjectAsync(CancellationToken token = default) |
异步获取底层连接对象 |
字符串操作(10 个方法)
| 方法 | 返回 | 描述 |
|---|---|---|
StringSet(key, value) |
bool |
存储字符串值 |
StringSetAsync(key, value) |
Task<bool> |
异步存储字符串 |
StringGet(key) |
string? |
获取字符串值 |
StringGetAsync(key) |
Task<string?> |
异步获取字符串 |
StringSet<T>(key, value) |
bool |
以 JSON 格式存储对象 |
StringSetAsync<T>(key, value) |
Task<bool> |
异步以 JSON 格式存储对象 |
StringGet<T>(key) |
T? |
获取并反序列化对象 |
StringGetAsync<T>(key) |
Task<T?> |
异步获取并反序列化对象 |
StringSet(kvPairs) |
bool |
批量存储键值对 |
StringSetAsync(kvPairs) |
Task<bool> |
异步批量存储 |
列表操作(22 个方法)
| 方法 | 返回 | 描述 |
|---|---|---|
ListLeftPush(key, value) |
long |
将值推入列表头部 |
ListLeftPushAsync(key, value) |
Task<long> |
异步推入头部 |
ListRightPush(key, value) |
long |
将值推入列表尾部 |
ListRightPushAsync(key, value) |
Task<long> |
异步推入尾部 |
ListLeftPop(key) |
string? |
从头部弹出值 |
ListLeftPopAsync(key) |
Task<string?> |
异步从头部弹出 |
ListRightPop(key) |
string? |
从尾部弹出值 |
ListRightPopAsync(key) |
Task<string?> |
异步从尾部弹出 |
ListLeftPush<T>(key, value) |
long |
将序列化对象推入头部 |
ListLeftPushAsync<T>(key, value) |
Task<long> |
异步推入序列化对象到头部 |
ListRightPush<T>(key, value) |
long |
将序列化对象推入尾部 |
ListRightPushAsync<T>(key, value) |
Task<long> |
异步推入序列化对象到尾部 |
ListLeftPop<T>(key) |
T? |
从头部弹出并反序列化 |
ListLeftPopAsync<T>(key) |
Task<T?> |
异步从头部弹出并反序列化 |
ListRightPop<T>(key) |
T? |
从尾部弹出并反序列化 |
ListRightPopAsync<T>(key) |
Task<T?> |
异步从尾部弹出并反序列化 |
ListRemove(key, value) |
long |
从列表中移除值 |
ListRemoveAsync(key, value) |
Task<long> |
异步移除值 |
ListLength(key) |
long |
获取列表长度 |
ListLengthAsync(key) |
Task<long> |
异步获取列表长度 |
ListRange(key, start, stop) |
IEnumerable<string?> |
获取范围内的值 |
ListRangeAsync(key, start, stop) |
Task<IEnumerable<string?>?> |
异步获取范围内的值 |
哈希操作(22 个方法)
| 方法 | 返回 | 描述 |
|---|---|---|
HashExists(key, hashField) |
bool |
检查哈希字段是否存在 |
HashExistsAsync(key, hashField) |
Task<bool> |
异步检查字段存在性 |
HashDelete(key, hashField) |
bool |
删除哈希字段 |
HashDeleteAsync(key, hashField) |
Task<bool> |
异步删除字段 |
HashSet(key, hashField, value) |
bool |
设置哈希字段值 |
HashSetAsync(key, hashField, value) |
Task<bool> |
异步设置字段值 |
HashGet(key, hashField) |
string? |
获取哈希字段值 |
HashGetAsync(key, hashField) |
Task<string?> |
异步获取字段值 |
HashSet<T>(key, hashField, value) |
bool |
设置序列化对象作为字段值 |
HashSetAsync<T>(key, hashField, value) |
Task<bool> |
异步设置序列化字段 |
HashGet<T>(key, hashField) |
T? |
获取并反序列化字段值 |
HashGetAsync<T>(key, hashField) |
Task<T?> |
异步获取反序列化字段 |
HashKeys(key) |
IEnumerable<string?> |
获取所有哈希字段名 |
HashKeysAsync(key) |
Task<IEnumerable<string?>?> |
异步获取字段名 |
HashValues(key) |
IEnumerable<string?> |
获取所有哈希值 |
HashValuesAsync(key) |
Task<IEnumerable<string?>?> |
异步获取值 |
多字段重载:
| 方法 | 返回 | 描述 |
|---|---|---|
HashGet(key, IEnumerable<string> hashFields) |
IEnumerable<string?> |
一次获取多个字段 |
HashGetAsync(key, IEnumerable<string> hashFields, string value) |
Task<IEnumerable<string?>?> |
异步多字段获取 |
HashSet(key, IEnumerable<KeyValuePair<string, string>> hashFields) |
void |
一次设置多个字段 |
HashSetAsync(key, IEnumerable<KeyValuePair<string, string>> hashFields) |
Task |
异步多字段设置 |
HashDelete(key, IEnumerable<string> hashFields) |
long |
删除多个字段 |
HashDeleteAsync(key, IEnumerable<string> hashFields) |
Task<long> |
异步多字段删除 |
有序集合操作(12 个方法)
| 方法 | 返回 | 描述 |
|---|---|---|
SortedSetAdd(key, member, score) |
bool |
添加带分数的成员 |
SortedSetAddAsync(key, member, score) |
Task<bool> |
异步添加成员 |
SortedSetAdd<T>(key, member, score) |
bool |
添加序列化成员及分数 |
SortedSetAddAsync<T>(key, member, score) |
Task<bool> |
异步添加序列化成员 |
SortedSetRemove(key, member) |
bool |
移除成员 |
SortedSetRemoveAsync(key, member) |
Task<bool> |
异步移除成员 |
SortedSetRangeByRank(key, start = 0L, stop = -1L, order = RedisOperate.OrderType.Asc) |
IEnumerable<string> |
按排名范围和排序方向获取成员 |
SortedSetRangeByRankAsync(key) |
Task<IEnumerable<string?>?> |
异步按排名获取所有成员 |
SortedSetLength(key) |
long |
获取集合基数 |
SortedSetLengthAsync(key) |
Task<long> |
异步获取基数 |
SortedSetIncrement(key, member, value) |
double |
增加成员分数 |
SortedSetIncrementAsync(key, member, value) |
Task<double> |
异步增加分数 |
键操作(10 个方法)
RedisOperate.OrderType 是公开的嵌套排序枚举:Asc = 0、Desc = 1。同步排名范围方法接收该参数;SortedSetRangeByRankAsync(string key) 没有范围或排序参数,按升序读取全部成员。
批量字符串写入以及 List/Hash/SortedSet 写入不会自动应用 Expiry。需要 TTL 时调用 KeyExpire/KeyExpireAsync。
| 方法 | 返回 | 描述 |
|---|---|---|
KeyDelete(key) |
bool |
删除键(通过 Lua 支持 * 通配符) |
KeyDeleteAsync(key) |
Task<bool> |
异步删除键(仅支持单个键,不支持通配符) |
KeyDelete(IEnumerable<string> keys) |
long |
删除多个完整 Redis 键,不自动添加 TAG |
KeyDeleteAsync(IEnumerable<string> keys) |
Task<long> |
异步删除完整 Redis 键,不自动添加 TAG |
KeyExists(key) |
bool |
检查键是否存在 |
KeyExistsAsync(key) |
Task<bool> |
异步检查键存在性 |
KeyRename(key, newKey) |
bool |
仅旧键添加前缀,newKey 原样使用 |
KeyRenameAsync(key, newKey) |
Task<bool> |
异步重命名,前缀规则相同 |
KeyExpire(key, expiry) |
bool |
设置键的 TTL |
KeyExpireAsync(key, expiry) |
Task<bool> |
异步设置 TTL |
发布/订阅操作(6 个方法)
| 方法 | 返回 | 描述 |
|---|---|---|
Subscribe(channel, handler) |
void |
订阅频道 |
SubscribeAsync(channel, handler) |
Task |
异步订阅 |
Publish(channel, message) |
long |
发布消息 |
PublishAsync(channel, message) |
Task<long> |
异步发布 |
Publish<T>(channel, message) |
long |
发布序列化对象 |
PublishAsync<T>(channel, message) |
Task<long> |
异步发布序列化对象 |
💻 代码示例
完整示例,涵盖四种Redis数据结构和键操作以及发布/订阅:
using Snet.Redis;
var redis = RedisOperate.Instance(new RedisData.Basics
{
ConnectStr = "127.0.0.1:6379",
DataBaseID = 0,
TAG = "Demo:"
});
await redis.OnAsync();
// ── 字符串 ──────────────────────────────────
await redis.StringSetAsync("key1", "value1");
Console.WriteLine(await redis.StringGetAsync("key1"));
// ── 列表 ────────────────────────────────────
await redis.ListRightPushAsync("mylist", "item1");
await redis.ListRightPushAsync("mylist", "item2");
Console.WriteLine(await redis.ListLeftPopAsync("mylist")); // "item1"
// ── 哈希 ────────────────────────────────────
await redis.HashSetAsync("user", "name", "Alice");
await redis.HashSetAsync("user", "age", "30");
Console.WriteLine(await redis.HashGetAsync("user", "name"));
// ── 有序集合 ───────────────────────────────
await redis.SortedSetAddAsync("scores", "player1", 100);
var top = await redis.SortedSetRangeByRankAsync("scores");
foreach (var item in top) Console.WriteLine(item);
// ── 键操作 ──────────────────────────
Console.WriteLine(await redis.KeyExistsAsync("key1")); // True
redis.KeyDelete("*"); // 清除所有 Demo: 键(同步版通过 Lua 支持 `*` 通配符;异步版删除的是字面键)
// ── 发布/订阅 ─────────────────────────────────
await redis.SubscribeAsync("news", (channel, value) =>
Console.WriteLine($"收到: {value}"));
await redis.PublishAsync("news", "Breaking!");
await redis.OffAsync();
对象序列化示例:
public class Order
{
public string Id { get; set; }
public decimal Total { get; set; }
public DateTime CreatedAt { get; set; }
}
// 存储
var order = new Order { Id = "ORD-001", Total = 99.99m, CreatedAt = DateTime.UtcNow };
await redis.StringSetAsync("order:001", order);
// 读取
var saved = await redis.StringGetAsync<Order>("order:001");
Console.WriteLine($"订单 {saved.Id}: ${saved.Total}");
连接事件监控:
redis.OnInfoEvent += (sender, e) =>
{
if (e.Status)
Console.WriteLine($"[信息] {e.Message}");
else
Console.WriteLine($"[错误] {e.Message}");
};
❓ FAQ
1. 如何连接远程 Redis 服务器?
将 ConnectStr 设置为远程端点,可选带密码:
"remote-host:6379,password=xxx"
如需 TLS/SSL 连接,追加 ,ssl=true。
2. Redis 宕机时会发生什么?
大多数数据方法在 GetStatus() 返回未连接时返回各自的默认值。发布/订阅没有此检查,命令和序列化异常也可能向外抛出。状态检查后连接仍可能失败,应检查连接结果并处理操作异常,通过连接事件观察恢复。
3. TAG 前缀是否支持通配符删除?
支持。同步方法 KeyDelete("*") 使用 Lua KEYS 匹配所有带 TAG 前缀的键,确保仅删除属于您应用的键。
4. 操作是否线程安全?
StackExchange.Redis 的 ConnectionMultiplexer 支持共享的并发命令,但这不保证 RedisOperate 的并发生命周期变化安全。应协调 On/Off/释放与运行中的命令;断开连接会把连接和数据库字段设为 null。
5. 如何处理对象序列化?
使用泛型 <T> 方法,写入时自动通过 JSON(Snet.Core 的 ToJson())进行序列化,读取时自动反序列化。无需手动序列化。
6. 同步和异步 API 有什么区别?
异步上下文宜使用异步版本,但需核对各个签名:只有同步单键 KeyDelete 通过 Lua 展开 *,只有同步 SortedSetRangeByRank 接收范围和排序参数。HashGetAsync 的多字段重载还要求传入当前未使用的 string value 参数。同步生命周期包装会阻塞等待对应异步实现。
