目前在開發(fā)的系統(tǒng)中有個(gè)結(jié)算的邏輯妈嘹,每張訂單到了賬期日后柳琢,平臺(tái)會(huì)給商家進(jìn)行結(jié)算。涉及到賬戶方面的操作包括润脸,平臺(tái)賬戶余額的扣減和商戶賬戶余額的增加染厅,以及賬戶流水的記錄。
像這個(gè)場(chǎng)景津函,如果不考慮并發(fā)的話肖粮,那么很容易出現(xiàn)數(shù)據(jù)不一致,導(dǎo)致記賬混亂尔苦。 當(dāng)然涩馆,這是比(xiāng)較(dāng)要命的!
那怎么解決這種并發(fā)呢允坚?
為了便于描述魂那,我們把場(chǎng)景簡(jiǎn)單化:db里有個(gè)Account表,記錄平臺(tái)及商戶的賬戶信息稠项; 程序邏輯為讀取出指定的賬戶記錄涯雅,修改其某個(gè)字段的值。 在這個(gè)場(chǎng)景下我們看并發(fā)如何處理展运。
想必大家都可以想到了活逆,用lock。lock將語(yǔ)句塊標(biāo)記為臨界區(qū)拗胜,獲取給定對(duì)象的互斥鎖蔗候,然后執(zhí)行語(yǔ)句塊,執(zhí)行完成后釋放鎖埂软。 這樣可以控制進(jìn)程內(nèi)多線程的并發(fā)锈遥。
這里, 我再說另一種也許更好的方法————借助一個(gè)時(shí)間戳勘畔。先看代碼邏輯:
public void MyBiz(string name = "")
{
Stopwatch watch = new Stopwatch();
watch.Start();
int loops = 0;//用以記錄循環(huán)次數(shù)
int i = 0;
while (i == 0)//在執(zhí)行db的update時(shí)所灸,成功update會(huì)返回1,否則(非異常情況下)會(huì)返回0炫七。所以爬立,每當(dāng)返回0時(shí),我們就嘗試再次執(zhí)行整個(gè)邏輯
{
loops++;
#region 業(yè)務(wù)邏輯
// 1. 讀
var dal = new GateWay.DAL.PriceDal.PriceDAL();
string mercode = "000001";
t_info_meraccount accountModel = dal.GetAcInfo(mercode, "1", "3");
if (name == "")
{
accountModel.MerName = accountModel.MerName + loops;
}
else
{
accountModel.MerName = name;
}
// 為了模擬并發(fā)诉字,這里讓線程隨機(jī)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 + " 執(zhí)行次數(shù):" + 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;
}
}
可以看到,代碼邏輯即是先取出一條記錄壤圃,然后修改其MerName屬性值陵霉, 然后將這個(gè)修改持久化到db。
也可以看到伍绳,這段程序里利用了時(shí)間戳踊挠。在表t_info_meraccount里有個(gè)時(shí)間戳字段LastTime varchar(20)。 在對(duì)表執(zhí)行update時(shí)冲杀,where子句除了必要的AcCode條件外效床,再追加一個(gè)LastTime∪ㄋ可以看到剩檀,當(dāng)LastTime被其他線程更改后就匹配不上了,就會(huì)update失敗旺芽,從而返回0沪猴。 那么這時(shí), while循環(huán)繼續(xù)采章, 直到返回1為止运嗜。
是否合理呢? 我們來寫個(gè)testcase悯舟,模擬多線程并發(fā)操作:
[TestMethod]
public void TestConcurrency()
{
MyBiz("測(cè)試商戶");
Thread.Sleep(1000);
List<Thread> ths = new List<Thread>();
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("測(cè)試商戶");
}
測(cè)試輸出:
執(zhí)行次數(shù):1 duation:1127
thread1 執(zhí)行次數(shù):1 duation:162
thread7 遭遇i=0担租,接著重試...
thread8 遭遇i=0,接著重試...
thread9 遭遇i=0抵怎,接著重試...
thread6 遭遇i=0奋救,接著重試...
thread2 遭遇i=0,接著重試...
thread9 執(zhí)行次數(shù):2 duation:140
thread8 遭遇i=0反惕,接著重試...
thread6 遭遇i=0菠镇,接著重試...
thread7 遭遇i=0,接著重試...
thread2 遭遇i=0承璃,接著重試...
thread3 遭遇i=0利耍,接著重試...
thread3 執(zhí)行次數(shù):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 執(zhí)行次數(shù):2 duation:1146
thread2 遭遇i=0进萄,接著重試...
thread7 遭遇i=0捻脖,接著重試...
thread0 遭遇i=0锐峭,接著重試...
thread4 遭遇i=0,接著重試...
thread2 執(zhí)行次數(shù):5 duation:1398
thread7 遭遇i=0可婶,接著重試...
thread6 遭遇i=0沿癞,接著重試...
thread8 遭遇i=0,接著重試...
thread7 執(zhí)行次數(shù):6 duation:1630
thread0 遭遇i=0矛渴,接著重試...
thread4 遭遇i=0椎扬,接著重試...
thread0 執(zhí)行次數(shù):4 duation:2081
thread4 遭遇i=0,接著重試...
thread6 遭遇i=0具温,接著重試...
thread8 遭遇i=0蚕涤,接著重試...
thread8 執(zhí)行次數(shù):6 duation:2587
thread6 遭遇i=0,接著重試...
thread4 遭遇i=0铣猩,接著重試...
thread6 執(zhí)行次數(shù):7 duation:2825
thread4 執(zhí)行次數(shù):6 duation:3328
db里MerName字段的初始值是“測(cè)試商戶”揖铜,執(zhí)行testcase后值是“測(cè)試商戶1222564676”。 可見达皿,驗(yàn)證了我們代碼是ok的蛮位。
這種方式也有效地處理了并發(fā)。 那么鳞绕,和lock相比失仁,它的優(yōu)勢(shì)在哪里呢?∶呛巍lock只能控制同一進(jìn)程內(nèi)線程萄焦。 當(dāng)這段程序部署在不同的主機(jī)上時(shí),lock就顯得疲軟了冤竹。 而后者這個(gè)方案拂封,正好解決了多機(jī)部署時(shí)的并發(fā)。
最后鹦蠕,附上時(shí)間戳的生成算法:
/// <summary>
/// 獲取當(dāng)前時(shí)間戳
/// </summary>
/// <param name="bflag">為真時(shí)獲取10位時(shí)間戳,為假時(shí)獲取13位時(shí)間戳.</param>
/// <returns></returns>
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;
}