北京遇上西雅圖之:當記賬系統遇上併發

目前在開發的系統中有個結算的邏輯,每張訂單到了賬期日後,平臺會給商家進行結算。涉及到賬戶方面的操作包括,平臺賬戶餘額的扣減和商戶賬戶餘額的增加,以及賬戶流水的記錄。

像這個場景,如果不考慮併發的話,那麼很容易出現數據不一致,導致記賬混亂。 當然,這是比(xiāng)較(dāng)要命的!

那怎麼解決這種併發呢?

北京遇上西雅圖之:當記賬系統遇上併發

北京遇上西雅圖之:當記賬系統遇上併發

為了便於描述,我們把場景簡單化:db裡有個t_info_meraccount表,記錄平臺及商戶的賬戶資訊; 程式邏輯為讀取出指定的賬戶記錄,修改其某個欄位的值。 在這個場景下我們看併發如何處理。

想必大家都可以想到了,用lock。lock將語句塊標記為臨界區,獲取給定物件的互斥鎖,然後執行語句塊,執行完成後釋放鎖。 這樣可以控制程序內多執行緒的併發。

這裡, 我再說另一種也許更好的方法————藉助一個時間戳用樂觀鎖。先看程式碼邏輯:

public void MyBiz(string name = “”){ Stopwatch watch = new Stopwatch(); watch。Start(); int loops = 0;//用以記錄迴圈次數 int i = 0; while (i == 0)//在執行db的update時,成功update會返回1,否則(非異常情況下)會返回0。所以,每當返回0時,我們就嘗試再次執行整個邏輯 { loops++; #region 業務邏輯 // 1。 讀 var dal = new GateWay。DAL。PriceDal。PriceDAL(); string mercode = “000001”; t_info_meraccount accountModel = dal。GetAcInfo(mercode); if (name == “”) { accountModel。MerName = accountModel。MerName + loops; } else { accountModel。MerName = name; } // 為了模擬併發,這裡讓執行緒隨機sleep Thread。Sleep(new Random()。Next(10, 1000)); // 2。 寫 i = Update(accountModel); #endregion if (i == 0) { Console。WriteLine(Thread。CurrentThread。Name + “ 遭遇i=0,接著重試。。。”); } } watch。Stop(); Console。WriteLine(Thread。CurrentThread。Name + “ 執行次數:” + loops + “ duation:” + watch。ElapsedMilliseconds);}private static int Update(t_info_meraccount model){ string sql = “update t_info_meraccount set MerName=@name,LastTime=@LastTime where AcCode=@AcCode and LastTime=@LastTime1”; int i = 0; using (var conn = ConnUtility。GateWayConntion) { conn。Open(); i = conn。Execute(sql, new { name = model。MerName, LastTime = CommonDataType_DateTime。GetTimeStamp(false), AcCode = model。AcCode, LastTime1 = model。LastTime }); return i; }}

可以看到,程式碼邏輯即是先

select

一條記錄,然後

update

改其MerName屬性值, 然後將這個修改持久化到db。

也可以看到,這段程式裡利用了時間戳。表t_info_meraccount裡有個時間戳欄位LastTime varchar(20)。

程式在對錶執行

update操作

時,除了update所需的欄位外,還要update這個LastTime;另外,

where

子句中除了必要的AcCode條件外,再追加一個LastTime。

可以看到,當LastTime被其他執行緒/程序更改後就匹配不上了,就會update失敗,從而返回0。 那麼這時, while迴圈繼續, 重複執行select和update,直到update返回1為止。

是否合理呢? 我們來寫個testcase,模擬多執行緒併發操作:

[TestMethod]public void TestConcurrency(){ MyBiz(“20161129測試商戶”); Thread。Sleep(1000); List ths = new List(); for (int i = 0; i < 10; i++) { var thread = new Thread(() => { try { MyBiz(); } catch (Exception ex) { Console。WriteLine(Thread。CurrentThread。Name + “——” + ex。Message); } }); thread。Name = “thread” + i; ths。Add(thread); } ths。ForEach(t => t。Start()); Thread。Sleep(10 * 1000); //Thread。Sleep(1000); //Test(“20161129測試商戶”);}

測試輸出:

> 執行次數:1 duation:1127> thread1 執行次數:1 duation:162> thread7 遭遇i=0,接著重試。。。> thread8 遭遇i=0,接著重試。。。> thread9 遭遇i=0,接著重試。。。> thread6 遭遇i=0,接著重試。。。> thread2 遭遇i=0,接著重試。。。> thread9 執行次數:2 duation:140> thread8 遭遇i=0,接著重試。。。> thread6 遭遇i=0,接著重試。。。> thread7 遭遇i=0,接著重試。。。> thread2 遭遇i=0,接著重試。。。> thread3 遭遇i=0,接著重試。。。> thread3 執行次數:2 duation:699> thread5 遭遇i=0,接著重試。。。> thread7 遭遇i=0,接著重試。。。> thread2 遭遇i=0,接著重試。。。> thread6 遭遇i=0,接著重試。。。> thread8 遭遇i=0,接著重試。。。> thread0 遭遇i=0,接著重試。。。> thread4 遭遇i=0,接著重試。。。> thread5 執行次數:2 duation:1146> thread2 遭遇i=0,接著重試。。。> thread7 遭遇i=0,接著重試。。。> thread0 遭遇i=0,接著重試。。。> thread4 遭遇i=0,接著重試。。。> thread2 執行次數:5 duation:1398> thread7 遭遇i=0,接著重試。。。> thread6 遭遇i=0,接著重試。。。> thread8 遭遇i=0,接著重試。。。> thread7 執行次數:6 duation:1630> thread0 遭遇i=0,接著重試。。。> thread4 遭遇i=0,接著重試。。。> thread0 執行次數:4 duation:2081> thread4 遭遇i=0,接著重試。。。> thread6 遭遇i=0,接著重試。。。> thread8 遭遇i=0,接著重試。。。> thread8 執行次數:6 duation:2587> thread6 遭遇i=0,接著重試。。。> thread4 遭遇i=0,接著重試。。。> thread6 執行次數:7 duation:2825> thread4 執行次數:6 duation:3328

查詢資料庫,t_info_meraccount裡AcCode = “000001”的賬戶的MerName=“20161129測試商戶1222564676”。 可見,驗證了我們程式碼是ok的。

這種利用資料庫樂觀鎖的方式也有效地處理了併發。 那麼,和lock相比,它的優勢在哪裡呢?lock只能控制同一程序內執行緒。 當這段程式部署在不同的主機上時,lock就顯得疲軟了。 而後者這個方案,正好解決了多機部署時的併發。

最後,附上時間戳的生成演算法:

///

/// 獲取當前時間戳 /// /// 為真時獲取10位時間戳,為假時獲取13位時間戳。 /// public static string GetTimeStamp(bool bflag = true){ TimeSpan ts = DateTime。UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0); string ret = string。Empty; if (bflag) ret = Convert。ToInt64(ts。TotalSeconds)。ToString(); else ret = Convert。ToInt64(ts。TotalMilliseconds)。ToString(); return ret;}