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