本文重點介紹通過事務控制,利用數據庫的樂觀鎖和時間戳,來解決並發(非高並發)環境下的臟讀、幻讀、不可重復讀等問題,同時也能解決超賣等現象,對開發企業管理系統的朋友提供一個思路,為更突出主題思路,文涉及到SqlSugar的一些代碼已隱去。
1. 數據庫建表
CREATE TABLE dbo.Test
(
tId INT IDENTITY NOT NULL
, tName NVARCHAR (20) NOT NULL
, tSalary DECIMAL (8, 2) NULL
, tTimeStamp TIMESTAMP
, PRIMARY KEY (tId)
)
2. 創建類
public partial class Test
{
[SugarColumn(IsPrimaryKey =true,IsIdentity =true)]
public int tId { get; set; }
public string tName { get; set; }
public decimal? tSalary { get; set; }
[SugarColumn(IsOnlyIgnoreInsert = true)]
public byte[] tTimeStamp { get; set; }
}
3. 代碼示例
static async Task Main(string[] args)
{
for (int i = 1; i <= 5; i++)
{
Task.Factory.StartNew(async (id) =>
{
await Test((int)id);
}, i);
}
}
static async Task Test(int threadID)
{
var db = SqlSugar.DB;
for (int k = 1; k <= 50; k++)
{
string log = string.Empty;
log += $"第{threadID,2}線程,第{k}次";
//客戶端從數據庫獲取數據
var firstRead = await db.Queryable<Test>().SingleAsync(x => x.tId == 2);
log += $" name:{firstRead.tName,5} version:{BitConverter.ToString(firstRead.tTimeStamp).Replace(" - ", "")}";
//客戶端修改數據需要時間
Thread.Sleep(10);
try
{
db.Ado.BeginTran();
log += " 事務開始";
//提交修改前數據進行驗證
var secondRead = await db.Queryable<Test>().SingleAsync(x => x.tId == 2);
if (BitConverter.ToString(secondRead.tTimeStamp) != BitConverter.ToString(firstRead.tTimeStamp))
{
log += $" 不可重復讀,version:{BitConverter.ToString(secondRead.tTimeStamp).Replace(" - ", "")}";
throw new Exception();
}
var data = new Test { tId = 2, tName = $"{threadID}-{k}" };
var result = await db.Updateable(data).Where(c => c.tTimeStamp == firstRead.tTimeStamp).IgnoreColumns(ignoreAllNullColumns: true).ExecuteCommandAsync();
db.Ado.CommitTran();
log += result > 0 ? $" 修改成功,當前name:{data.tName}" : " 修改失敗,數據被其它線程修改";
}
catch (Exception)
{
db.Ado.RollbackTran();
log += " 事務回滾";
}
finally
{
Console.WriteLine(log);
};
Thread.Sleep(10);
}
}