MASA Framework的分散式鎖設計

語言: CN / TW / HK

前言

什麼是鎖?什麼是分散式鎖?它們之間有什麼樣的關係?

什麼是鎖

加鎖(lock)是2018年公佈的計算機科學技術名詞,是指將控制變數置位,控制共享資源不能被其他執行緒訪問。通過加鎖,可以確保在同一時刻只有一個執行緒在訪問被鎖住的程式碼片段,我們在單機部署時可使用最簡單的加鎖完成資源的獨享,如:

public class Program
{
    private static readonly object Obj = new { };

    public static void Main()
    {
        lock (obj)
        {
            //同一時刻只有一個執行緒可以訪問
        }
    }
}

什麼是分散式鎖

但隨著業務發展的需要,原單體單機部署的系統被部署成分散式集群系統後,原來的併發控制策略失效,為了解決這個問題就需要引入分散式鎖,那分散式鎖應該具備哪些條件?

  • 原子性:在分散式環境下,一個方法在同一個時間點只能被一臺機器下的一個執行緒所執行,防止資料資源的併發訪問,避免資料不一致情況
  • 高可用:具備自動失效機制,防止死鎖,獲取鎖後如果出現錯誤,並且無法釋放鎖,則使用租約一段時間後自動釋放鎖
  • 阻塞性:具備非阻塞鎖特性(沒有獲取到鎖時直接返回獲取鎖失敗,不會長時間因等待鎖導致阻塞)
  • 高效能:高效能的獲取鎖與釋放鎖
  • 可重入性:具備可重入特性,在同一執行緒外層函式獲得鎖之後,內層方法會自動獲取鎖

實現

分散式鎖是特定於實現的,目前MasaFramework提供了兩個實現,分別是LocalMedallion,下面會介紹如何配置並使用它們

本地鎖

是基於SemaphoreSlim實現的,它不是真正的分散式鎖,我們建議你在開發和測試環境中使用它,不需要聯網也不會與其他人衝突

Medallion

是基於DistributedLock實現的分散式鎖,它提供了很多種技術的實現,包括Microsoft SQL ServerPostgresqlMySQL 或 MariaDBOracleRedisAzure blobApache ZooKeeper鎖檔案作業系統全域性WaitHandles(Windows),我們只需要任選一種實現即可,目前Medallion提供的分散式鎖並不支援可重入性,點選瞭解原因

快速入門

本地鎖單應用鎖為例:

  1. 新建ASP.NET Core 空專案Assignment.DistributedLock.Local,並安裝Masa.Contrib.Data.DistributedLock.Local
dotnet new web -o Assignment.DistributedLock.Local
cd Assignment.DistributedLock.Local
dotnet add package Masa.Contrib.Data.DistributedLock.Local --version 0.6.0-preview.10
  1. 註冊鎖,修改類Program
builder.Services.AddLocalDistributedLock();//註冊本地鎖
  1. 如何使用鎖?修改類Program
app.MapGet("lock", (IDistributedLock distributedLock) =>
{
    using var @lock = distributedLock.TryGet("test");//獲取鎖
    if (@lock != null)
    {
        //todo: 獲取鎖成功
        return "success";
    }
    return "獲取超時";
});

通過DI獲取IDistributedLock,並通過TryGet方法獲取鎖,如果獲取鎖失敗,則返回null,如果返回到的物件不為null,則表明獲取鎖成功,最後在獲取鎖成功後寫自己的業務程式碼即可

TryGet方法擁有以下引數

  • key (string, 必須): 鎖的唯一名稱,可通過key來訪問不同的資源,執行不同的業務
  • timeout (TimeSpan): 等待獲取鎖的最大超時時間. 預設值為: TimeSpan.Zero(代表如果鎖已經被另一個應用程式擁有, 它不會等待.)

TryGetAsync方法除了擁有TryGet的所有引數之外,還擁有以下引數

  • cancellationToken: 取消令牌可在觸發後取消操作

如果你選擇使用Medallion,只需要選擇一種技術實現,並根據Readme註冊鎖即可,在使用鎖上是沒有區別的

如何擴充套件其它的分散式鎖

  1. 新建類庫Masa.Contrib.Data.DistributedLock.{分散式鎖名},並新增引用Masa.BuildingBlocks.Data.csproj

  2. 新建分散式鎖實現類DefaultDistributedLock,並實現IDistributedLock

public class DefaultDistributedLock : IDistributedLock
{
    public IDisposable? TryGet(string key, TimeSpan timeout = default)
    {
        // 獲取鎖失敗則返回null,當資源被釋放時,主動釋放鎖, 無需人為手動釋放
        throw new NotImplementedException();
    }

    public Task<IAsyncDisposable?> TryGetAsync(string key, TimeSpan timeout = default, CancellationToken cancellationToken = default)
    {
        //獲取鎖失敗則返回null,當資源被釋放時,主動釋放鎖, 無需人為手動釋放
        throw new NotImplementedException();
    }
}
  1. 新建類ServiceCollectionExtensions,註冊分散式鎖到服務集合
public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddDistributedLock(this IServiceCollection services, Action<MedallionBuilder> builder)
    {
        services.TryAddSingleton<IDistributedLock, DefaultDistributedLock>();
        return services;
    }
}

小知識

為什麼TryGetTryGetAsync方法的返回型別分別是IDisposableIAsyncDisposable

我們希望使用鎖可以足夠的簡單,在使用完鎖之後可以自動釋放鎖,而不是必須手動釋放,當返回型別為IDisposableIAsyncDisposable時,使用完畢後會觸發DisposeDisposeAsync,這樣一來就可以使得開發者可以忽略釋放鎖的邏輯

以本地鎖為例:

public class DefaultLocalDistributedLock : IDistributedLock
{
    private readonly MemoryCache<string, SemaphoreSlim> _localObjects = new();

    public IDisposable? TryGet(string key, TimeSpan timeout = default)
    {
        var semaphore = GetSemaphoreSlim(key);

        if (!semaphore.Wait(timeout))
        {
            return null;
        }

        return new DisposeAction(semaphore);
    }

    //todo: 以下省略 TryGetAsync 方法

    private SemaphoreSlim GetSemaphoreSlim(string key)
    {
        ArgumentNullOrWhiteSpaceException.ThrowIfNullOrWhiteSpace(key);
        return _localObjects.GetOrAdd(key, _ => new SemaphoreSlim(1, 1));
    }
}

internal class DisposeAction : IDisposable, IAsyncDisposable
{
    private readonly SemaphoreSlim _semaphore;

    public DisposeAction(SemaphoreSlim semaphore) => _semaphore = semaphore;

    public ValueTask DisposeAsync()
    {
        _semaphore.Release();
        return ValueTask.CompletedTask;
    }

    public void Dispose() => _semaphore.Release();
}

本章原始碼

Assignment09

http://github.com/zhenlei520/MasaFramework.Practice

開源地址

MASA.Framework:http://github.com/masastack/MASA.Framework

如果你對我們的 MASA Framework 感興趣,無論是程式碼貢獻、使用、提 Issue,歡迎聯絡我們

  • WeChat:MasaStackTechOps
  • QQ:7424099