Asp.net並發請求導致的數據重復插入問題


前段時間工作中,有客戶反應了系統中某類待辦重復出現兩次的情況。我核實了數據之后,分析認為是並發請求下導致的數據不一致性問題,並做了重現。其實這並不是一個需要頻繁調用的功能,但是客戶連續點擊了兩次,導致出現了並發問題。除了前端優化,這里重點探討后台方面代碼層面的處理,最終解決問題。

一、情景分析

Asp.net程序部署Web服務,是多主線程並發執行的,當多個用戶請求進入同一個后台函數時,后進入的請求有可能會獲取到非最新狀態的數據。

結合我遇到的實際情況舉個例子,假設后台函數Func1,先讀取表TableA,TableB的數據,進行處理后,存入TableB中,而數據庫事務執行會在函數結束前才提交。請求Req1執行Func1提交事務之前,Req2又進入Func1並讀取了TableA,TableB的數據,這時Req1執行完成,這就相當於Req2拿到的已經是舊的數據,在舊的數據的基礎上再做數據處理操作,結果自然不正確了。

說到這里,你可能還不能想象具體會出現什么問題,而確實這種並發情況在非冪等功能下才會導致數據錯誤,下面就舉實例說明。

二、實例重現

現在有數據表Info,Info2,Info2的數據就是基於Info表數據產生的,兩個表都有字段-證件號碼IdentNo。

函數SyncWork()的功能為:

1,讀取Info表和Info2表中共同的IdentNo行數據,將Info表中的其它字段同步到Info2表;

2,讀取Info表中有,而Info2表中沒有的IdentNo行數據,將這些數據插入Info2表。

表實體代碼實現如下:

 1     /// <summary>
 2     /// 信息表
 3     /// </summary>
 4     public class Info
 5     {
 6         public int Id { get; set; }
 7         /// <summary>
 8         /// 證件號碼
 9         /// </summary>
10         public string IdentNo { get; set; }
11         /// <summary>
12         /// 姓名
13         /// </summary>
14         public string Name { get; set; }
15         /// <summary>
16         /// 愛好
17         /// </summary>
18         public string Hobby { get; set; }
19         /// <summary>
20         /// 備注信息
21         /// </summary>
22         public string Bz { get; set; }
23     }
24 
25     /// <summary>
26     /// 信息表2
27     /// </summary>
28     public class Info2
29     {
30         public int Id { get; set; }
31         /// <summary>
32         /// 證件號碼
33         /// </summary>
34         public string IdentNo { get; set; }
35         /// <summary>
36         /// 姓名
37         /// </summary>
38         public string Name { get; set; }
39         /// <summary>
40         /// 愛好
41         /// </summary>
42         public string Hobby { get; set; }
43         /// <summary>
44         /// 創建時間
45         /// </summary>
46         public DateTime CreateTime { get; set; }
47         /// <summary>
48         /// 最后修改時間
49         /// </summary>
50         public DateTime? UpdateTime { get; set; }
51         /// <summary>
52         /// 評分
53         /// </summary>
54         public int? Score { get; set; }
55     }
View Code

 SyncWork()代碼實現如下,代碼中加入了輔助的輸出信息:

 1         public static string SyncWork()
 2         {
 3             StringBuilder sb = new StringBuilder();
 4             //
 5             int threadId = Thread.CurrentThread.ManagedThreadId;
 6             sb.AppendLine($"{DateTime.Now.ToString("HH:mm:ss:fff")}.線程Id:{threadId}");
 7             //
 8             MyDbContext db = new MyDbContext();
 9             sb.AppendLine($"{DateTime.Now.ToString("HH:mm:ss:fff")}.db初始化");
10             //新增數據
11             var dataToAdd = db.Info.Where(x => !db.Info2.Select(y => y.IdentNo).Contains(x.IdentNo))
12                 .ToList();
13             sb.AppendLine($"{DateTime.Now.ToString("HH:mm:ss:fff")}.獲取待新增數據{dataToAdd.Count}條");
14             dataToAdd.ForEach(x =>
15             {
16                 var info2 = new Info2
17                 {
18                     IdentNo = x.IdentNo,
19                     Name = x.Name,
20                     Hobby = x.Hobby,
21                     CreateTime = DateTime.Now
22                 };
23                 db.Info2.Add(info2);
24             });
25             //更新原有數據
26             var dataToEdit = db.Info.AsQueryable().Join(db.Info2.AsQueryable(), m => m.IdentNo, n => n.IdentNo, 
27                 (m, n) =>  new
28                 {
29                     info = m,
30                     info2 = n
31                 })
32                 .ToList();
33             sb.AppendLine($"{DateTime.Now.ToString("HH:mm:ss:fff")}獲取待更新數據{dataToEdit.Count}條");
34             dataToEdit.ForEach(x =>
35             {
36                 x.info2.Name = x.info.Name;
37                 x.info2.Hobby = x.info.Hobby;
38                 x.info2.UpdateTime = DateTime.Now;
39             });
40             sb.AppendLine($"{DateTime.Now.ToString("HH:mm:ss:fff")}開始休眠5s");
41             Thread.Sleep(5000);
42             sb.AppendLine($"{DateTime.Now.ToString("HH:mm:ss:fff")}dbSaveBegin");
43             db.SaveChanges();
44             sb.AppendLine($"{DateTime.Now.ToString("HH:mm:ss:fff")}dbSaveEnd");
45             return sb.ToString();
46         }
View Code

 里邊的這幾行代碼就是問題重現的重點了:

1             sb.AppendLine($"{DateTime.Now.ToString("HH:mm:ss:fff")}開始休眠5s");
2             Thread.Sleep(5000);
3             sb.AppendLine($"{DateTime.Now.ToString("HH:mm:ss:fff")}dbSaveBegin");
4             db.SaveChanges();
5             sb.AppendLine($"{DateTime.Now.ToString("HH:mm:ss:fff")}dbSaveEnd");

在db提交之前,我們讓此線程休眠了5s,以模仿現實中一些耗時的操作。這5s期間可能有后續的N個新請求線程進入此函數,那么這些線程獲取的數據都會是同樣的舊數據了。第一個執行完函數的線程,提交了更改,而后續這些線程再提交的更改,便是基於舊的數據做的更改了。

下面開始重新問題,我們執行一次請求,兩表數據情況分別如下:

 然后,Info表中新增了一條數據:

 這時,我們再執行請求,預期結果應該將行003老虎的數據添加到Info2中,但是我們現在模擬並發,連續調用2次請求看看結果:

 

可以看到,居然將003老虎的數據插入了兩次。這就是並發帶來的副作用了。

附上兩次請求的輔助輸出信息:

 1 //Request1
 2 22:50:05:953.線程Id:44
 3 22:50:05:953.db初始化
 4 22:50:05:982.獲取待新增數據1條
 5 22:50:06:000.獲取待更新數據2條
 6 22:50:06:001.開始休眠5s
 7 22:50:11:001.dbSaveBegin
 8 22:50:11:084.dbSaveEnd
 9 //Request2
10 22:50:07:240.線程Id:48
11 22:50:07:240.db初始化
12 22:50:07:270.獲取待新增數據1條
13 22:50:07:287.獲取待更新數據2條
14 22:50:07:287.開始休眠5s
15 22:50:12:287.dbSaveBegin
16 22:50:12:339.dbSaveEnd

三、問題解決

既然問題是並發請求導致的,而這個功能不是需要頻繁調用的功能,最簡便的解決方法就是,我們可以設置此功能同一時間只能由一個線程來訪問,即通過lock()的方式。

最終實現代碼如下:

 1     public class InfoSync
 2     {
 3         private static object syncObject = new object();
 4         public static string Sync()
 5         {
 6             lock (syncObject)
 7             {
 8                 return SyncWork();
 9             }
10         }
11         private static string SyncWork()
12         {
13             //...  
14         }
15     }
View Code

同時貼出示例控制器的簡單實現:

 1     public class DataController : Controller
 2     {
 3         // GET: Data
 4         public ActionResult Index()
 5         {
 6             try
 7             {
 8                 var str = InfoSync.Sync();
 9                 return Content(str);
10             }
11             catch (Exception ex)
12             {
13                 return Content($"程序發生錯誤:{ex.Message}\n內部錯誤:{ex.InnerException.Message}");
14             }
15         }
16     }
View Code

四、總結

類似文中數據同步並發情況的實際應用情況還有很多,比如系統有時會需要產生編號,我們會訪問數據庫中這類編號的最新值,然后計算出下一個編號值,如果不處理並發情況,業務量大時可能就會出現重復編號了。

本文中,針對這類請求並發問題,通過代碼鎖的方式,將特定功能的並發請求執行轉化為隊列請求執行,從而避免了問題的發生。

當然,處理並發還有其它途徑,如通過數據庫鎖的方式,再如分布式部署情況下,我們用代碼鎖的方式也會失效了,實際工作中還需要根據具體情況采用最小代價成本的處理方式。

--End

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM