1、什麼是鎖
鎖是為了解決多執行緒或者多程式資源競爭的問題。
同一程式的多個執行緒資源競爭可以用lock解決。
lock 關鍵字可確保當一個執行緒位於程式碼的臨界區時,另一個執行緒不會進入該臨界區。 如果其他執行緒嘗試進入鎖定的程式碼,則它將一直等待(即被阻止),直到該物件被釋放。
class Test { //定義一個私有成員變數,用於Lock private static object lockobj = new object(); void DoSomething() { lock (lockobj) { //需要鎖定的程式碼塊 } } }
多程式之間解決資源競爭問題我們則需要引入分散式鎖。通過一個協調者來解決,通常的解決辦法是通過redis來解決,這裡不展開redis分散式鎖的討論。 接下來我們來聊聊如何自己實現一個分散式鎖(不依賴於redis)。
2、分散式鎖是個什麼鬼
分散式鎖是分散式、微服務中一個必然要討論的話題。他為的是解決多程式多執行緒資源競爭的問題。
下面我們以訂單系統下單扣減庫存為例聊一聊扣減庫存的問題。
三個客戶KA、KB、KC同時下單購買物品P1,請求通過負載均衡器分發到訂單服務A、訂單服務B、訂單服務C。這個時候三個服務同時要對資料庫中的P1物品判斷庫存是否充足。假設庫存剩餘10個,KA需要購買6個、KB需要購買6個、KC需要購買6個。
正常情況下服務A、B、C都查詢了庫存大於購買的數量,那麼三個服務都判斷可以下單。此時我們可以看到,她們都進行下單明顯剩餘庫存不足18個,那麼就會出現超賣的問題。那我們怎麼辦。我們第一時間會想到鎖,不過在分散式環境下程式自帶的Lock已經不能解決我們的問題。
訊息佇列也可以解決這個問題,不過這裡我們不討論,我們要討論的是用鎖來解決。
這個時候我們需要一個協調者來協調三個服務同時只能有一個請求進入下單程式碼塊。原理同本地鎖一樣(當一個執行緒位於程式碼的臨界區時,另一個執行緒不會進入該臨界區。 如果其他執行緒嘗試進入鎖定的程式碼,則它將一直等待(即被阻止),直到該物件被釋放)。另外我們還需要注意的是,如果鎖的擁有者出現問題,不能及時釋放鎖。那麼就會導致其他服務一直等待。那麼就會出現死鎖的問題,因此我們也必須一如超時機制。在我們預設的處理時間內不能釋放鎖則需要協調者自動釋放鎖。防止出現死鎖。
下面我們來看看微服務框架Anno是如何實現一個分散式鎖。
如果對Anno微服務框架不瞭解可以看這裡《【開源】.net微服務開發引擎Anno開源啦》
2、實現一個分散式鎖
using System; using System.Collections.Generic; using System.Text; using System.Threading.Tasks; namespace ConsoleTest { using Anno.Const; using Anno.EngineData; using Anno.Loader; using Anno.Rpc.Client; using Anno.Rpc.Server; using Autofac; public class DLockTest { public void Handle() { Init(); To: List<Task> ts = new List<Task>(); Console.WriteLine("請輸入執行緒數:"); int.TryParse(Console.ReadLine(), out int n); for (int i = 0; i < n; i++) { var task = Task.Factory.StartNew(() => { DLTest1("Anno"); }); ts.Add(task); //var taskXX = Task.Factory.StartNew(() => { DLTest1("Viper"); }); //ts.Add(taskXX); //var taskJJ = Task.Factory.StartNew(() => { DLTest1("Key001"); }); //ts.Add(taskJJ); } Task.WaitAll(ts.ToArray()); goto To; } private void DLTest1(string lk = "duyanming") { try { Console.WriteLine($"{DateTime.Now:yyyy-MM-dd HH:mm:ss:ffff} {System.Threading.Thread.CurrentThread.ManagedThreadId} DLTest1拉取鎖({lk})"); using (DLock dLock = new DLock(lk, 10000)) { Console.WriteLine($"{DateTime.Now:yyyy-MM-dd HH:mm:ss:ffff} {System.Threading.Thread.CurrentThread.ManagedThreadId} DLTest1進入鎖({lk})"); System.Threading.Thread.Sleep(50); } Console.WriteLine($"{DateTime.Now:yyyy-MM-dd HH:mm:ss:ffff} {System.Threading.Thread.CurrentThread.ManagedThreadId} DLTest1離開鎖({lk})"); } catch (Exception e) { Console.WriteLine(e.Message); } } void Init() { //SettingService.AppName = "DLockTest"; //SettingService.Local.IpAddress = "127.0.0.1"; //SettingService.Local.Port = 6660; IocLoader.GetAutoFacContainerBuilder().RegisterType(typeof(RpcConnectorImpl)).As(typeof(IRpcConnector)).SingleInstance(); IocLoader.Build(); DefaultConfigManager.SetDefaultConnectionPool(100, Environment.ProcessorCount * 2, 50); DefaultConfigManager.SetDefaultConfiguration("DLockTest", "127.0.0.1", 6660, false); } } }
GitHub地址:https://github.com/duyanming/Anno.Core/blob/master/test/ConsoleTest/DLockTest.cs
不同型別的鎖可以同時進入相互不影響
var task = Task.Factory.StartNew(() => { DLTest1("Anno"); }); ts.Add(task); var taskXX = Task.Factory.StartNew(() => { DLTest1("Viper"); }); ts.Add(taskXX); var taskJJ = Task.Factory.StartNew(() => { DLTest1("Key001"); }); ts.Add(taskJJ);
上圖我們開了12個程式同時進入DLTest1 方法,
using (DLock dLock = new DLock(lk, 10000))設定超時時間10秒。
關鍵程式碼:
private void DLTest1(string lk = "duyanming") { try { Console.WriteLine($"{DateTime.Now:yyyy-MM-dd HH:mm:ss:ffff} {System.Threading.Thread.CurrentThread.ManagedThreadId} DLTest1拉取鎖({lk})"); using (DLock dLock = new DLock(lk, 10000)) { Console.WriteLine($"{DateTime.Now:yyyy-MM-dd HH:mm:ss:ffff} {System.Threading.Thread.CurrentThread.ManagedThreadId} DLTest1進入鎖({lk})"); System.Threading.Thread.Sleep(50); } Console.WriteLine($"{DateTime.Now:yyyy-MM-dd HH:mm:ss:ffff} {System.Threading.Thread.CurrentThread.ManagedThreadId} DLTest1離開鎖({lk})"); } catch (Exception e) { Console.WriteLine(e.Message); } }
所有原始碼都可以在 Anno中找到。
Anno核心原始碼:https://github.com/duyanming/Anno.Core
Viper示例專案:https://github.com/duyanming/Viper
體驗地址:http://140.143.207.244/Home/Login
QQ交流群:478399354