在 .NET Core 中使用 Redis 创建分布式锁

.NET Core Redis 分布式锁,在本文中,我们将讨论如何在 .NET Core 中使用 Redis 创建分布式锁。

我们在构建分布式系统的时候,会面临多个进程共同处理一个共享资源,由于一次只能有一个进程使用共享资源,会导致一些意想不到的问题!

我们可以使用分布式锁来解决这个问题。

为什么是分布式锁?

像往常一样,我们将使用锁来处理这个问题。

下面显示了一些演示锁的使用的示例代码。
public void SomeMethod()  
{  
    //do something...  
    lock(obj)  
    {  
        //do ....  
    }  
    //do something...  
} 

但是,这种锁并不能帮助我们解决问题!这是一个进程内锁,只能解决共享资源的一个进程。

这也是我们需要分布式锁的主要原因!

我将在这里使用 Redis 创建一个简单的分布式锁。

为什么我要使用 Redis 来完成这项工作?因为 Redis 的单线程特性和它执行原子操作的能力。

如何创建锁?

我将创建一个 .NET Core 控制台应用程序来向您展示。

在下一步之前,我们应该运行 Redis 服务器!

StackExchange.Redis是 .NET 中最流行的 Reids 客户端,毫无疑问,我们将使用它来完成以下工作。

首先创建与 Redis 的连接。

/// <summary>  
/// The lazy connection.  
/// </summary>  
private static Lazy<ConnectionMultiplexer> lazyConnection = new Lazy<ConnectionMultiplexer>(() =>  
{  
    ConfigurationOptions configuration = new ConfigurationOptions  
    {  
        AbortOnConnectFail = false,  
        ConnectTimeout = 5000,  
    };  
  
    configuration.EndPoints.Add("localhost", 6379);  
  
    return ConnectionMultiplexer.Connect(configuration.ToString());  
});  
  
/// <summary>  
/// Gets the connection.  
/// </summary>  
/// <value>The connection.</value>  
public static ConnectionMultiplexer Connection => lazyConnection.Value; 
为了请求锁定共享资源,我们执行以下操作:
SET resource_name unique_value NX PX duration  // 持续时间  

resource_name 是应用程序的所有实例将共享的值。

unique_value 对于应用程序的每个实例都必须是唯一的。而这个唯一值的作用就是解除锁定(unlock)。

最后我们还提供了一个持续时间(以毫秒为单位),超过这个时间后,Redis 会自动移除锁。

这是 C# 代码中的实现。

/// <summary>  
/// Acquires the lock.  
/// </summary>  
/// <returns><c>true</c>, if lock was acquired, <c>false</c> otherwise.</returns>  
/// <param name="key">Key.</param>  
/// <param name="value">Value.</param>  
/// <param name="expiration">Expiration.</param>  
static bool AcquireLock(string key, string value, TimeSpan expiration)  
{  
    bool flag = false;  
  
    try  
    {  
        flag = Connection.GetDatabase().StringSet(key, value, expiration, When.NotExists);  
    }  
    catch (Exception ex)  
    {  
        Console.WriteLine($"Acquire lock fail...{ex.Message}");  
        flag = true;  
    }  
  
    return flag;  
} 
 下面是测试获取锁的代码。
static void Main(string[] args)  
{  
    string lockKey = "lock:eat";  
    TimeSpan expiration = TimeSpan.FromSeconds(5);  
    //5 person eat something...  
    Parallel.For(0, 5, x =>  
    {  
        string person = $"person:{x}";  
        bool isLocked = AcquireLock(lockKey, person, expiration);  
  
        if (isLocked)  
        {  
            Console.WriteLine($"{person} begin eat food(with lock) at {DateTimeOffset.Now.ToUnixTimeMilliseconds()}.");  
        }  
        else  
        {  
            Console.WriteLine($"{person} can not eat food due to don't get the lock.");  
        }  
    });  
  
    Console.WriteLine("end");  
    Console.Read();  
} 
 运行代码后,我们可能会得到如下结果。

只有一个人可以拿到锁!其他人在等待。

虽然锁会被Redis自动移除,但是也没有很好的利用好共享资源!

因为当一个进程完成它的工作时,它应该让其他人使用资源,而不是无休止地等待!

所以我们也需要释放锁。

如何释放锁?为了释放锁,我们只是删除 Redis 中的项目!正如我们在创建锁时所采取的,我们需要匹配资源的唯一值,这样释放正确的锁会更加安全。匹配时,我们将删除锁,即解锁成功。否则解锁不成功。而我们需要一次执行getdel命令,所以我们将使用一个lua脚本来做这件事!

/// <summary>  
/// Releases the lock.  
/// </summary>  
/// <returns><c>true</c>, if lock was released, <c>false</c> otherwise.</returns>  
/// <param name="key">Key.</param>  
/// <param name="value">Value.</param>  
static bool ReleaseLock(string key, string value)  
{  
    string lua_script = @"  
    if (redis.call('GET', KEYS[1]) == ARGV[1]) then  
        redis.call('DEL', KEYS[1])  
        return true  
    else  
        return false  
    end  
    ";  
  
    try  
    {  
        var res = Connection.GetDatabase().ScriptEvaluate(lua_script,  
                                                   new RedisKey[] { key },  
                                                   new RedisValue[] { value });  
        return (bool)res;  
    }  
    catch (Exception ex)  
    {  
        Console.WriteLine($"ReleaseLock lock fail...{ex.Message}");  
        return false;  
    }  
} 

我们应该在进程结束时调用此方法。当一个进程拿到锁,由于某些原因没有释放锁时,其他进程就等不及它释放了。这时,其他进程应该继续进行。这是处理此场景的示例。

Parallel.For(0, 5, x =>  
{  
    string person = $"person:{x}";  
    var val = 0;  
    bool isLocked = AcquireLock(lockKey, person, expiration);  
    while (!isLocked && val <= 5000)  
    {  
        val += 250;  
        System.Threading.Thread.Sleep(250);  
        isLocked = AcquireLock(lockKey, person, expiration);  
    }  
  
    if (isLocked)  
    {  
        Console.WriteLine($"{person} begin eat food(with lock) at {DateTimeOffset.Now.ToUnixTimeMilliseconds()}.");  
        if (new Random().NextDouble() < 0.6)  
        {  
            Console.WriteLine($"{person} release lock {ReleaseLock(lockKey, person)}  {DateTimeOffset.Now.ToUnixTimeMilliseconds()}");  
        }  
        else  
        {  
            Console.WriteLine($"{person} do not release lock ....");  
        }  
    }  
    else  
    {  
        Console.WriteLine($"{person} begin eat food(without lock) at {DateTimeOffset.Now.ToUnixTimeMilliseconds()}.");  
    }  
}); 

运行示例后,您可能会得到以下结果。如您所见,第 3 个人和第 4 个人将在没有锁定的情况下继续运行。这是您可以在我的 github 页面中找到的源代码。

概括

.NET Core Redis 分布式锁,本文介绍了如何在 .NET Core 中使用 Redis 创建分布式锁。而且是基础版,您可以根据自己的业务进行改进。

 

注:用这个包

 

 

 

转载

https://www.muyuanzhan.com/tutorials/dotnet/9230.html

posted @ 2023-03-04 22:27  竹林听雨行  阅读(665)  评论(0编辑  收藏  举报