DDD實戰:應對并發挑戰,五個技巧讓你輕松應對
當前位置:點晴教程→知識管理交流
→『 技術文檔交流 』
并發管理是一個高級話題,也是設計中的難點,一不小心就會出問題。讓每個開發人員都成為并發高手又是一件不太現實的事,但,好在存在很多并發管理的成熟方案,業務開發者按照場景進行落地即可。
在業務開發中,事務一致性核心在于“原子性”,則并發管理的核心在于“隔離性”。
1. 無處不在的并發并發管理是指在多個用戶同時訪問、修改同一數據時,如何保證數據的準確性、一致性和完整性的一系列管理措施。 并發無處不在是指在當前的業務系統和應用程序中,幾乎所有的操作都是并發的。無論是網絡請求、數據庫操作、I/O讀寫操作等,都可能在同一時刻被多個線程或進程同時執行。這意味著在業務開發中,必須充分考慮并發處理問題,避免出現數據競爭、死鎖等問題,同時合理利用多線程、協程等技術來提高系統的性能和處理能力。 1.1. 常見業務流程首先看以下流程: 圖片 這是一個聚合根更新操作,包括:
也許還沒有使用DDD,對聚合根不太熟悉,那再看一個流程: 圖片 這是一個更為通用的數據編輯流程,包括:
仔細對比這兩張圖,其實他們都在做同樣的事情:
在這里便存在并發問題。 1.2. 并發問題上面所提到的流程是否存在并發問題,仔細看下圖: 圖片 同一個流程,操作同一數據,只是操作順序不同,也會出現并發安全問題:
看起來沒什么問題,但 V3 是業務期望的嗎?V2 的變更又去哪里了呢? 此時,V2 被 V3 覆蓋,V2 的變更丟失了。 如果還不清楚,明確業務操作為 count++,如下圖所示: 圖片 對數據庫的 count 進行累加操作
操作完成后,最終結果為2。實際期望結果為3,Action2 的修改被 Action1 覆蓋,導致一次累加操作被覆蓋。 當然,這僅僅是同一流程下的并發問題,多流程間也存在并發問題: 圖片 對于同一記錄,自增流程和設置流程并發執行,同樣發生了寫覆蓋。 2. 局部串行
2.1. 線程方案如下圖所示: 圖片 訂單流程中的核心操作:
由于多個訂單間不存在關系,可以并發執行;但同一訂單,必須保障業務執行順序。 什么是“局部串行”:
其中分發器是核心,它連接訂單事件和后臺線程:
這樣,相同訂單號的訂單事件均由同一個線程處理,從而保證局部串行化。不同訂單之間,不存在相互影響,可以在多個線程中并行執行。 2.2. MQ 方案當然,內存操作存在數據安全問題(重啟任務會丟失),不少MQ也提供了相關功能,以 RocketMQ 的順序消息為例,如下圖所示: 圖片
局部串行對性能存在一定影響,系統最大的并發量為 partition 數量。如果出現增加 Worker 節點無法提升系統吞吐時,需要擴展 partition 數量。 【備注】在系統做 rebalance 時,可能會出現短暫的消息混亂,通常情況下,業務是可接受的。如果必須保障強順序,如 binlog 場景,只能使用一個 partition,但會極大的影響性能。 3. 最后寫勝出
如下圖所示: 圖片
此時,不會出現并發問題。但由于時序問題,數據的最終狀態以“最后更新”為準。 4. 原子指令
比如在庫存扣減的場景,可以使用 Redis 或 DB 的原子指令進行操作。 4.1. Redis使用 Redis 的 incr 指令: 圖片 由于 redis 指令是單線程處理不存在并發問題,直接使用 incr key -1 質量對數量進行扣減。當然,這樣可能會出現數量為負值情況,此時可以引入 LUA 腳本進行保障: -- KEYS[1]: 庫存鍵的名稱,例如 stock:1001 -- ARGV[1]: 要扣減的數量 local stock = tonumber(redis.call('GET', KEYS[1])) -- 判斷扣減的數量是否大于庫存數量 if stock < tonumber(ARGV[1]) then return -1 end -- 扣減庫存,并返回剩余的庫存數量 stock = stock - tonumber(ARGV[1]) redis.call('SET', KEYS[1], stock) -- 返回剩余的庫存數量 return stock1.2.3.4.5.6.7.8.9.10.11.12.13.14.15. 4.2. MySQL同樣的操作也可以在 MySQL 中操作,如下圖所示: 圖片 也可避免扣減為 負值的情況,如下圖所示: 圖片 新增對 count 的條件判斷,通過操作結果控制不同的流程:
5. 樂觀鎖
業務中使用最多的場景仍舊是 讀-改-寫,此時最佳處理方案便是樂觀鎖。 圖片 相對于數據更新,樂觀鎖方案只是增加了 version 判斷,并未引入其他復雜性,對性能影響非常小。
對于聚合根來說,這是數據更新最常見的并發保障機制。 6. 悲觀鎖當一個事務(線程)正在使用某個數據時,其他事務(線程)就不能訪問該數據,必須等待鎖釋放后才能訪問。悲觀鎖能夠保證數據的一致性,但是對并發性能影響比較大。 悲觀鎖是最后的辦法,由于其對性能沖擊較大,不到萬不得已不要隨便使用。 6.1. 數據庫悲觀鎖
使用 for update 加載數據,操作如下: 圖片 for update 語句將對數據進行強制加鎖,只有在事務提交后,鎖才會釋放。如圖所示,for update 會對操作進行強制排序,最終使單條操作變成串行化,從而影響并發度最終影響系統性能。 6.2. 分布式鎖
比如,在訂單系統中,對于特價商品一個用戶只能購買一次,如下圖所示: 圖片 該流程存在并發問題,可能導致一個用戶下單多次:
由于是新增場景,沒有什么資源可鎖定,所以樂觀鎖方案無法落地,此時就需要引入分布式鎖,如下圖所示: 圖片 以 user 為單位申請分布式鎖,保證同一用戶只有一個線程能進行被保護流程,從而保證同一用戶不會購買多次。 4. 小結并發管理是一個高級話題,也是設計中的難點,一不小心就會出問題。讓每個開發人員都成為并發高手又是一件不太現實的事,但,好在存在很多并發管理的成熟方案,業務開發者按照場景進行落地即可:
責任編輯:武曉燕來源: geekhalo
該文章在 2023/10/28 10:37:26 編輯過 |
關鍵字查詢
相關文章
正在查詢... |