MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C#与Redis缓存交互及分布式锁实现

2022-06-272.5k 阅读

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 常用操作

  1. 字符串操作
    • 设置值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");
  • 递增递减StringIncrementStringDecrement 方法可以对存储在 Redis 中的数字字符串进行递增和递减操作。
// 假设 "counter" 键存储的值为 "10"
db.StringSet("counter", "10");
long incrementedValue = db.StringIncrement("counter");
long decrementedValue = db.StringDecrement("counter");
  1. 哈希操作
    • 设置哈希字段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}");
}
  1. 列表操作
    • 向列表右侧添加元素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");
  1. 集合操作
    • 向集合添加元素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");
  1. 有序集合操作
    • 向有序集合添加元素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 分布式锁

  1. 简单实现 以下是一个使用 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 方法首先检查当前锁的值是否与当前实例设置的值相同,只有相同才删除键以释放锁,这样可以避免误释放其他实例获取的锁。

  1. 优化实现 上述简单实现存在一些问题,例如在高并发场景下,锁的过期时间可能设置不合理,导致关键操作未完成锁就过期了。可以通过使用 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# 开发者提供了优秀的解决方案。