C#与Redis缓存交互及分布式锁实现
C# 与 Redis 缓存交互
Redis 基础介绍
Redis(Remote Dictionary Server)是一个开源的、基于内存的数据结构存储系统,常用作数据库、缓存和消息中间件。它支持多种数据结构,如字符串(string)、哈希(hash)、列表(list)、集合(set)和有序集合(sorted set)。由于 Redis 基于内存操作,所以读写速度极快,非常适合作为缓存使用。
Redis 的优势众多。首先,它具备高性能,能够轻松应对高并发的读写请求。其次,丰富的数据结构使其适用于各种不同的应用场景,例如用哈希存储用户信息,用列表实现消息队列等。再者,Redis 支持数据持久化,通过 RDB(Redis Database)和 AOF(Append - Only File)两种方式,可以将内存中的数据保存到磁盘,保证数据的安全性和可恢复性。此外,Redis 还支持主从复制、集群模式,可扩展性强。
在 C# 中连接 Redis
在 C# 项目中与 Redis 交互,首先需要引入相关的客户端库。常用的 Redis 客户端库有 StackExchange.Redis,它是一个高性能、功能丰富的 Redis 客户端,支持同步和异步操作。
安装 StackExchange.Redis 可以通过 NuGet 包管理器。在 Visual Studio 中,右键点击项目,选择“管理 NuGet 程序包”,在搜索框中输入“StackExchange.Redis”,然后点击安装。
连接 Redis 服务器的代码示例如下:
using StackExchange.Redis;
using System;
class Program
{
static void Main()
{
// 连接字符串,格式为 "host:port,password=yourpassword"
string connectionString = "127.0.0.1:6379,password=yourpassword";
ConnectionMultiplexer redis = ConnectionMultiplexer.Connect(connectionString);
IDatabase db = redis.GetDatabase();
// 测试连接
string testKey = "testKey";
string testValue = "testValue";
db.StringSet(testKey, testValue);
string retrievedValue = db.StringGet(testKey);
Console.WriteLine($"Retrieved value: {retrievedValue}");
redis.Close();
}
}
在上述代码中,首先通过 ConnectionMultiplexer.Connect
方法连接到 Redis 服务器,然后获取一个 IDatabase
对象用于执行各种 Redis 命令。这里通过 StringSet
方法设置一个键值对,再通过 StringGet
方法获取该键对应的值。
C# 中 Redis 常用操作
- 字符串操作
- 设置值:
StringSet
方法用于设置字符串类型的键值对。它有多种重载形式,可以设置过期时间等参数。例如:
- 设置值:
// 设置一个简单的字符串键值对
bool setResult = db.StringSet("name", "John");
// 设置带有过期时间(10 秒)的字符串键值对
TimeSpan expiration = TimeSpan.FromSeconds(10);
bool setResultWithExpiry = db.StringSet("nameWithExpiry", "Jane", expiration);
- 获取值:
StringGet
方法用于获取字符串类型键对应的值。
string name = db.StringGet("name");
string nameWithExpiry = db.StringGet("nameWithExpiry");
- 递增递减:
StringIncrement
和StringDecrement
方法可以对存储在 Redis 中的数字字符串进行递增和递减操作。
// 假设 "counter" 键存储的值为 "10"
db.StringSet("counter", "10");
long incrementedValue = db.StringIncrement("counter");
long decrementedValue = db.StringDecrement("counter");
- 哈希操作
- 设置哈希字段:
HashSet
方法用于设置哈希类型中的字段值。
- 设置哈希字段:
HashEntry[] userData = new HashEntry[]
{
new HashEntry("name", "Alice"),
new HashEntry("age", 30),
new HashEntry("email", "alice@example.com")
};
db.HashSet("user:1", userData);
- 获取哈希字段:
HashGet
方法用于获取哈希类型中指定字段的值。
RedisValue nameFromHash = db.HashGet("user:1", "name");
RedisValue ageFromHash = db.HashGet("user:1", "age");
- 获取所有哈希字段和值:
HashGetAll
方法可以获取哈希类型中的所有字段和值。
HashEntry[] allUserData = db.HashGetAll("user:1");
foreach (HashEntry entry in allUserData)
{
Console.WriteLine($"{entry.Name}: {entry.Value}");
}
- 列表操作
- 向列表右侧添加元素:
ListRightPush
方法用于向列表的右侧(尾部)添加一个或多个元素。
- 向列表右侧添加元素:
// 向 "mylist" 列表右侧添加元素
db.ListRightPush("mylist", "element1");
db.ListRightPush("mylist", new RedisValue[] { "element2", "element3" });
- 从列表左侧弹出元素:
ListLeftPop
方法用于从列表的左侧(头部)弹出一个元素。
RedisValue poppedValue = db.ListLeftPop("mylist");
- 获取列表长度:
ListLength
方法用于获取列表的长度。
long listLength = db.ListLength("mylist");
- 集合操作
- 向集合添加元素:
SetAdd
方法用于向集合中添加一个或多个元素。
- 向集合添加元素:
// 向 "myset" 集合添加元素
db.SetAdd("myset", "item1");
db.SetAdd("myset", new RedisValue[] { "item2", "item3" });
- 获取集合中的所有元素:
SetMembers
方法用于获取集合中的所有元素。
RedisValue[] setMembers = db.SetMembers("myset");
foreach (RedisValue member in setMembers)
{
Console.WriteLine(member);
}
- 检查元素是否在集合中:
SetContains
方法用于检查指定元素是否在集合中。
bool containsItem2 = db.SetContains("myset", "item2");
- 有序集合操作
- 向有序集合添加元素:
SortedSetAdd
方法用于向有序集合中添加一个或多个元素,并指定每个元素的分值(score)。
- 向有序集合添加元素:
// 向 "myzset" 有序集合添加元素
db.SortedSetAdd("myzset", "member1", 10);
db.SortedSetAdd("myzset", new SortedSetEntry[]
{
new SortedSetEntry("member2", 20),
new SortedSetEntry("member3", 15)
});
- 获取有序集合中指定范围的元素:
SortedSetRangeByScore
方法用于获取有序集合中指定分值范围的元素。
RedisValue[] rangeByScore = db.SortedSetRangeByScore("myzset", 10, 15);
foreach (RedisValue member in rangeByScore)
{
Console.WriteLine(member);
}
- 获取元素的分值:
SortedSetScore
方法用于获取有序集合中指定元素的分值。
double scoreOfMember2 = db.SortedSetScore("myzset", "member2");
C# 与 Redis 分布式锁实现
分布式锁概述
在分布式系统中,多个应用实例可能同时访问共享资源,为了避免并发访问导致的数据不一致等问题,需要使用分布式锁。分布式锁是一种跨多个进程或实例的互斥机制,确保在同一时间只有一个实例能够获取锁并执行关键操作。
与单机锁(如 C# 中的 lock
关键字)不同,分布式锁需要在多个节点之间进行协调。Redis 由于其高性能、原子性操作等特点,非常适合用于实现分布式锁。
Redis 分布式锁的实现原理
Redis 分布式锁的核心原理基于 Redis 的原子性操作。常用的实现方式是使用 SETNX
(SET if Not eXists)命令。SETNX
命令在键不存在时,为键设置指定的值,若键已存在则不做任何操作。
当一个客户端尝试获取锁时,它会使用 SETNX
命令尝试设置一个特定的键值对。如果设置成功,说明该客户端获取到了锁;如果设置失败,说明锁已被其他客户端获取。
为了避免死锁,通常会给锁设置一个过期时间。这样即使持有锁的客户端出现故障未能释放锁,锁也会在过期后自动释放。
使用 C# 实现 Redis 分布式锁
- 简单实现 以下是一个使用 StackExchange.Redis 实现 Redis 分布式锁的简单示例:
using StackExchange.Redis;
using System;
using System.Threading;
class RedisDistributedLock
{
private readonly IDatabase _database;
private readonly string _lockKey;
private readonly string _lockValue;
private readonly TimeSpan _expiry;
public RedisDistributedLock(IDatabase database, string lockKey, TimeSpan expiry)
{
_database = database;
_lockKey = lockKey;
_lockValue = Guid.NewGuid().ToString();
_expiry = expiry;
}
public bool TryAcquireLock()
{
return _database.StringSet(_lockKey, _lockValue, _expiry, When.NotExists);
}
public void ReleaseLock()
{
if (_database.StringGet(_lockKey) == _lockValue)
{
_database.KeyDelete(_lockKey);
}
}
}
class Program
{
static void Main()
{
string connectionString = "127.0.0.1:6379,password=yourpassword";
ConnectionMultiplexer redis = ConnectionMultiplexer.Connect(connectionString);
IDatabase db = redis.GetDatabase();
string lockKey = "myDistributedLock";
TimeSpan lockExpiry = TimeSpan.FromSeconds(10);
RedisDistributedLock lockObj = new RedisDistributedLock(db, lockKey, lockExpiry);
if (lockObj.TryAcquireLock())
{
try
{
Console.WriteLine("Lock acquired. Performing critical operation...");
// 模拟关键操作
Thread.Sleep(5000);
}
finally
{
lockObj.ReleaseLock();
Console.WriteLine("Lock released.");
}
}
else
{
Console.WriteLine("Failed to acquire lock.");
}
redis.Close();
}
}
在上述代码中,RedisDistributedLock
类封装了分布式锁的获取和释放逻辑。TryAcquireLock
方法使用 StringSet
方法并设置 When.NotExists
选项,模拟 SETNX
操作来尝试获取锁。ReleaseLock
方法首先检查当前锁的值是否与当前实例设置的值相同,只有相同才删除键以释放锁,这样可以避免误释放其他实例获取的锁。
- 优化实现 上述简单实现存在一些问题,例如在高并发场景下,锁的过期时间可能设置不合理,导致关键操作未完成锁就过期了。可以通过使用 Lua 脚本来优化分布式锁的实现,确保锁的获取、续期和释放操作的原子性。
以下是一个优化后的示例:
using StackExchange.Redis;
using System;
using System.Threading;
class RedisDistributedLock
{
private readonly IDatabase _database;
private readonly string _lockKey;
private readonly string _lockValue;
private readonly TimeSpan _expiry;
private const string AcquireScript = @"
if redis.call('SETNX', KEYS[1], ARGV[1]) == 1 then
redis.call('PEXPIRE', KEYS[1], ARGV[2])
return 1
else
return 0
end";
private const string ReleaseScript = @"
if redis.call('GET', KEYS[1]) == ARGV[1] then
return redis.call('DEL', KEYS[1])
else
return 0
end";
private const string RenewScript = @"
if redis.call('GET', KEYS[1]) == ARGV[1] then
return redis.call('PEXPIRE', KEYS[1], ARGV[2])
else
return 0
end";
public RedisDistributedLock(IDatabase database, string lockKey, TimeSpan expiry)
{
_database = database;
_lockKey = lockKey;
_lockValue = Guid.NewGuid().ToString();
_expiry = expiry;
}
public bool TryAcquireLock()
{
var parameters = new RedisParameter[]
{
_lockKey,
_lockValue,
(long)_expiry.TotalMilliseconds
};
return (long)_database.ScriptEvaluate(AcquireScript, parameters) == 1;
}
public void ReleaseLock()
{
var parameters = new RedisParameter[]
{
_lockKey,
_lockValue
};
_database.ScriptEvaluate(ReleaseScript, parameters);
}
public bool RenewLock()
{
var parameters = new RedisParameter[]
{
_lockKey,
_lockValue,
(long)_expiry.TotalMilliseconds
};
return (long)_database.ScriptEvaluate(RenewScript, parameters) == 1;
}
}
class Program
{
static void Main()
{
string connectionString = "127.0.0.1:6379,password=yourpassword";
ConnectionMultiplexer redis = ConnectionMultiplexer.Connect(connectionString);
IDatabase db = redis.GetDatabase();
string lockKey = "myDistributedLock";
TimeSpan lockExpiry = TimeSpan.FromSeconds(10);
RedisDistributedLock lockObj = new RedisDistributedLock(db, lockKey, lockExpiry);
if (lockObj.TryAcquireLock())
{
try
{
Console.WriteLine("Lock acquired. Performing critical operation...");
// 模拟关键操作,可能需要续期
for (int i = 0; i < 10; i++)
{
if (i == 5)
{
if (lockObj.RenewLock())
{
Console.WriteLine("Lock renewed.");
}
}
Thread.Sleep(1000);
}
}
finally
{
lockObj.ReleaseLock();
Console.WriteLine("Lock released.");
}
}
else
{
Console.WriteLine("Failed to acquire lock.");
}
redis.Close();
}
}
在这个优化版本中,AcquireScript
使用 SETNX
设置锁并同时设置过期时间,保证了这两个操作的原子性。ReleaseScript
用于释放锁,只有当锁的值与当前实例设置的值相同时才删除键。RenewScript
用于续期锁,同样通过 Lua 脚本保证操作的原子性。在 Main
方法中,模拟了关键操作过程中对锁进行续期的场景。
通过以上对 C# 与 Redis 缓存交互以及分布式锁实现的介绍和代码示例,开发者可以在 C# 项目中有效地利用 Redis 的强大功能,提高系统的性能和稳定性,尤其是在分布式系统环境下。无论是缓存数据提高访问速度,还是通过分布式锁保证数据一致性,Redis 都为 C# 开发者提供了优秀的解决方案。