一文說清楚併發

併發在程序開發中是經常碰到的,怎麼解決併發一直是我們需要解決的難題。併發的定義是多個請求同時訪問一個接口,或者一個函數。併發可能帶來的危險:

  1. 大量讀請求會導致訪問變得緩慢,用戶操作不流暢
  2. 資源競爭,死鎖,程序崩潰

併發也分爲兩種情況,一種就是讀併發,一個就是寫併發,也許讀寫併發都會有。針對這兩種情況,我們要不同的處理。如果是併發是讀的瓶頸,就要提升查詢的效率,第一個很簡單的就是添加緩存,給熱點數據放到redis或者其他nosql數據庫,減少數據庫的壓力,然後就是給DB加索引,再不行就做讀寫分離,還不行就分庫分表;針對寫瓶頸我們可以有兩種解決方案:

  1. 用事務處理,把併發交給數據庫處理
  2. 在寫的地方加上lock
  3. 用隊列處理

併發寫數據首先要滿足的一點就是保持數據的完整性,不能在多線程情況下,造成數據錯亂,比如出現超賣,超買這種情況。
針對第一種解決方法,我們知道每個數據庫都有事務隔離級別,事務級別從低到高分別是(1.讀未提交,髒讀 2.讀已提交 3.可重複讀 4.串行化)SQL server默認的隔離級別是讀已提交,mysql默認的隔離級別是可重複讀。在可能出現寫併發的地方用一個事務包裹,然後提交到數據庫,數據庫在寫數據的事務會有數據庫的鎖機制,數據庫的鎖分爲讀鎖和寫鎖,也就是共享鎖和排他鎖,也有行鎖和表鎖,如果是寫數據,就會觸發排他鎖,不讓其他的線程訪問這行數據,或者整個表的數據(根據索引情況是鎖行還是鎖表)。這樣就可以保證數據的正確性。
針對第二種方案,在可能發生的併發的代碼前,用lock關鍵字鎖住,相當於請求這個臨界區,其他的線程請求時,要等待上一個線程處理完,才能進入這個臨界區。

也有人說用樂觀鎖和悲觀鎖可以解決併發。
樂觀鎖( Optimistic Locking ) 相對悲觀鎖而言,樂觀鎖機制採取了更加寬鬆的加鎖機制。悲觀鎖大多數情況下依靠數據庫的鎖機制實現,以保證操作最大程度的獨佔性。但隨之而來的就是數據庫性能的大量開銷,特別是對長事務而言,這樣的開銷往往無法承受。而樂觀鎖機制在一定程度上解決了這個問題。樂觀鎖,大多是基於數據版本( Version )記錄機制實現。何謂數據版本?即爲數據增加一個版本標識,在基於數據庫表的版本解決方案中,一般是通過爲數據庫表增加一個 “version” 字段來實現。讀取出數據時,將此版本號一同讀出,之後更新時,對此版本號加一。此時,將提交數據的版本數據與數據庫表對應記錄的當前版本信息進行比對,如果提交的數據版本號等於數據庫表當前版本號,則予以更新,否則認爲是過期數據。
樂觀鎖其實就是不加鎖,寫數據的時候會用一個版本號判斷,根據自己的思路要解決併發請求,但是這並不能解決髒數據的問題。這樣能解決數據庫鎖行或者鎖表的時候的性能開銷。因此樂觀鎖適用於併發讀。
最後談談,EFcore對於樂觀併發的做法:
併發控制在 EF Core 中的工作原理
配置爲併發令牌的屬性用於實現樂觀併發控制:每當在 SaveChanges 期間執行更新或刪除操作時,會將數據庫上的併發令牌值與通過 EF Core 讀取的原始值進行比較。
如果這些值匹配,則可以完成該操作。
如果這些值不匹配,EF Core 會假設另一個用戶已執行衝突操作,並中止當前事務。
另一個用戶已執行與當前操作衝突的操作的情況被稱爲_併發衝突_。
數據庫提供程序負責實現併發令牌值的比較。
在關係數據庫上,EF Core 會對任何 WHERE 或 UPDATE 語句的 DELETE 子句中的併發令牌值進行檢查。 執行這些語句後,EF Core 會讀取受影響的行數。
如果未影響任何行,將檢測到併發衝突,並且 EF Core 會引發 DbUpdateConcurrencyException。

他們用了一個併發令牌的概念ConcurrencyCheck,其實是一個標記,如果是標記的字段有併發系統會檢測,然後會拋出一個異常,讓你去解決衝突。

        public async Task<bool> UpdateUser(User user)
        {
            User updateUser;
            updateUser = await _absDbContext.Users.FindAsync(user.Id);
            if (updateUser == null)
                return false;
            if (!string.IsNullOrEmpty(user.Name))
            {
                updateUser.Name = user.Name;
                //這裏模擬另一個併發請求,在sex併發標記字段做更改
                await _absDbContext.Database.ExecuteSqlCommandAsync(
     "UPDATE users SET Sex = 40 WHERE Id = 1");
                var saved = false;
                //如果沒有解決併發,一直重試
                while (!saved)
                {
                    try
                    {
                        await _absDbContext.SaveChangesAsync();
                        saved = true;
                    }
                    catch (DbUpdateConcurrencyException ex)
                    {
                        foreach (var entry in ex.Entries)
                        {
                            if (entry.Entity is User)
                            {
                                var proposedValues = entry.CurrentValues;
                                var databaseValues = entry.GetDatabaseValues();

                                foreach (var property in proposedValues.Properties)
                                {
                                    var proposedValue = proposedValues[property];
                                    var databaseValue = databaseValues[property];
                                    //在這裏你可以選擇要保存的值,解決併發衝突
                                }
                                entry.OriginalValues.SetValues(databaseValues);
                            }
                            else
                            {
                                throw new NotSupportedException(entry.Metadata.Name);
                            }
                        }
                    }
                }


                return true;
            }
            return false;
        }

很簡單,就是修改用戶信息的時候,我在sex字段添加了ConcurrencyCheck標記,當有兩個進程訪問的時候,如果有一個要修改sex,就會拋出一個DbUpdateConcurrencyException。
處理併發衝突的常規方法是:
1.在 DbUpdateConcurrencyException 期間捕獲 SaveChanges。
2.使用 DbUpdateConcurrencyException.Entries 爲受影響的實體準備一組新更改。
3.刷新併發令牌的原始值以反映數據庫中的當前值。
4.重試該過程,直到不發生任何衝突。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章