基于C#解決庫(kù)存扣減及訂單創(chuàng)建時(shí)防止并發(fā)死鎖的問題
解決庫(kù)存扣減及訂單創(chuàng)建時(shí)防止并發(fā)死鎖的問題
在我們?nèi)粘i_發(fā)的過程可有會(huì)遇到以下錯(cuò)誤
事務(wù)(進(jìn)程 ID 82)與另一個(gè)進(jìn)程被死鎖在 鎖 資源上,并且已被選作死鎖犧牲品。請(qǐng)重新運(yùn)行該事務(wù)
很多開發(fā)人員對(duì)于這個(gè)問題的排查起來是比較困難的,而生產(chǎn)生的原因多種多樣,很多人認(rèn)是因?yàn)楸碇械臄?shù)據(jù)太多了同時(shí)操作的人多人才會(huì)產(chǎn)生這種錯(cuò)誤,下面我們來還原一下死鎖的過程。
我們看一下以下sql代碼(該樣例代碼測(cè)試環(huán)境為SqlServer)
1. 第一先創(chuàng)建一個(gè)測(cè)試表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. 打開兩個(gè)查詢窗口
在窗口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í)間后你就可以看到以下錯(cuò)誤如下所示
通過以上的測(cè)試就還原了產(chǎn)生死鎖的過程,剛才的測(cè)試表H_Test
中只有兩條數(shù)據(jù),其實(shí)產(chǎn)生死鎖與數(shù)據(jù)大小沒有很大的關(guān)系,其實(shí)與整個(gè)事務(wù)的執(zhí)行長(zhǎng)短有關(guān)系,兩個(gè)業(yè)務(wù)都在操作同一條數(shù)據(jù),且一個(gè)事務(wù)中包含非常復(fù)雜的處理邏輯且執(zhí)行時(shí)間比較長(zhǎng)那么在并發(fā)或相對(duì)較多的業(yè)務(wù)操作時(shí)就會(huì)產(chǎn)生死鎖。
那么怎樣解決死鎖?
1. 減少事務(wù)的執(zhí)行時(shí)間。
優(yōu)化代碼將不需要包在事務(wù)的邏輯分離出來以減少鎖的占用時(shí)間.可以減少一部分的死鎖,但在高并發(fā)操作時(shí)依然會(huì)產(chǎn)生死鎖
2. 業(yè)務(wù)鎖
日常我們用到的鎖都是高度依賴于數(shù)據(jù)來鎖定來保證數(shù)據(jù)的原子性問題,但這樣有一個(gè)很大的BUG就是對(duì)數(shù)據(jù)庫(kù)的性能壓力非常大,在出現(xiàn)高并發(fā)時(shí)可能應(yīng)用扛得住數(shù)據(jù)庫(kù)扛不住的情況
下面介紹的就是基于HiSql
的業(yè)務(wù)鎖機(jī)制解決死鎖問題,我們模擬一種場(chǎng)景 扣減庫(kù)存并生成訂單那么我們模擬創(chuàng)建兩張表 庫(kù)存表H_Stock
及訂單表H_Order
表創(chuàng)建的sql如下
HiSql怎樣使用 請(qǐng)參照hisql快速上手
庫(kù)存表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'批次號(hào)' , @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'款號(hào)' , @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'庫(kù)位' , @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'庫(kù)存數(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'批次號(hào)' , @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'款號(hào)' , @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'出庫(kù)庫(kù)位' , @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
測(cè)試場(chǎng)景
開啟多個(gè)線程隨機(jī)產(chǎn)生不同的訂單(一個(gè)訂單中有不同批次和數(shù)量)直至庫(kù)存扣減完成并檢測(cè)是否有鎖產(chǎn)生,且?guī)齑嬗袥]有少扣和超扣,如果達(dá)到這兩個(gè)目標(biāo)說明測(cè)試是成功的
c# 代碼
class Program { static void Main(string[] args) { Console.WriteLine("測(cè)試!"); StockThread(); var s = Console.ReadLine(); } static void StockThread() { //如果有安裝redis可以啟用以下測(cè)試一下 //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(); //清除庫(kù)存表和訂單表數(shù)據(jù) sqlClient.CodeFirst.Truncate("H_Stock"); sqlClient.CodeFirst.Truncate("H_Order"); //初始化庫(kù)存數(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(); //第一種場(chǎng)景 一個(gè)訂單中只有一個(gè)批次 string[] grp_arr1 = new string[] { "9000112112" }; //第二種場(chǎng)景 一個(gè)訂單中有兩個(gè)批次 string[] grp_arr2 = new string[] { "8000252241" , "9000112112" }; //第三中場(chǎng)景一個(gè)訂單中有三個(gè)批次 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個(gè)線程運(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)生一個(gè)唯一的訂單號(hào) Int64 orderid = HiSql.Snowflake.NextId(); //加鎖并執(zhí)行 將一個(gè)訂單的批次都加鎖防止同一時(shí)間被其它業(yè)務(wù)修改 var _rtn = HiSql.Lock.LockOnExecute(grparr, () => { //能執(zhí)行到此說明已經(jīng)加鎖成功(注:非數(shù)據(jù)庫(kù)級(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";//門店編號(hào) _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}]庫(kù)存已經(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}]失敗 未找到庫(kù)存"); _sqlClient.RollBackTran(); break; } } } if (_flag) { //生成訂單 if (lstorder.Count > 0) _sqlClient.Insert("H_Order", lstorder).ExecCommand(); _sqlClient.CommitTran(); } } else { Console.WriteLine($"庫(kù)存不足..."); rtn = new Tuple<bool, string>(false, "庫(kù)存已經(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ù)庫(kù)連接配置
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",//可以指定登陸用戶的帳號(hào) Schema = "dbo", IsEncrypt = true, IsAutoClose = true, SqlExecTimeOut = 60000, AppEvents = new AopEvent() { OnDbDecryptEvent = (connstr) => { //解密連接字段 //Console.WriteLine($"數(shù)據(jù)庫(kù)連接:{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í)行錯(cuò)誤后 日志記錄 (異步) Console.WriteLine(sqlEx.Message.ToString()); }, OnTimeOut = (int timer) => { //Console.WriteLine($"執(zhí)行SQL語句超過[{timer.ToString()}]毫秒..."); } } } ); //sqlclient.CodeFirst.InstallHisql(); return sqlclient; } }
通過查詢庫(kù)存和訂單信息核對(duì)庫(kù)存是否扣減正常
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é)果
通過測(cè)試過程可以發(fā)現(xiàn) 不會(huì)產(chǎn)生死鎖也不會(huì)造成庫(kù)存扣減異常保證了數(shù)據(jù)的一致性
到此這篇關(guān)于基于C#解決庫(kù)存扣減及訂單創(chuàng)建時(shí)防止并發(fā)死鎖的問題的文章就介紹到這了,更多相關(guān)c#庫(kù)存扣減防止并發(fā)死鎖內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C# 實(shí)現(xiàn)Table的Merge,Copy和Clone
這篇文章主要介紹了C# 實(shí)現(xiàn)Table的Merge,Copy和Clone,幫助大家更好的利用c#處理文件,感興趣的朋友可以了解下2020-12-12C#條件拼接Expression<Func<T, bool>>的使用
本文主要介紹了C#條件拼接Expression<Func<T, bool>>的使用,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-02-02C#實(shí)現(xiàn)Winform小數(shù)字鍵盤模擬器
本文主要介紹了C#實(shí)現(xiàn)Winform小數(shù)字鍵盤模擬器,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-11-11Unity3D利用DoTween實(shí)現(xiàn)卡牌翻轉(zhuǎn)效果
這篇文章主要為大家詳細(xì)介紹了Unity3D利用DoTween實(shí)現(xiàn)卡牌翻轉(zhuǎn)效果,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-02-02C# 中的GroupBy的動(dòng)態(tài)拼接問題及GroupBy<>用法介紹
這篇文章主要介紹了C# 中的GroupBy的動(dòng)態(tài)拼接問題,在文章給大家提到了C# List泛型集合中的GroupBy<>用法詳解,需要的朋友可以參考下2017-12-12ADO.NET實(shí)體數(shù)據(jù)模型詳細(xì)介紹
本文將詳細(xì)介紹ADO.NET實(shí)體數(shù)據(jù)模型,下面先看看簡(jiǎn)單的單表的增刪改查操作,然后再看多表的關(guān)聯(lián)查詢,帶參數(shù)查詢等2012-11-11WPF使用Dragablz構(gòu)建可拖拽分離的Tab頁程序
這篇文章介紹了WPF使用Dragablz構(gòu)建可拖拽分離Tab頁的方法,對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-06-06