scanf_s 发送访问冲突_程序员如何解决并发冲突的难题?
作者?| 羽生結(jié)弦
責(zé)編?| 胡雪蕊
出品 | CSDN(ID: CSDNnews)
在大多數(shù)的應(yīng)用中都會(huì)出現(xiàn)客戶端同時(shí)發(fā)送多個(gè)請(qǐng)求對(duì)同一條數(shù)據(jù)就行修改,這個(gè)時(shí)候就會(huì)出現(xiàn)并發(fā)沖突。我們一般的做法會(huì)有如下兩種:
1.?樂觀并發(fā)所謂的樂觀并發(fā)就是多個(gè)請(qǐng)求同時(shí)對(duì)同一條數(shù)據(jù)的更新,只有最后一個(gè)更新請(qǐng)求會(huì)被保存,其他更新請(qǐng)求將會(huì)被拋棄。
2.?悲觀并發(fā)所謂悲觀并發(fā)就是多個(gè)請(qǐng)求同時(shí)對(duì)同一條數(shù)據(jù)的更新,只有當(dāng)前更新請(qǐng)求完成或者被拋棄,才會(huì)執(zhí)行下一個(gè)更新請(qǐng)求,如果當(dāng)前更新請(qǐng)求未完成或者未被拋棄,那么后面所有的更新請(qǐng)求將會(huì)被阻塞。
通過上面的簡(jiǎn)單講解我們簡(jiǎn)單的了解了如何處理并發(fā)請(qǐng)求,那么下面我們來看一下上面兩種做法的具體講解和實(shí)現(xiàn)。
方法一
在 Entity Framework 中,默認(rèn)的解決方案是樂觀并發(fā),原因是當(dāng)出現(xiàn)并發(fā)情況的時(shí)候,內(nèi)部沒有任何對(duì)其他客戶端訪問同一行數(shù)據(jù)的限制。我們來看一下例子,我們?cè)跀?shù)據(jù)庫(kù)中存有一條數(shù)據(jù),數(shù)據(jù)如下圖所示:下面我們來修改一下 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 的形式實(shí)現(xiàn)了并發(fā)訪問。首先我們同時(shí)查詢出 id 等于1的人員,然后將 user1 中的 Name 修改為李四并提交,接著再把 user2 中的 Name 修改為王五并提交。這個(gè)時(shí)候我們?cè)俨樵償?shù)據(jù)庫(kù)就會(huì)發(fā)現(xiàn) Name 列被更新為了最后一次提交值王五,如下圖所示:上述操作發(fā)生了什么呢?我們來看一下,首先我們利用 db 從數(shù)據(jù)庫(kù)中讀取了 id 等于1的人員信息,此時(shí)該人員信息為張三,然后我們將 Name 值改為李四,并且提交到了數(shù)據(jù)庫(kù),在這個(gè)時(shí)候,數(shù)據(jù)庫(kù)中的Name值將不再是張三,而是李四。接著我們?cè)賹?user2 的 Name 值修改為王五,并提交的數(shù)據(jù)庫(kù),這個(gè)時(shí)候數(shù)據(jù)庫(kù)的 Name 列的值變?yōu)榱送跷濉I鲜銮闆r下,Entity Framework 將修改轉(zhuǎn)換為 update 語句時(shí)是利用主鍵來定位指定行,因此上面兩次操作都會(huì)成功,只不過最后一次修改的數(shù)據(jù)會(huì)最終持久化到數(shù)據(jù)庫(kù)中。但是這種方式存在一個(gè)巨大的隱患,例如在門票預(yù)售系統(tǒng)中,門票的數(shù)量是有限制的,購(gòu)票人數(shù)超過門票數(shù)量限制將會(huì)禁止購(gòu)買。如果利用 Entity Framework 默認(rèn)的樂觀并發(fā)模式,每次有并發(fā)請(qǐng)求購(gòu)票時(shí),每個(gè)請(qǐng)求都會(huì)減去門票數(shù)量,并且向數(shù)據(jù)庫(kù)中插入一條購(gòu)票信息,這樣一來永遠(yuǎn)是最后一個(gè)請(qǐng)求的數(shù)據(jù)會(huì)持久化到數(shù)據(jù)庫(kù)中,這樣就造成了門票預(yù)約人數(shù)超過了門票的限制數(shù)量。
針對(duì)上面所說的問題,我么可以利用如下兩種方式來解決:
1.?并發(fā) Token
利用這個(gè)方法我們只需在實(shí)體類對(duì)應(yīng)的 Map 文件的構(gòu)造函數(shù)中加讓類似下面的代碼即可:csharpProperty(p?=>?p.Name).IsConcurrencyToken();2.?行版本
通過行版本設(shè)置,我們需要為實(shí)體添加一個(gè)行版本子字節(jié)數(shù)組,代碼如下:?
csharppublic?byte[]?RowVersion?{?get;?set;?}然后將行版本字段映射進(jìn)數(shù)據(jù)庫(kù),這樣每次更新數(shù)據(jù)的時(shí)候都行版本字段也會(huì)跟著更新。最后我們?cè)趯?shí)體類對(duì)應(yīng)的 Map 文件的構(gòu)造函數(shù)中添加如下代碼即可:csharpProperty(p?=>?p.RowVersion).IsRowVersion();這樣在每次提交修改請(qǐng)求時(shí) Entity Framework 都會(huì)檢查數(shù)據(jù)庫(kù)中的行版本和當(dāng)前提交數(shù)據(jù)的行版本是否一致,如果一直就更新數(shù)據(jù)和行版本信息。上述兩種方法都將會(huì)引發(fā)并發(fā)異常,那么我們?cè)撊绾谓鉀Q這個(gè)異常呢?我們需要用到并發(fā)異常類( DbUpdateConcurrencyException )中的 Entries 屬性,該屬性是一個(gè)集合。我們需要調(diào)用集合中每個(gè)對(duì)象的 Reload 方法將數(shù)據(jù)庫(kù)中最新的值放在內(nèi)存中。這樣后續(xù)的實(shí)體值將和數(shù)據(jù)庫(kù)保持一致。完成這一步后,我們可以重新向數(shù)據(jù)庫(kù)提交更新數(shù)據(jù)。具體實(shí)現(xiàn)代碼如下: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();
????????????????????}
????????????????}
????????????}
????????}
????}
}這里需要注意的是這個(gè)方法并不是萬能的,只是將當(dāng)前客戶端的值成功存入數(shù)據(jù)庫(kù)中,這種情況被稱為客戶端獲勝,當(dāng)然了還有數(shù)據(jù)庫(kù)獲勝,以及數(shù)據(jù)庫(kù)和客戶端合并獲勝(這三個(gè)概念解決并發(fā)沖突的方式將在下一小節(jié)講解)。在講解這個(gè)問題前我們先來了解一下 Entity Framework 的原始值和更新后的數(shù)據(jù)庫(kù)值以及當(dāng)前值從哪里獲得。代碼如下:csharptry{??//more?code}catch?(DbUpdateConcurrencyException?e){????foreach?(var?item?in?e.Entries)????{????????//原始值????????var?ov?=?item.OriginalValues.ToObject();????????//更新后數(shù)據(jù)庫(kù)值????????var?dv?=?item.GetDatabaseValues().ToObject();????????//?當(dāng)前值????????var?nv?=?item.CurrentValues.ToObject();????}}從上面的代碼中我們可以看到獲取這三種值我們依然是從并發(fā)異常類的 Entries 屬性中獲得。看到這里一定會(huì)有人想到不利用 Reload 方法來更新內(nèi)存中的最新值,而是直接利數(shù)據(jù)庫(kù)值更新當(dāng)前內(nèi)存中的值,如果你想到這里說明你已經(jīng)掌握了解決并發(fā)沖突最簡(jiǎn)單的方法。那么我們就來看一下代碼:csharptry{????//more?code}catch?(DbUpdateConcurrencyException?e){????foreach?(var?item?in?e.Entries)????{????????Object?dv?=?item.GetDatabaseValues().ToObject();????????item.OriginalValues.SetValues(dv);????????ef.SaveChanges();????}}
方法二
上一小節(jié)中我們提到了客戶端獲勝、數(shù)據(jù)庫(kù)獲勝以及數(shù)據(jù)庫(kù)和客戶端合并獲勝,并且講解了原始值和更新后的數(shù)據(jù)庫(kù)值以及當(dāng)前值從哪里獲得的。在這一節(jié)將利用客戶端獲勝、數(shù)據(jù)庫(kù)獲勝以及客戶端和數(shù)據(jù)庫(kù)合并獲勝處理并發(fā)的方法。
1.?客戶端獲勝
當(dāng)調(diào)用 SaveChanges 方法時(shí),如果存在并發(fā)沖突將會(huì)引發(fā) DbUpdateConcurrencyException 異常,那么這個(gè)時(shí)候我們將調(diào)用 handleDbUpdateConcurrencyException 函數(shù)來處理異常并正確解決沖突,最后在調(diào)用 SaveChanges 方法重試提交數(shù)據(jù)。如果依然排除 DbUpdateConcurrencyException 異常,將不在進(jìn)行處理。我們來看以下代碼:
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());????????????????????????}????????????????????});????????????????}????????????}????????}????}}上述代碼中發(fā)生并發(fā)異常時(shí),將會(huì)將數(shù)據(jù)庫(kù)的值提交到內(nèi)存中,然后重新提交更新數(shù)據(jù)。2.?數(shù)據(jù)庫(kù)獲勝如果你想讓數(shù)據(jù)庫(kù)獲勝,那就簡(jiǎn)單了。再發(fā)生異常時(shí)不需做任何處理,只返回方法的返回值即可。我們將上一個(gè)例子的代碼更新一下: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;????????????????}????????????}????????}????}}上面代碼運(yùn)行后,只有李四會(huì)被更新到數(shù)據(jù)庫(kù)中,王五因?yàn)椴l(fā)沖突且異常捕獲后沒有進(jìn)行任何處理而不會(huì)存入數(shù)據(jù)庫(kù)。
3.?數(shù)據(jù)庫(kù)和客戶端合并獲勝
這種方式是最復(fù)雜的,需要合并數(shù)據(jù)庫(kù)和客戶端的數(shù)據(jù),如果用到此方法我們需要謹(jǐn)記如下兩點(diǎn):
如果原始值與數(shù)據(jù)庫(kù)中的值不通,就說明數(shù)據(jù)庫(kù)中的值已經(jīng)被其他客戶端更新,這時(shí)必須放棄當(dāng)前的更新,保留數(shù)據(jù)庫(kù)的更新;
如果原始值與數(shù)據(jù)庫(kù)的值相同,代表不會(huì)發(fā)生并發(fā)沖突,按照正常處理流程處理即可。
同樣,我們將上面的例子按照上面兩點(diǎn)進(jìn)行修改:
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 捕獲并發(fā)異常,其實(shí)我們也可以自定義 SaveChanges 的擴(kuò)展方法來處理并發(fā)異常。下面我們就來看一下具體的兩種策略。
1.?普通策略
這個(gè)策略非常簡(jiǎn)單,就是利用循環(huán)來實(shí)現(xiàn)重試機(jī)制,代碼如下:
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.?高級(jí)策略
在 .NET 中已經(jīng)有開發(fā)人員幫我們開發(fā)出了強(qiáng)大的工具 Polly ,Polly 是一個(gè) .NET 彈性和瞬態(tài)故障處理庫(kù),允許開發(fā)人員以 Fluent 和線程安全的方式來實(shí)現(xiàn)重試、斷路、超時(shí)、隔離和回退策略。
首先我們需要定義一個(gè)枚舉類型
csharppublic?enum?RefreshConflict{????StoreWins,????ClientWins,????MergeClientAndStore}
然后根據(jù)不同的獲勝模式來刷新數(shù)據(jù)庫(kù)的值
{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
????????????????{//當(dāng)實(shí)體被更新時(shí),刷新數(shù)據(jù)庫(kù)原始值
????????????????????PropertyValues?originalValues?=?tracking.OriginalValues.Clone();
????????????????????tracking.OriginalValues.SetValues(databaseValues);//如果數(shù)據(jù)庫(kù)中對(duì)于屬性有不同的值保留數(shù)據(jù)庫(kù)中的值#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;
????}
}
最后定義刷新狀態(tài)的方法
【END】
?熱 文?推 薦?
?@程序員,一文掌握 Web 應(yīng)用中的圖片優(yōu)化技巧!
?Google 搜索點(diǎn)擊量不到 50%?
?馬云馬斯克激辯:AI 是威脅還是被低估了?
?程序員易踩的 9 大坑!
?99年少年12歲時(shí)買下100枚比特幣, 如今卻將所有積蓄壓在一個(gè)不知名的代幣上,還放話將超越Libra!
?強(qiáng)推!阿里數(shù)據(jù)科學(xué)家一次講透數(shù)據(jù)中臺(tái)
?冠軍獎(jiǎng)3萬元!CSDN×易觀算法大賽開賽啦
?可惜了,你們只看到“雙馬會(huì)”大型尬聊
?如何寫出讓同事無法維護(hù)的代碼?
你點(diǎn)的每個(gè)“在看”,我都認(rèn)真當(dāng)成了喜歡
總結(jié)
以上是生活随笔為你收集整理的scanf_s 发送访问冲突_程序员如何解决并发冲突的难题?的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python抢票代码_教你用Python
- 下一篇: python判断英文字母_Python判