基於 Redis 的分佈式鎖和 Redlock 算法
背景
在單進程的系統中,當存在多個線程可以同時改變某個變量(可變共享變量)時,就需要對變量或代碼塊做同步,使其在修改這種變量時能夠線性執行消除併發修改變量。
而同步的本質是通過鎖來實現的。爲了實現多個線程在一個時刻同一個代碼塊只能有一個線程可執行,那麼需要在某個地方做個標記,這個標記必須每個線程都能看到,當標記不存在時可以設置該標記,其餘後續線程發現已經有標記了則等待擁有標記的線程結束同步代碼塊取消標記後再去嘗試設置標記。這個標記可以理解爲鎖。
不同地方實現鎖的方式也不一樣,只要能滿足所有線程都能看得到標記即可。如 Java 中 synchronize 是在對象頭設置標記,Lock 接口的實現類基本上都只是某一個 volitile 修飾的 int 型變量其保證每個線程都能擁有對該 int 的可見性和原子修改,linux 內核中也是利用互斥量或信號量等內存數據做標記。
除了利用內存數據做鎖其實任何互斥的都能做鎖(只考慮互斥情況),如流水錶中流水號與時間結合做冪等校驗可以看作是一個不會釋放的鎖,或者使用某個文件是否存在作爲鎖等。只需要滿足在對標記進行修改能保證原子性和內存可見性即可。
概念
1 什麼是分佈式?
分佈式的 CAP 理論告訴我們:
任何一個分佈式系統都無法同時滿足一致性(Consistency)、可用性(Availability)和分區容錯性(Partition tolerance),最多隻能同時滿足兩項。
目前很多大型網站及應用都是分佈式部署的,分佈式場景中的數據一致性問題一直是一個比較重要的話題。基於 CAP 理論,很多系統在設計之初就要對這三者做出取捨。在互聯網領域的絕大多數的場景中,都需要犧牲強一致性來換取系統的高可用性,系統往往只需要保證最終一致性。
場景
分佈式場景
此處主要指集羣模式下,多個相同服務同時開啓.
在許多的場景中,我們爲了保證數據的最終一致性,需要很多的技術方案來支持,比如分佈式事務、分佈式鎖等。很多時候我們需要保證一個方法在同一時間內只能被同一個線程執行。在單機環境中,通過 Java 提供的併發 API 我們可以解決,但是在分佈式環境下,就沒有那麼簡單啦。
● 分佈式與單機情況下最大的不同在於其不是多線程而是多進程。
● 多線程由於可以共享堆內存,因此可以簡單的採取內存作爲標記存儲位置。而進程之間甚至可能都不在同一臺物理機上,因此需要將標記存儲在一個所有進程都能看到的地方。
什麼是分佈式鎖?
● 當在分佈式模型下,數據只有一份(或有限制),此時需要利用鎖的技術控制某一時刻修改數據的進程數。
● 與單機模式下的鎖不僅需要保證進程可見,還需要考慮進程與鎖之間的網絡問題。(我覺得分佈式情況下之所以問題變得複雜,主要就是需要考慮到網絡的延時和不可靠。。。一個大坑)
● 分佈式鎖還是可以將標記存在內存,只是該內存不是某個進程分配的內存而是公共內存如 Redis、Memcache。至於利用數據庫、文件等做鎖與單機的實現是一樣的,只要保證標記能互斥就行。
2 我們需要怎樣的分佈式鎖?
可以保證在分佈式部署的應用集羣中,同一個方法在同一時間只能被一臺機器上的一個線程執行。
這把鎖要是一把可重入鎖(避免死鎖)
這把鎖最好是一把阻塞鎖(根據業務需求考慮要不要這條)
這把鎖最好是一把公平鎖(根據業務需求考慮要不要這條)
有高可用的獲取鎖和釋放鎖功能
獲取鎖和釋放鎖的性能要好
代碼實現
代碼實現
public interface IDistributedLock
{
ILockResult Lock(string resourceKey);
ILockResult Lock(string resourceKey, TimeSpan expiryTime);
ILockResult Lock(string resourceKey, TimeSpan expiryTime, TimeSpan waitTime, TimeSpan retryTime);
ILockResult Lock(string resourceKey, TimeSpan expiryTime, TimeSpan waitTime, TimeSpan retryTime, CancellationToken cancellationToken);
Task<ILockResult> LockAsync(string resourceKey);
Task<ILockResult> LockAsync(string resourceKey, TimeSpan expiryTime);
Task<ILockResult> LockAsync(string resourceKey, TimeSpan expiryTime, TimeSpan waitTime, TimeSpan retryTime);
Task<ILockResult> LockAsync(string resourceKey, TimeSpan expiryTime, TimeSpan waitTime, TimeSpan retryTime, CancellationToken cancellationToken);
}
public interface ILockResult : IDisposable
{
string LockId { get; }
bool IsAcquired { get; }
int ExtendCount { get; }
}
class EndPoint:RedLock.RedisLockEndPoint
{
private readonly string _connectionString;
public EndPoint(string connectionString)
{
_connectionString = connectionString;
//139.196.40.252,password=xstudio,defaultDatabase=9
var connection = connectionString.Split(',');
var dict = new Dictionary<string, string>();
foreach (var item in connection)
{
var keypar = item.Split('=');
if (keypar.Length>1)
{
dict[keypar[0]] = keypar[1];
}
}
this.EndPoint = new System.Net.DnsEndPoint(connection[0], 6379);
if (dict.TryGetValue("password", out string password))
{
this.Password = password;
}
if (dict.TryGetValue("defaultDatabase", out string defaultDatabase) && int.TryParse(defaultDatabase,out int database))
{
RedisDatabase = database;
}
}
}
[Export(typeof(IDistributedLock))]
class InnerLock : IDistributedLock
{
private static Lazy<RedLock.RedisLockFactory> _factory;
static InnerLock()
{
_factory = new Lazy<RedisLockFactory>(() => new RedisLockFactory(new EndPoint(ConfigurationManager.AppSettings["Redis"])), System.Threading.LazyThreadSafetyMode.ExecutionAndPublication);
}
public ILockResult Lock(string resourceKey)
{
return new LockResult(_factory.Value.Create(resourceKey, TimeSpan.FromDays(1)));
}
public ILockResult Lock(string resourceKey, TimeSpan expiryTime)
{
return new LockResult(_factory.Value.Create(resourceKey, expiryTime));
}
public ILockResult Lock(string resourceKey, TimeSpan expiryTime, TimeSpan waitTime, TimeSpan retryTime)
{
return new LockResult(_factory.Value.Create(resourceKey, expiryTime, waitTime, retryTime));
}
public ILockResult Lock(string resourceKey, TimeSpan expiryTime, TimeSpan waitTime, TimeSpan retryTime, CancellationToken cancellationToken)
{
return new LockResult(_factory.Value.Create(resourceKey, expiryTime, waitTime, retryTime, cancellationToken));
}
public async Task<ILockResult> LockAsync(string resourceKey)
{
var result = await _factory.Value.CreateAsync(resourceKey, TimeSpan.FromDays(1));
return new LockResult(result);
}
public async Task<ILockResult> LockAsync(string resourceKey, TimeSpan expiryTime)
{
var result = await _factory.Value.CreateAsync(resourceKey, expiryTime);
return new LockResult(result);
}
public async Task<ILockResult> LockAsync(string resourceKey, TimeSpan expiryTime, TimeSpan waitTime, TimeSpan retryTime)
{
var result = await _factory.Value.CreateAsync(resourceKey, expiryTime, waitTime, retryTime);
return new LockResult(result);
}
public async Task<ILockResult> LockAsync(string resourceKey, TimeSpan expiryTime, TimeSpan waitTime, TimeSpan retryTime, CancellationToken cancellationToken)
{
var result = await _factory.Value.CreateAsync(resourceKey, expiryTime, waitTime, retryTime, cancellationToken);
return new LockResult(result);
}
}
class LockResult : ILockResult
{
private IRedisLock _lock;
public LockResult(IRedisLock redisLock)
{
_lock = redisLock;
}
public string LockId => _lock.LockId;
public bool IsAcquired => _lock.IsAcquired;
public int ExtendCount => _lock.ExtendCount;
public void Dispose()
{
_lock.Dispose();
}
}
開源地址
https://github.com/samcook/RedLock.net
https://github.com/StackExchange/StackExchange.Redis/
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/qpfE4iho4S-iGo52tKVEXQ