分散式鎖的核心其實就是採用一個集中式的服務,然後多個應用節點進行搶佔式鎖定來進行實現,今天介紹如何採用Redis作為基礎服務,實現一個分散式鎖的類庫,本方案不考慮 Redis 叢集多節點問題,如果引入叢集多節點問題,會導致解決成本大幅上升,因為 Redis 單節點就可以很容易的處理10萬並行量了,這對於日常開發中 99% 的專案足夠使用了。
目標如下:
程式碼整體結構圖
建立 DistributedLock 類庫,然後定義介面檔案 IDistributedLock ,方便我們後期擴充套件其他分散式鎖的實現。
namespace DistributedLock
{
public interface IDistributedLock
{
/// <summary>
/// 獲取鎖
/// </summary>
/// <param name="key">鎖的名稱,不可重複</param>
/// <param name="expiry">失效時長</param>
/// <param name="semaphore">號誌</param>
/// <returns></returns>
public IDisposable Lock(string key, TimeSpan expiry = default, int semaphore = 1);
/// <summary>
/// 嘗試獲取鎖
/// </summary>
/// <param name="key">鎖的名稱,不可重複</param>
/// <param name="expiry">失效時長</param>
/// <param name="semaphore">號誌</param>
/// <returns></returns>
public IDisposable? TryLock(string key, TimeSpan expiry = default, int semaphore = 1);
}
}
建立 DistributedLock.Redis 類庫,安裝下面兩個 Nuget 包
StackExchange.Redis
Microsoft.Extensions.Options
定義設定模型 RedisSetting
namespace DistributedLock.Redis.Models
{
public class RedisSetting
{
public string Configuration { get; set; }
public string InstanceName { get; set; }
}
}
定義 RedisLockHandle
using StackExchange.Redis;
namespace DistributedLock.Redis
{
public class RedisLockHandle : IDisposable
{
public IDatabase Database { get; set; }
public string LockKey { get; set; }
public void Dispose()
{
try
{
Database.LockRelease(LockKey, "123456");
}
catch
{
}
GC.SuppressFinalize(this);
}
}
}
實現 RedisLock
using DistributedLock.Redis.Models;
using Microsoft.Extensions.Options;
using StackExchange.Redis;
using System.Security.Cryptography;
using System.Text;
namespace DistributedLock.Redis
{
public class RedisLock : IDistributedLock
{
private readonly ConnectionMultiplexer connectionMultiplexer;
private readonly RedisSetting redisSetting;
public RedisLock(IOptionsMonitor<RedisSetting> config)
{
connectionMultiplexer = ConnectionMultiplexer.Connect(config.CurrentValue.Configuration);
redisSetting = config.CurrentValue;
}
/// <summary>
/// 獲取鎖
/// </summary>
/// <param name="key">鎖的名稱,不可重複</param>
/// <param name="expiry">失效時長</param>
/// <param name="semaphore">號誌</param>
/// <returns></returns>
public IDisposable Lock(string key, TimeSpan expiry = default, int semaphore = 1)
{
if (expiry == default)
{
expiry = TimeSpan.FromMinutes(1);
}
var endTime = DateTime.UtcNow + expiry;
RedisLockHandle redisLockHandle = new();
StartTag:
{
for (int i = 0; i < semaphore; i++)
{
var keyMd5 = redisSetting.InstanceName + Convert.ToHexString(MD5.HashData(Encoding.UTF8.GetBytes(key + i)));
try
{
var database = connectionMultiplexer.GetDatabase();
if (database.LockTake(keyMd5, "123456", expiry))
{
redisLockHandle.LockKey = keyMd5;
redisLockHandle.Database = database;
return redisLockHandle;
}
}
catch
{
}
}
if (redisLockHandle.LockKey == default)
{
if (DateTime.UtcNow < endTime)
{
Thread.Sleep(1000);
goto StartTag;
}
else
{
throw new Exception("獲取鎖" + key + "超時失敗");
}
}
}
return redisLockHandle;
}
public IDisposable? TryLock(string key, TimeSpan expiry = default, int semaphore = 1)
{
if (expiry == default)
{
expiry = TimeSpan.FromMinutes(1);
}
for (int i = 0; i < semaphore; i++)
{
var keyMd5 = redisSetting.InstanceName + Convert.ToHexString(MD5.HashData(Encoding.UTF8.GetBytes(key + i)));
try
{
var database = connectionMultiplexer.GetDatabase();
if (database.LockTake(keyMd5, "123456", expiry))
{
RedisLockHandle redisLockHandle = new()
{
LockKey = keyMd5,
Database = database
};
return redisLockHandle;
}
}
catch
{
}
}
return null;
}
}
}
定義 ServiceCollectionExtensions
using DistributedLock.Redis.Models;
using Microsoft.Extensions.DependencyInjection;
namespace DistributedLock.Redis
{
public static class ServiceCollectionExtensions
{
public static void AddRedisLock(this IServiceCollection services, Action<RedisSetting> action)
{
services.Configure(action);
services.AddSingleton<IDistributedLock, RedisLock>();
}
}
}
使用時只要在組態檔中加入 redis 連線字串資訊,然後注入服務即可。
appsettings.json
{
"ConnectionStrings": {
"redisConnection": "127.0.0.1,Password=123456,DefaultDatabase=0"
}
}
注入範例程式碼:
//註冊分散式鎖 Redis模式
builder.Services.AddRedisLock(options =>
{
options.Configuration = builder.Configuration.GetConnectionString("redisConnection")!;
options.InstanceName = "lock";
});
使用範例
using DistributedLock;
using Microsoft.AspNetCore.Mvc;
namespace WebAPI.Controllers
{
[Route("[controller]")]
[ApiController]
public class DemoController : ControllerBase
{
private readonly IDistributedLock distLock;
public DemoController(IDistributedLock distLock)
{
this.distLock = distLock;
}
[HttpGet("Test")]
public void Test()
{
//鎖定鍵只要是一個字串即可,可以簡單理解為鎖的標識名字,可以是使用者名稱,使用者id ,訂單id 等等,根據業務需求自己定義
string lockKey = "xx1";
using (distLock.Lock(lockKey))
{
//程式碼塊同時只有一個請求可以進來執行,其餘沒有獲取到鎖的全部處於等待狀態
//鎖定時常1分鐘,1分鐘後無論程式碼塊是否執行完成鎖都會被釋放,同時等待時常也為1分鐘,1分鐘後還沒有獲取到鎖,則丟擲異常
}
using (distLock.Lock(lockKey, TimeSpan.FromSeconds(300)))
{
//程式碼塊同時只有一個請求可以進來執行,其餘沒有獲取到鎖的全部處於等待狀態
//鎖定時常300秒,300秒後無論程式碼塊是否執行完成鎖都會被釋放,同時等待時常也為300秒,300秒後還沒有獲取到鎖,則丟擲異常
}
using (distLock.Lock(lockKey, TimeSpan.FromSeconds(300), 5))
{
//程式碼塊同時有五個請求可以進來執行,其餘沒有獲取到鎖的全部處於等待狀態
//鎖定時常300秒,300秒後無論程式碼塊是否執行完成鎖都會被釋放,同時等待時常也為300秒,300秒後還沒有獲取到鎖,則丟擲異常
//該程式碼塊有5個請求同時拿到鎖,簽發出去的5把鎖,每把鎖的時間都是單獨計算的,並非300秒後 5個鎖會全部同時釋放,可能只會釋放 2個或3個,釋放之後心的請求又可以獲取到,總之最多隻有5個請求可以進入
}
var lockHandle1 = distLock.TryLock(lockKey);
if (lockHandle1 != null)
{
//程式碼塊同時只有一個請求可以進來執行,其餘沒有獲取到鎖的直接為 null 不等待,也不執行
//鎖定時常1分鐘,1分鐘後無論程式碼塊是否執行完成鎖都會被釋放
}
var lockHandle2 = distLock.TryLock(lockKey, TimeSpan.FromSeconds(300));
if (lockHandle2 != null)
{
//程式碼塊同時只有一個請求可以進來執行,其餘沒有獲取到鎖的直接為 null 不等待,也不執行
//鎖定時常300秒,300秒後無論程式碼塊是否執行完成鎖都會被釋放
}
var lockHandle3 = distLock.TryLock(lockKey, TimeSpan.FromSeconds(300), 5);
if (lockHandle3 != null)
{
//程式碼塊同時有五個請求可以進來執行,其餘沒有獲取到鎖的直接為 null 不等待,也不執行
//鎖定時常300秒,300秒後無論程式碼塊是否執行完成鎖都會被釋放
//該程式碼塊有5個請求同時拿到鎖,簽發出去的5把鎖,每把鎖的時間都是單獨計算的,並非300秒後 5個鎖會全部同時釋放,可能只會釋放 2個或3個,釋放之後心的請求又可以獲取到,總之最多隻有5個請求可以進入
}
}
}
}
至此關於 自己動手基於 Redis 實現一個 .NET 的分散式鎖類庫 就講解完了,有任何不明白的,可以在文章下面評論或者私信我,歡迎大家積極的討論交流,有興趣的朋友可以關注我目前在維護的一個 .NET 基礎框架專案,專案地址如下
https://github.com/berkerdong/NetEngine.git
https://gitee.com/berkerdong/NetEngine.git