對於企業應用來說,完全不涉及到併發的問題,基本是不可能的。因為對於一個應用中很多的事情都是同時進行的。併發可能發生在資料獲取,服務呼叫乃至於使用者互動中。併發問題有兩個重要的解決方案,一個是隔離,另一個是不變性。
併發問題會發生在多個執行單元同時訪問同一資源的時候,此時,一個好的方法就是分好“蛋糕”,讓每一個執行單元都能訪問到各自的資源。好的併發設計就是:找到建立好隔離區的辦法,然後通過分析工作流讓隔離區能夠完成儘可能多的任務。
在共享資料可以改變的情況下,併發問題就有可能發生。從實際的場景出發,同時有兩個客戶詢問兩位服務員是否還有某一貨品時,兩位服務員各自去檢視了一下系統並回復客戶還有一份,兩位客戶中一定有一位會失望。那麼這件事情的解決方案就是新增隔離區(購物車),服務員把當前貨品放入客戶的購物車成功後告知使用者,然後失敗的一方就可以告知使用者貨品已經銷售一空。雖然存在已購使用者退貨的可能,但無疑比前一結果要好太多。這也就是下文中所說的悲觀鎖。
下面我們開始介紹兩種併發控制策略:
樂觀和悲觀併發控制
在某個系統中,同時有兩個企業員工 A 和 B 想要編輯同一個使用者資訊。此時 A 和 B 都獲取到了使用者的資訊資料。然後他們兩個進行了修改,A 員工先完成了操作並且進行了提交。然後 B 員工完成了操作也進行了提交。此時系統中的這個使用者資訊只保留了 B 提供的資料,而丟棄了 A 員工的資料。這可能會造成一些難以預料的問題,甚至有可能導致他們丟掉工作。雖然可以通過操作日誌來追溯到是哪個員工操作了資料,但這個資訊沒有任何意義,因為系統並沒有讓任何員工得知修改這一情況。
當一些可變資料無法隔離時候,我們可以用兩種不同的控制策略:樂觀鎖策略和悲觀鎖策略。樂觀鎖用於衝突檢測,悲觀鎖用於衝突避免。
悲觀者策略非常簡單,當 A 使用者獲取到使用者資訊時系統把當前使用者資訊給鎖定,然後 B 使用者在獲取使用者資訊時就會被告知別人正在編輯。等到 A 員工進行了提交,系統才允許 B 員工獲取資料。此時 B 獲取的是 A 更新後的資料。
樂觀者策略則不對獲取進行任何限制,這時候我們可以在使用者資訊中新增版本號來告知使用者資訊已被修改。樂觀鎖要求每條資料都有一個版本號,同時在更新資料時候就會更新版本號,如 A 員工在更新使用者資訊時候提交了當前的版本號。系統判斷 A 提交的時候的版本號和該條資訊版本號一致,允許更新。然後系統就會把版本號修改掉,B 員工來進行提交時攜帶的是之前版本號,此時系統判定失敗,要求 B 重新獲取資料和版本號,然後再一次進行提交。
樂觀鎖和悲觀鎖進行選擇的標準是: 衝突的頻率和嚴重性。如果衝突的結果對於使用者是難以接受的,我們只能採用悲觀鎖策略。如果衝突的結果不會很嚴重,或者頻率也較低,我們就可以選擇樂觀鎖,它更容易實現,也具有更好的併發性。
當然,我們也可以對樂觀鎖進行一些優化,把更新時間(作為版本號)和更新使用者新增到資訊中,如此以來,系統就可以告知 B 員工該條資訊被修改過,以及在何時何人操作。系統還可以提供給 B 新的更新時間以及是否強制更新的選擇。當然,甚至可以基於業務需求以及日誌資訊等來告知 B 員工之前具體修改的資訊。
死鎖
使用悲觀鎖技術有一個特別的問題就是死鎖,即使用者在已經獲取鎖的情況下還想要獲取更多的鎖。以最早的兩個客戶的問題來說,就是水果蛋糕需要獲取水果和蛋糕,兩個使用者各有其中一種,並期望獲取對方東西。
解決死鎖的方法是檢測處理和超時控制。
檢查處理會檢測出死鎖發生並且會選擇一個“犧牲者”,讓他放棄他所擁有的已保證另外一個客戶可以獲取水果蛋糕。而超時控制則是給每個鎖新增一個超時時間,一旦達到了超時時間,當前的購物車裡面的物品就被清掉。
超時控制和檢測機制用於已經發生了死鎖的情況,而另外的方法則是避免死鎖的發生。防止死鎖的方法就是在使用者獲取鎖的時候就獲取所有可能需要的鎖,粗力度鎖(這很保守,但很有效),即水果蛋糕不是由兩個貨品組合而成的。
粗力度鎖是覆蓋多個資源的單個鎖,這樣會簡化多個鎖帶來的複雜性。這其實也會發生在樂觀鎖的過程中,例如使用者和使用者相關地址資訊,如果使用者地址資訊修改後也會更改使用者資訊,這樣如何獲取和設定樂觀鎖呢?我們需要尋找到一組資源的核心。
同時,找到一組資源的核心也會使得開發的程式碼邏輯更加清晰。大家不妨想一下,在資料庫層面的操作中,是選擇先更新子表然後再去更新主表這樣的邏輯順序更好,還是以主表為入口進行更新修改更好呢?
鼓勵一下
如果你覺得這篇文章不錯,希望可以給與我一些鼓勵,在我的 github 部落格下幫忙 star 一下。