基于 StackExchange.Redis 的客户端封装与事务机制解析
Redis 事务机制解析
在传统关系型数据库中,事务的核心诉求是保证操作的原子性(ACID)。然而,Redis 的事务模型设计有所不同,其本质是一个指令批处理队列。通过 MULTI 开启事务后,客户端发送的指令会进入队列暂存,直到收到 EXEC 命令才统一触发执行。
该机制具备以下关键特征:
- 非严格原子性:队列中的某条指令若在语法上存在错误,Redis 会直接拒绝执行
EXEC;但若是在运行时出现错误(例如对字符串执行集合操作),已执行成功的指令不会回滚,失败指令后的后续指令仍会继续执行。 - 执行隔离性:事务的主要价值在于命令排队期间的互斥性。当一组指令被标记为事务后,其他客户端的请求无法插入该指令序列,从而保证业务逻辑执行的连贯性。
企业级 Redis 客户端封装实践
在实际 .NET 项目中,直接调用底层驱动容易引发连接泄露、键名冲突以及序列化兼容性问题。通过封装统一的缓存客户端,可以集中管理连接池、注入全局键前缀,并采用现代化的 JSON 序列化替代过时的二进制序列化方案。
1. 核心依赖引入
dotnet add package StackExchange.Redis
dotnet add package System.Text.Json
2. 客户端封装实现
以下实现采用依赖注入友好的结构,内置连接懒加载、键前缀自动拼接、泛型序列化以及完整的异步 API 覆盖。代码已重构逻辑流与变量命名,同时保持对 String、Hash、List 及 SortedSet 等核心数据类型的支持。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using StackExchange.Redis;
namespace App.Infrastructure.Caching
{
public class RedisClientManager : IDisposable
{
private readonly IConnectionMultiplexer _connection;
private readonly IDatabase _cache;
private readonly string _prefix;
private readonly JsonSerializerOptions _jsonSettings;
public RedisClientManager(string connectionString, int databaseIndex = 0, string keyNamespace = "APP")
{
_connection = ConnectionMultiplexer.Connect(connectionString);
_cache = _connection.GetDatabase(databaseIndex);
_prefix = string.IsNullOrEmpty(keyNamespace) ? string.Empty : keyNamespace + ":";
_jsonSettings = new JsonSerializerOptions { WriteIndented = false, PropertyNameCaseInsensitive = true };
}
#region 键值基础操作
public async Task<bool> SetValueAsync<T>(string key, T value, TimeSpan? ttl = null)
{
var payload = JsonSerializer.Serialize(value, _jsonSettings);
return await _cache.StringSetAsync(FormatKey(key), payload, ttl);
}
public async Task<T?> GetValueAsync<T>(string key)
{
var data = await _cache.StringGetAsync(FormatKey(key));
return data.IsNullOrEmpty ? default : JsonSerializer.Deserialize<T>(data!, _jsonSettings);
}
public async Task RemoveKeyAsync(params string[] keys)
{
var formattedKeys = keys.Select(FormatKey).ToArray();
await _cache.KeyDeleteAsync(formattedKeys);
}
public async Task UpdateTtlAsync(string key, TimeSpan expiry)
{
await _cache.KeyExpireAsync(FormatKey(key), expiry);
}
#endregion
#region Hash 结构操作
public async Task SetHashFieldAsync<T>(string hashKey, string field, T value)
{
var payload = JsonSerializer.Serialize(value, _jsonSettings);
await _cache.HashSetAsync(FormatKey(hashKey), field, payload);
}
public async Task<Dictionary<string, T>> GetHashFieldsAsync<T>(string hashKey, params string[] fields)
{
var redisHash = FormatKey(hashKey);
var results = await _cache.HashGetAsync(redisHash, fields.Select(f => (RedisValue)f).ToArray());
var dict = new Dictionary<string, T>();
for (int i = 0; i < fields.Length; i++)
{
dict[fields[i]] = results[i].IsNullOrEmpty
? default
: JsonSerializer.Deserialize<T>(results[i]!, _jsonSettings);
}
return dict;
}
#endregion
#region List 队列操作
public async Task EnqueueAsync<T>(string queueName, T item)
{
var payload = JsonSerializer.Serialize(item, _jsonSettings);
await _cache.ListRightPushAsync(FormatKey(queueName), payload);
}
public async Task<T?> DequeueAsync<T>(string queueName)
{
var data = await _cache.ListLeftPopAsync(FormatKey(queueName));
return data.IsNullOrEmpty ? default : JsonSerializer.Deserialize<T>(data!, _jsonSettings);
}
public async Task<long> GetQueueLengthAsync(string queueName)
{
return await _cache.ListLengthAsync(FormatKey(queueName));
}
#endregion
#region SortedSet 有序集合
public async Task AddToRankingAsync<T>(string key, T member, double score)
{
var payload = JsonSerializer.Serialize(member, _jsonSettings);
await _cache.SortedSetAddAsync(FormatKey(key), payload, score);
}
public async Task<IEnumerable<T>> GetTopRangeAsync<T>(string key, int count = 10)
{
var values = await _cache.SortedSetRangeByRankAsync(FormatKey(key), stop: count - 1);
return values.Select(v => JsonSerializer.Deserialize<T>(v, _jsonSettings)!).ToList();
}
#endregion
private string FormatKey(string originalKey)
{
return $"{_prefix}{originalKey}";
}
public void Dispose()
{
_connection?.Dispose();
}
}
}
3. 调用示例
封装完成后,业务层可通过初始化客户端实例直接进行异步缓存读写。以下演示了基础字符串存储、哈希表写入以及队列操作的典型用法:
using var client = new RedisClientManager(
connectionString: "localhost:6379,abortConnect=false",
databaseIndex: 1,
keyNamespace: "PROD"
);
// 写入带过期时间的对象缓存
var user = new { Id = 101, Name = "Developer" };
await client.SetValueAsync("user:profile", user, TimeSpan.FromMinutes(30));
// 读取并反序列化
var cachedUser = await client.GetValueAsync<object>("user:profile");
// 哈希表字段更新
await client.SetHashFieldAsync("app:config", "theme_mode", "dark");
// 消息入队与出队
await client.EnqueueAsync("task:queue", new { JobId = Guid.NewGuid(), Priority = 1 });
var nextTask = await client.DequeueAsync<object>("task:queue");