scanf_s 发送访问冲突_程序员如何解决并发冲突的难题?
作者?| 羽生結弦
責編?| 胡雪蕊
出品 | CSDN(ID: CSDNnews)
在大多數的應用中都會出現客戶端同時發送多個請求對同一條數據就行修改,這個時候就會出現并發沖突。我們一般的做法會有如下兩種:
1.?樂觀并發所謂的樂觀并發就是多個請求同時對同一條數據的更新,只有最后一個更新請求會被保存,其他更新請求將會被拋棄。
2.?悲觀并發所謂悲觀并發就是多個請求同時對同一條數據的更新,只有當前更新請求完成或者被拋棄,才會執行下一個更新請求,如果當前更新請求未完成或者未被拋棄,那么后面所有的更新請求將會被阻塞。
通過上面的簡單講解我們簡單的了解了如何處理并發請求,那么下面我們來看一下上面兩種做法的具體講解和實現。
方法一
在 Entity Framework 中,默認的解決方案是樂觀并發,原因是當出現并發情況的時候,內部沒有任何對其他客戶端訪問同一行數據的限制。我們來看一下例子,我們在數據庫中存有一條數據,數據如下圖所示:下面我們來修改一下 Name 字段的值:csharpclass?Program{????static?void?Main(string[]?args){????????int?userId?=?1;????????using?(var?db?=?new?EfContext())????????{????????????using?(var?ef?=?new?EfContext())????????????{????????????????User?user1?=?db.Users.FirstOrDefault(p?=>?p.Id?==?userId);????????????????User?user2=?ef.Users.FirstOrDefault(p?=>?p.Id?==?userId);????????????????user1.Name?=?"李四";????????????????db.SaveChanges();????????????????user2.Name?=?"王五";????????????????ef.SaveChanges();????????????}????????}????}}在上面的代碼中我們利用嵌套 using 的形式實現了并發訪問。首先我們同時查詢出 id 等于1的人員,然后將 user1 中的 Name 修改為李四并提交,接著再把 user2 中的 Name 修改為王五并提交。這個時候我們再查詢數據庫就會發現 Name 列被更新為了最后一次提交值王五,如下圖所示:上述操作發生了什么呢?我們來看一下,首先我們利用 db 從數據庫中讀取了 id 等于1的人員信息,此時該人員信息為張三,然后我們將 Name 值改為李四,并且提交到了數據庫,在這個時候,數據庫中的Name值將不再是張三,而是李四。接著我們再將 user2 的 Name 值修改為王五,并提交的數據庫,這個時候數據庫的 Name 列的值變為了王五。上述情況下,Entity Framework 將修改轉換為 update 語句時是利用主鍵來定位指定行,因此上面兩次操作都會成功,只不過最后一次修改的數據會最終持久化到數據庫中。但是這種方式存在一個巨大的隱患,例如在門票預售系統中,門票的數量是有限制的,購票人數超過門票數量限制將會禁止購買。如果利用 Entity Framework 默認的樂觀并發模式,每次有并發請求購票時,每個請求都會減去門票數量,并且向數據庫中插入一條購票信息,這樣一來永遠是最后一個請求的數據會持久化到數據庫中,這樣就造成了門票預約人數超過了門票的限制數量。
針對上面所說的問題,我么可以利用如下兩種方式來解決:
1.?并發 Token
利用這個方法我們只需在實體類對應的 Map 文件的構造函數中加讓類似下面的代碼即可:csharpProperty(p?=>?p.Name).IsConcurrencyToken();2.?行版本
通過行版本設置,我們需要為實體添加一個行版本子字節數組,代碼如下:?
csharppublic?byte[]?RowVersion?{?get;?set;?}然后將行版本字段映射進數據庫,這樣每次更新數據的時候都行版本字段也會跟著更新。最后我們在實體類對應的 Map 文件的構造函數中添加如下代碼即可:csharpProperty(p?=>?p.RowVersion).IsRowVersion();這樣在每次提交修改請求時 Entity Framework 都會檢查數據庫中的行版本和當前提交數據的行版本是否一致,如果一直就更新數據和行版本信息。上述兩種方法都將會引發并發異常,那么我們該如何解決這個異常呢?我們需要用到并發異常類( DbUpdateConcurrencyException )中的 Entries 屬性,該屬性是一個集合。我們需要調用集合中每個對象的 Reload 方法將數據庫中最新的值放在內存中。這樣后續的實體值將和數據庫保持一致。完成這一步后,我們可以重新向數據庫提交更新數據。具體實現代碼如下:csharpclass?Program
{static?void?Main(string[]?args){int?userId?=?1;using?(var?db?=?new?EfContext())
????????{using?(var?ef?=?new?EfContext())
????????????{
????????????????User?user1?=?db.Users.FirstOrDefault(p?=>?p.Id?==?userId);
????????????????User?user2=?ef.Users.FirstOrDefault(p?=>?p.Id?==?userId);
????????????????user1.Name?=?"李四";
????????????????db.SaveChanges();try
????????????????{
????????????????????user2.Name?=?"王五";
????????????????????ef.SaveChanges();
????????????????}catch?(DbUpdateConcurrencyException?e)
????????????????{foreach?(var?item?in?e.Entries)
????????????????????{
????????????????????????item.Reload();
????????????????????????ef.SaveChanges();
????????????????????}
????????????????}
????????????}
????????}
????}
}這里需要注意的是這個方法并不是萬能的,只是將當前客戶端的值成功存入數據庫中,這種情況被稱為客戶端獲勝,當然了還有數據庫獲勝,以及數據庫和客戶端合并獲勝(這三個概念解決并發沖突的方式將在下一小節講解)。在講解這個問題前我們先來了解一下 Entity Framework 的原始值和更新后的數據庫值以及當前值從哪里獲得。代碼如下:csharptry{??//more?code}catch?(DbUpdateConcurrencyException?e){????foreach?(var?item?in?e.Entries)????{????????//原始值????????var?ov?=?item.OriginalValues.ToObject();????????//更新后數據庫值????????var?dv?=?item.GetDatabaseValues().ToObject();????????//?當前值????????var?nv?=?item.CurrentValues.ToObject();????}}從上面的代碼中我們可以看到獲取這三種值我們依然是從并發異常類的 Entries 屬性中獲得。看到這里一定會有人想到不利用 Reload 方法來更新內存中的最新值,而是直接利數據庫值更新當前內存中的值,如果你想到這里說明你已經掌握了解決并發沖突最簡單的方法。那么我們就來看一下代碼:csharptry{????//more?code}catch?(DbUpdateConcurrencyException?e){????foreach?(var?item?in?e.Entries)????{????????Object?dv?=?item.GetDatabaseValues().ToObject();????????item.OriginalValues.SetValues(dv);????????ef.SaveChanges();????}}
方法二
上一小節中我們提到了客戶端獲勝、數據庫獲勝以及數據庫和客戶端合并獲勝,并且講解了原始值和更新后的數據庫值以及當前值從哪里獲得的。在這一節將利用客戶端獲勝、數據庫獲勝以及客戶端和數據庫合并獲勝處理并發的方法。
1.?客戶端獲勝
當調用 SaveChanges 方法時,如果存在并發沖突將會引發 DbUpdateConcurrencyException 異常,那么這個時候我們將調用 handleDbUpdateConcurrencyException 函數來處理異常并正確解決沖突,最后在調用 SaveChanges 方法重試提交數據。如果依然排除 DbUpdateConcurrencyException 異常,將不在進行處理。我們來看以下代碼:
csharpclass?Program{????static?void?Main(string[]?args){????????int?userId?=?1;????????using?(var?db?=?new?EfContext())????????{????????????using?(var?ef?=?new?EfContext())????????????{????????????????User?user1?=?db.Users.FirstOrDefault(p?=>?p.Id?==?userId);????????????????User?user2?=?ef.Users.FirstOrDefault(p?=>?p.Id?==?userId);????????????????user1.Name?=?"李四";????????????????db.SaveChanges();????????????????try????????????????{????????????????????user2.Name?=?"王五";????????????????????ef.SaveChanges();????????????????}????????????????catch?(DbUpdateConcurrencyException?e)????????????????{????????????????????Retry(ef,?handleDbUpdateConcurrencyException:?exception?=>????????????????????{????????????????????????exception?=?(e?as?DbUpdateConcurrencyException).Entries;????????????????????????foreach?(var?item?in?exception)????????????????????????{????????????????????????????item.OriginalValues.????????????????????????????????SetValues(item.GetDatabaseValues());????????????????????????}????????????????????});????????????????}????????????}????????}????}}上述代碼中發生并發異常時,將會將數據庫的值提交到內存中,然后重新提交更新數據。2.?數據庫獲勝如果你想讓數據庫獲勝,那就簡單了。再發生異常時不需做任何處理,只返回方法的返回值即可。我們將上一個例子的代碼更新一下:csharpclass?Program{????static?void?Main(string[]?args){????????int?userId?=?1;????????using?(var?db?=?new?EfContext())????????{????????????using?(var?ef?=?new?EfContext())????????????{????????????????User?user1?=?db.Users.FirstOrDefault(p?=>?p.Id?==?userId);????????????????User?user2?=?ef.Users.FirstOrDefault(p?=>?p.Id?==?userId);????????????????user1.Name?=?"李四";????????????????db.SaveChanges();????????????????try????????????????{????????????????????user2.Name?=?"王五";????????????????????ef.SaveChanges();????????????????}????????????????catch?(DbUpdateConcurrencyException?e)????????????????{????????????????????return;????????????????}????????????}????????}????}}上面代碼運行后,只有李四會被更新到數據庫中,王五因為并發沖突且異常捕獲后沒有進行任何處理而不會存入數據庫。
3.?數據庫和客戶端合并獲勝
這種方式是最復雜的,需要合并數據庫和客戶端的數據,如果用到此方法我們需要謹記如下兩點:
如果原始值與數據庫中的值不通,就說明數據庫中的值已經被其他客戶端更新,這時必須放棄當前的更新,保留數據庫的更新;
如果原始值與數據庫的值相同,代表不會發生并發沖突,按照正常處理流程處理即可。
同樣,我們將上面的例子按照上面兩點進行修改:
csharpclass?Program{????static?void?Main(string[]?args)????{????????int?userId?=?1;????????using?(var?db?=?new?EfContext())????????{????????????using?(var?ef?=?new?EfContext())????????????{????????????????User?user1?=?db.Users.FirstOrDefault(p?=>?p.Id?==?userId);????????????????User?user2?=?ef.Users.FirstOrDefault(p?=>?p.Id?==?userId);????????????????user1.Name?=?"李四";????????????????db.SaveChanges();????????????????try????????????????{????????????????????user2.Name?=?"王五";????????????????????ef.SaveChanges();????????????????}????????????????catch?(DbUpdateConcurrencyException?e)????????????????{????????????????????Retry(ef,?handleDbUpdateConcurrencyException:?exception?=>????????????????????{????????????????????????exception?=?(e?as?DbUpdateConcurrencyException).Entries;????????????????????????foreach?(var?item?in?exception)????????????????????????{????????????????????????????Object?dv?=?item.GetDatabaseValues();????????????????????????????Object?ov?=?item.OriginalValues();????????????????????????????item.OriginalValues.SetValues(dv);????????????????????????????dv.PropertyNames.Where(property?=>????????????????????????????????!object.Equals(ov[property],?dv[property])).ToList().ForEach(property?=>????????????????????????????????item.Property(property).IsModified?=?false);????????????????????????}????????????????????});????????????????}????????????}????????}????}}方法三前面兩種方法都是利用 SaveChanges 捕獲并發異常,其實我們也可以自定義 SaveChanges 的擴展方法來處理并發異常。下面我們就來看一下具體的兩種策略。
1.?普通策略
這個策略非常簡單,就是利用循環來實現重試機制,代碼如下:
csharppublic?static?partial?class?DbContextExtensions{????public?static?int?SaveChanges(this?DbContext?dbContext,?Action>?action,int?retryCount?=?3){????????if?(retryCount?<=?0)????????{????????????throw?new?ArgumentOutOfRangeException(nameof(retryCount),?$"{retryCount}必須大于0");????????}????????for?(int?retry=1;retry????????{????????????try????????????{????????????}????????????catch?(DbUpdateConcurrencyException?e)?when?(retry?????????????{????????????????resolveConficts(e.Entries);????????????}????????}????????return?dbContext.SaveChanges();????}}2.?高級策略
在 .NET 中已經有開發人員幫我們開發出了強大的工具 Polly ,Polly 是一個 .NET 彈性和瞬態故障處理庫,允許開發人員以 Fluent 和線程安全的方式來實現重試、斷路、超時、隔離和回退策略。
首先我們需要定義一個枚舉類型
csharppublic?enum?RefreshConflict{????StoreWins,????ClientWins,????MergeClientAndStore}
然后根據不同的獲勝模式來刷新數據庫的值
{public?static?EntityEntry?Refresh(this?EntityEntry?tracking,
????????RefreshConflict?refreshMode){switch?(refreshMode)
????????{case?RefreshConflict.StoreWins:
????????????{
????????????????tracking.Reload();break;
????????????}case?RefreshConflict.ClientWins:
????????????{
????????????????PropertyValues?databaseValues?=?tracking.GetDatabaseValues();if?(databaseValues?==?null)
????????????????{
????????????????????tracking.State?=?EntityState.Detached;
????????????????}else
????????????????{
????????????????????tracking.OriginalValues.SetValues(databaseValues);
????????????????}break;
????????????}case?RefreshConflict.MergeClientAndStore:
????????????{
????????????????PropertyValues?databaseValues?=?tracking.GetDatabaseValues();if?(databaseValues?==?null)
????????????????{
????????????????????tracking.State?=?EntityState.Detached;
????????????????}else
????????????????{//當實體被更新時,刷新數據庫原始值
????????????????????PropertyValues?originalValues?=?tracking.OriginalValues.Clone();
????????????????????tracking.OriginalValues.SetValues(databaseValues);//如果數據庫中對于屬性有不同的值保留數據庫中的值#if?SelfDefine
??????????????????????databaseValues.PropertyNames?//?Navigation?properties?are?not?included.
??????????????????????????.Where(property?=>?!object.Equals(originalValues[property],?databaseValues[property]))
??????????????????????????.ForEach(property?=>?tracking.Property(property).IsModified?=?false);#else
????????????????????????databaseValues.Properties
????????????????????????????.Where(property?=>?!object.Equals(originalValues[property.Name],
????????????????????????????????databaseValues[property.Name]))
????????????????????????????.ToList()
????????????????????????????.ForEach(property?=>
????????????????????????????????tracking.Property(property.Name).IsModified?=?false);#endif
????????????????}break;
????????????}
????????}return?tracking;
????}
}
最后定義刷新狀態的方法
【END】
?熱 文?推 薦?
?@程序員,一文掌握 Web 應用中的圖片優化技巧!
?Google 搜索點擊量不到 50%?
?馬云馬斯克激辯:AI 是威脅還是被低估了?
?程序員易踩的 9 大坑!
?99年少年12歲時買下100枚比特幣, 如今卻將所有積蓄壓在一個不知名的代幣上,還放話將超越Libra!
?強推!阿里數據科學家一次講透數據中臺
?冠軍獎3萬元!CSDN×易觀算法大賽開賽啦
?可惜了,你們只看到“雙馬會”大型尬聊
?如何寫出讓同事無法維護的代碼?
你點的每個“在看”,我都認真當成了喜歡
總結
以上是生活随笔為你收集整理的scanf_s 发送访问冲突_程序员如何解决并发冲突的难题?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python抢票代码_教你用Python
- 下一篇: exe一机一码加密工具_文件夹加密软件有