基于C#解決庫存扣減及訂單創(chuàng)建時(shí)防止并發(fā)死鎖的問題
解決庫存扣減及訂單創(chuàng)建時(shí)防止并發(fā)死鎖的問題
在我們?nèi)粘i_發(fā)的過程可有會遇到以下錯誤
事務(wù)(進(jìn)程 ID 82)與另一個進(jìn)程被死鎖在 鎖 資源上,并且已被選作死鎖犧牲品。請重新運(yùn)行該事務(wù)
很多開發(fā)人員對于這個問題的排查起來是比較困難的,而生產(chǎn)生的原因多種多樣,很多人認(rèn)是因?yàn)楸碇械臄?shù)據(jù)太多了同時(shí)操作的人多人才會產(chǎn)生這種錯誤,下面我們來還原一下死鎖的過程。
我們看一下以下sql代碼(該樣例代碼測試環(huán)境為SqlServer)
1. 第一先創(chuàng)建一個測試表H_Test
復(fù)制以下代碼
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[H_TEST](
[Id] [int] IDENTITY(1,1) NOT NULL,
[DID] [int] NULL,
[UNAME] [nvarchar](50) NULL,
[UNAME2] [nvarchar](50) NULL,
CONSTRAINT [PK_H_TEST_3994ceeb-a4b8-41e1-b06b-1e59a2e51d8c] PRIMARY KEY CLUSTERED
(
[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'自增主鍵' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'H_TEST', @level2type=N'COLUMN',@level2name=N'Id'
GO
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'DID' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'H_TEST', @level2type=N'COLUMN',@level2name=N'DID'
GO
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'UNAME' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'H_TEST', @level2type=N'COLUMN',@level2name=N'UNAME'
GO
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'UNAME2' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'H_TEST', @level2type=N'COLUMN',@level2name=N'UNAME2'
GO
insert [dbo].[H_TEST](DID,UNAME,UNAME2) VALUES(1,'HI','HI2');
insert [dbo].[H_TEST](DID,UNAME,UNAME2) VALUES(2,'HISQL','HISQL2');
2. 打開兩個查詢窗口
在窗口1中復(fù)制以下代碼
begin tran update dbo.H_TEST set UNAME='d1' where dID=1 waitfor delay '00:00:10' update H_TEST set UNAME='d2' where dID=2 commit tran
在窗口2中復(fù)制以下代碼
begin tran update H_TEST set UNAME='d2' where dID=2 waitfor delay '00:00:10' update dbo.H_TEST set UNAME='d1' where dID=1 commit tran
3. 執(zhí)行代碼
同時(shí)執(zhí)行窗口1和窗口2的代碼,在等待一段時(shí)間后你就可以看到以下錯誤如下所示

通過以上的測試就還原了產(chǎn)生死鎖的過程,剛才的測試表H_Test中只有兩條數(shù)據(jù),其實(shí)產(chǎn)生死鎖與數(shù)據(jù)大小沒有很大的關(guān)系,其實(shí)與整個事務(wù)的執(zhí)行長短有關(guān)系,兩個業(yè)務(wù)都在操作同一條數(shù)據(jù),且一個事務(wù)中包含非常復(fù)雜的處理邏輯且執(zhí)行時(shí)間比較長那么在并發(fā)或相對較多的業(yè)務(wù)操作時(shí)就會產(chǎn)生死鎖。
那么怎樣解決死鎖?
1. 減少事務(wù)的執(zhí)行時(shí)間。
優(yōu)化代碼將不需要包在事務(wù)的邏輯分離出來以減少鎖的占用時(shí)間.可以減少一部分的死鎖,但在高并發(fā)操作時(shí)依然會產(chǎn)生死鎖
2. 業(yè)務(wù)鎖
日常我們用到的鎖都是高度依賴于數(shù)據(jù)來鎖定來保證數(shù)據(jù)的原子性問題,但這樣有一個很大的BUG就是對數(shù)據(jù)庫的性能壓力非常大,在出現(xiàn)高并發(fā)時(shí)可能應(yīng)用扛得住數(shù)據(jù)庫扛不住的情況
下面介紹的就是基于HiSql 的業(yè)務(wù)鎖機(jī)制解決死鎖問題,我們模擬一種場景 扣減庫存并生成訂單那么我們模擬創(chuàng)建兩張表 庫存表H_Stock 及訂單表H_Order 表創(chuàng)建的sql如下
HiSql怎樣使用 請參照hisql快速上手
庫存表sql代碼
CREATE TABLE [dbo].[H_Stock](
[Batch] [varchar](20) NOT NULL,
[Material] [varchar](20) NOT NULL,
[Location] [varchar](5) NULL,
[st_kc] [decimal](18, 2) NULL,
[CreateTime] [datetime] NULL,
[CreateName] [nvarchar](50) NULL,
[ModiTime] [datetime] NULL,
[ModiName] [nvarchar](50) NULL,
CONSTRAINT [PK_H_Stock] PRIMARY KEY CLUSTERED
(
[Batch] ASC,
[Material] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
ALTER TABLE [dbo].[H_Stock] ADD CONSTRAINT [DF_H_Stock_st_kc] DEFAULT ((0)) FOR [st_kc]
GO
ALTER TABLE [dbo].[H_Stock] ADD CONSTRAINT [DF_H_Stock_CreateTime] DEFAULT (getdate()) FOR [CreateTime]
GO
ALTER TABLE [dbo].[H_Stock] ADD CONSTRAINT [DF_H_Stock_CreateName] DEFAULT ('') FOR [CreateName]
GO
ALTER TABLE [dbo].[H_Stock] ADD CONSTRAINT [DF_H_Stock_ModiTime] DEFAULT (getdate()) FOR [ModiTime]
GO
ALTER TABLE [dbo].[H_Stock] ADD CONSTRAINT [DF_H_Stock_ModiName] DEFAULT ('') FOR [ModiName]
GO
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'批次號' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'H_Stock', @level2type=N'COLUMN',@level2name=N'Batch'
GO
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'款號' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'H_Stock', @level2type=N'COLUMN',@level2name=N'Material'
GO
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'庫位' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'H_Stock', @level2type=N'COLUMN',@level2name=N'Location'
GO
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'庫存數(shù)' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'H_Stock', @level2type=N'COLUMN',@level2name=N'st_kc'
GO
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'創(chuàng)建時(shí)間' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'H_Stock', @level2type=N'COLUMN',@level2name=N'CreateTime'
GO
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'創(chuàng)建人' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'H_Stock', @level2type=N'COLUMN',@level2name=N'CreateName'
GO
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'修改時(shí)間' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'H_Stock', @level2type=N'COLUMN',@level2name=N'ModiTime'
GO
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'修改人' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'H_Stock', @level2type=N'COLUMN',@level2name=N'ModiName'
GO
訂單表sql
CREATE TABLE [dbo].[H_Order](
[OrderId] [bigint] NOT NULL,
[Batch] [varchar](20) NOT NULL,
[Material] [varchar](20) NOT NULL,
[Shop] [varchar](5) NULL,
[Location] [varchar](5) NULL,
[SalesNum] [decimal](18, 2) NULL,
[CreateTime] [datetime] NULL,
[CreateName] [nvarchar](50) NULL,
[ModiTime] [datetime] NULL,
[ModiName] [nvarchar](50) NULL,
CONSTRAINT [PK_H_Order] PRIMARY KEY CLUSTERED
(
[OrderId] ASC,
[Batch] ASC,
[Material] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
ALTER TABLE [dbo].[H_Order] ADD CONSTRAINT [DF_H_Order_SalesNum] DEFAULT ((0)) FOR [SalesNum]
GO
ALTER TABLE [dbo].[H_Order] ADD CONSTRAINT [DF_H_Order_CreateTime] DEFAULT (getdate()) FOR [CreateTime]
GO
ALTER TABLE [dbo].[H_Order] ADD CONSTRAINT [DF_H_Order_CreateName] DEFAULT ('') FOR [CreateName]
GO
ALTER TABLE [dbo].[H_Order] ADD CONSTRAINT [DF_H_Order_ModiTime] DEFAULT (getdate()) FOR [ModiTime]
GO
ALTER TABLE [dbo].[H_Order] ADD CONSTRAINT [DF_H_Order_ModiName] DEFAULT ('') FOR [ModiName]
GO
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'批次號' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'H_Order', @level2type=N'COLUMN',@level2name=N'Batch'
GO
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'款號' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'H_Order', @level2type=N'COLUMN',@level2name=N'Material'
GO
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'門店' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'H_Order', @level2type=N'COLUMN',@level2name=N'Shop'
GO
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'出庫庫位' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'H_Order', @level2type=N'COLUMN',@level2name=N'Location'
GO
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'銷售數(shù)量' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'H_Order', @level2type=N'COLUMN',@level2name=N'SalesNum'
GO
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'創(chuàng)建時(shí)間' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'H_Order', @level2type=N'COLUMN',@level2name=N'CreateTime'
GO
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'創(chuàng)建人' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'H_Order', @level2type=N'COLUMN',@level2name=N'CreateName'
GO
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'修改時(shí)間' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'H_Order', @level2type=N'COLUMN',@level2name=N'ModiTime'
GO
EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'修改人' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'H_Order', @level2type=N'COLUMN',@level2name=N'ModiName'
GO
測試場景
開啟多個線程隨機(jī)產(chǎn)生不同的訂單(一個訂單中有不同批次和數(shù)量)直至庫存扣減完成并檢測是否有鎖產(chǎn)生,且?guī)齑嬗袥]有少扣和超扣,如果達(dá)到這兩個目標(biāo)說明測試是成功的
c# 代碼
class Program
{
static void Main(string[] args)
{
Console.WriteLine("測試!");
StockThread();
var s = Console.ReadLine();
}
static void StockThread()
{
//如果有安裝redis可以啟用以下測試一下
//HiSql.Global.RedisOn = true;//開啟redis緩存
//HiSql.Global.RedisOptions = new RedisOptions { Host = "172.16.80.178", PassWord = "pwd123", Port = 6379, CacheRegion = "TST", Database = 0 };
HiSqlClient sqlClient = Demo_Init.GetSqlClient();
//清除庫存表和訂單表數(shù)據(jù)
sqlClient.CodeFirst.Truncate("H_Stock");
sqlClient.CodeFirst.Truncate("H_Order");
//初始化庫存數(shù)據(jù)
sqlClient.Modi("H_Stock", new List<object> {
new { Batch="9000112112",Material="ST0021",Location="A001",st_kc=5000},
new { Batch="8000252241",Material="ST0080",Location="A001",st_kc=1000},
new { Batch="7000252241",Material="ST0026",Location="A001",st_kc=1500}
}).ExecCommand();
//第一種場景 一個訂單中只有一個批次
string[] grp_arr1 = new string[] { "9000112112" };
//第二種場景 一個訂單中有兩個批次
string[] grp_arr2 = new string[] { "8000252241" , "9000112112" };
//第三中場景一個訂單中有三個批次
string[] grp_arr3 = new string[] { "8000252241", "9000112112", "7000252241" };
Random random = new Random();
HiSqlClient _sqlClient = Demo_Init.GetSqlClient();
//表結(jié)構(gòu)緩存預(yù)熱
var _dt1= _sqlClient.HiSql("select * from H_Order").Take(1).Skip(1).ToTable();
var _dt2 = _sqlClient.HiSql("select * from H_Stock").Take(1).Skip(1).ToTable();
//開啟10個線程運(yùn)行
Parallel.For(0, 10, (index, y) => {
int grpidx = index % 3;
string[] grparr = null;
if (grpidx == 0)
grparr = grp_arr1;
else if (grpidx == 1)
grparr = grp_arr2;
else
grparr = grp_arr3;
//Thread.Sleep(random.Next(10) * 200);
Console.WriteLine($" {index}線程Id:{Thread.CurrentThread.ManagedThreadId}\t{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}");
//執(zhí)行訂單創(chuàng)建
var rtn = CreateSale(grparr);
Console.WriteLine(rtn.Item2);
});
}
static Tuple<bool, string> CreateSale(string[] grparr)
{
Random random = new Random();
HiSqlClient _sqlClient = Demo_Init.GetSqlClient();
bool _flag = true;
Tuple<bool, string> rtn = new Tuple<bool, string>(true, "執(zhí)行");
//指定雪花ID使用的引擎 (可以不指定)
HiSql.Snowflake.SnowType = SnowType.IdWorker;
//產(chǎn)生一個唯一的訂單號
Int64 orderid = HiSql.Snowflake.NextId();
//加鎖并執(zhí)行 將一個訂單的批次都加鎖防止同一時(shí)間被其它業(yè)務(wù)修改
var _rtn = HiSql.Lock.LockOnExecute(grparr, () =>
{
//能執(zhí)行到此說明已經(jīng)加鎖成功(注:非數(shù)據(jù)庫級加鎖)
DataTable dt = _sqlClient.HiSql($"select Batch,Material,Location,st_kc from H_Stock where Batch in ({grparr.ToSqlIn()}) and st_kc>0").ToTable();
if (dt.Rows.Count > 0)
{
List<object> lstorder = new List<object>();
Console.WriteLine($"雪花ID{orderid}");
string _shop = "4301";//門店編號
_sqlClient.BeginTran();
foreach (string n in grparr)
{
int s = random.Next(1,10);
int v = _sqlClient.Update("H_Stock", new { st_kc = $"`st_kc`-{s}" }).Where($"Batch='{n}' and st_kc>={s}").ExecCommand();
if (v == 0)
{
_flag = false;
Console.WriteLine($"批次:[{n}]扣減[{s}]失敗");
rtn = new Tuple<bool, string>(false, $"批次:[{n}]庫存已經(jīng)不足");
_sqlClient.RollBackTran();
break;
}
else
{
DataRow _drow = dt.AsEnumerable().Where(s => s.Field<string>("Batch").Equals(n)).FirstOrDefault();
if (_drow != null)
{
lstorder.Add(
new
{
OrderId = orderid,
Batch = _drow["Batch"].ToString(),
Material = _drow["Material"].ToString(),
Shop = _shop,
Location = _drow["Location"].ToString(),
SalesNum = s,
}
);
}
else
{
_flag = false;
Console.WriteLine($"批次:[{n}]扣減[{s}]失敗 未找到庫存");
_sqlClient.RollBackTran();
break;
}
}
}
if (_flag)
{
//生成訂單
if (lstorder.Count > 0)
_sqlClient.Insert("H_Order", lstorder).ExecCommand();
_sqlClient.CommitTran();
}
}
else
{
Console.WriteLine($"庫存不足...");
rtn = new Tuple<bool, string>(false, "庫存已經(jīng)不足");
}
}, new LckInfo
{
UName = "tanar",
Ip = "127.0.0.1"
}, 20, 10);//加鎖超時(shí)時(shí)間設(shè)定
_sqlClient.Close();
Console.WriteLine(_rtn.Item2);
//可以注釋線程等待
//Thread.Sleep(random.Next(1,10)*100);
if (rtn.Item1)
return CreateSale(grparr);
else
return rtn;
}
}數(shù)據(jù)庫連接配置
internal class Demo_Init
{
public static HiSqlClient GetSqlClient()
{
HiSqlClient sqlclient = new HiSqlClient(
new ConnectionConfig()
{
DbType = DBType.SqlServer,
DbServer = "local-HoneBI",
ConnectionString = "server=(local);uid=sa;pwd=Hone@123;database=HiSql;Encrypt=True; TrustServerCertificate=True;",//; MultipleActiveResultSets = true;
User = "tansar",//可以指定登陸用戶的帳號
Schema = "dbo",
IsEncrypt = true,
IsAutoClose = true,
SqlExecTimeOut = 60000,
AppEvents = new AopEvent()
{
OnDbDecryptEvent = (connstr) =>
{
//解密連接字段
//Console.WriteLine($"數(shù)據(jù)庫連接:{connstr}");
return connstr;
},
OnLogSqlExecuting = (sql, param) =>
{
//sql執(zhí)行前 日志記錄 (異步)
//Console.WriteLine($"sql執(zhí)行前記錄{sql} time:{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss ffff")}");
},
OnLogSqlExecuted = (sql, param) =>
{
//sql執(zhí)行后 日志記錄 (異步)
//Console.WriteLine($"sql執(zhí)行后記錄{sql} time:{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss ffff")}");
},
OnSqlError = (sqlEx) =>
{
//sql執(zhí)行錯誤后 日志記錄 (異步)
Console.WriteLine(sqlEx.Message.ToString());
},
OnTimeOut = (int timer) =>
{
//Console.WriteLine($"執(zhí)行SQL語句超過[{timer.ToString()}]毫秒...");
}
}
}
);
//sqlclient.CodeFirst.InstallHisql();
return sqlclient;
}
}通過查詢庫存和訂單信息核對庫存是否扣減正常
select * from H_Stock select batch,sum(salesnum) as salesnum from H_Order group by batch select orderid,sum(salesnum) as salesnum from H_Order group by orderid select * from H_Order
核驗(yàn)結(jié)果

通過測試過程可以發(fā)現(xiàn) 不會產(chǎn)生死鎖也不會造成庫存扣減異常保證了數(shù)據(jù)的一致性
到此這篇關(guān)于基于C#解決庫存扣減及訂單創(chuàng)建時(shí)防止并發(fā)死鎖的問題的文章就介紹到這了,更多相關(guān)c#庫存扣減防止并發(fā)死鎖內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C# 實(shí)現(xiàn)Table的Merge,Copy和Clone
這篇文章主要介紹了C# 實(shí)現(xiàn)Table的Merge,Copy和Clone,幫助大家更好的利用c#處理文件,感興趣的朋友可以了解下2020-12-12
C#條件拼接Expression<Func<T, bool>>的使用
本文主要介紹了C#條件拼接Expression<Func<T, bool>>的使用,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-02-02
C#實(shí)現(xiàn)Winform小數(shù)字鍵盤模擬器
本文主要介紹了C#實(shí)現(xiàn)Winform小數(shù)字鍵盤模擬器,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-11-11
Unity3D利用DoTween實(shí)現(xiàn)卡牌翻轉(zhuǎn)效果
這篇文章主要為大家詳細(xì)介紹了Unity3D利用DoTween實(shí)現(xiàn)卡牌翻轉(zhuǎn)效果,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-02-02
C# 中的GroupBy的動態(tài)拼接問題及GroupBy<>用法介紹
這篇文章主要介紹了C# 中的GroupBy的動態(tài)拼接問題,在文章給大家提到了C# List泛型集合中的GroupBy<>用法詳解,需要的朋友可以參考下2017-12-12
ADO.NET實(shí)體數(shù)據(jù)模型詳細(xì)介紹
本文將詳細(xì)介紹ADO.NET實(shí)體數(shù)據(jù)模型,下面先看看簡單的單表的增刪改查操作,然后再看多表的關(guān)聯(lián)查詢,帶參數(shù)查詢等2012-11-11
WPF使用Dragablz構(gòu)建可拖拽分離的Tab頁程序
這篇文章介紹了WPF使用Dragablz構(gòu)建可拖拽分離Tab頁的方法,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-06-06

