本篇文章以我在真實項目中遇到的數據併發問題作爲背景,講解問題出現的原因及解決的辦法,以及從中得到的反思。併發中踩過很多坑,可能還有不足的地方,但會一直學習成長,現在將學習到的東西記錄下來,,,,努力努力。
一:併發操作出現的原因
原因:多個動作在一瞬間同時操作同一數據
現象:
- 多人在某一瞬間通過相同的方式操作同一條數據
- 多人在某一瞬間通過不同的方式操作同一條數據
- 在某一瞬間,同一動作,多次執行
二:併發舉例及解決辦法
針對上述的三種的情況,分別以實際情況進行舉例。
【多人在某一瞬間通過相同的方式操作同一條數據】
1.某倉庫系統有一品牌商品A,商品A在數據庫中只允許存在一條記錄,庫存的數量是這條數據的一個字段,現有庫存100件,在某一天到貨了1000件。由於數量比較大,現在需要10名操作員去處理這1000件商品進行入庫,操作的途徑都是使用PDA掃描完成後進行入庫。我們假設至少存在1名以上的操作員同時進行入庫操作。這樣就可以滿足上述條件【多人在某一瞬間通過相同的方式操作同一條數據】。在這種情況下,如果不進行處理,就會導致數據錯亂,錯亂的原因簡單說就是在雙方寫數據時沒有獲取到最新的數據庫數據。
解決方法:
方法一: 加鎖。加鎖是比較常用的方法。從系統的架構上來說,鎖被分爲單機鎖和分佈式鎖。如果系統只是部署在單一機器上,可以簡單通過java提供的各種鎖來進行操作。如果系統被部署在多臺機器上,可以使用redis來實現分佈式加鎖。這兩種加鎖方式從某種意義上來說是悲觀鎖。上述的問題,我們可以使用商品的唯一屬性,比如id或者商品的唯一條碼來進行加鎖。
方法二:數據庫樂觀鎖。數據庫樂觀鎖幾乎適用於所有的併發場景。使用方法:在數據庫表中增加一個版本號字段,每一次更新和刪除時把當前持有的對象版本號和數據庫中最新的版本號進行比對,如果相同則驗證通過,不然則操作失敗。
方法三:使用消息隊列。這種方式在消息過多時,對庫存的處理可能不會特別及時。由於庫存一般是需要比較及時的可見,所以這種方式並不建議。
【多人在某一瞬間通過不同的方式操作同一條數據】
2. 還是按照上述的背景來說。在這10名操作員進行入庫的同時,還有至少1名操作員對A商品進行出庫操作。我們假設入庫時沒有併發問題,但是其中一個入庫和一個出庫同時操作了A商品的庫存,通過兩種不同的方式對庫存進行操作。如果不進行處理,庫存也會出現數據錯亂的問題。
解決方法:
方法一: 加鎖。這個時候使用普通的單機鎖已經沒有意義了,可以使用分佈式鎖,依舊使用唯一屬性來進行加鎖,儘管方法不同,但關鍵的key是一樣的,這樣就可以鎖住操作。
方法二:數據庫樂觀鎖。
對於上述的問題,我擴展一下,如果是一批商品,你總不能一個一個進行加鎖處理吧,那樣效率也太低了。所以這種情況下,簡單的加鎖已經不能滿足現在的需要了。所以數據庫樂觀鎖又重新出現了。在批量更新時,發現其中任何一個商品的版本號不一致,立即報錯回滾。
【在某一瞬間,同一動作,多次執行】
3.這一種情況屬於請求重複提交,同樣,如果沒有進行處理,數據也會出現問題。
一個用戶在入庫時重複提交了兩次,這樣在不考慮其它併發的影響下,庫存中的數據會多增加一次,但在入庫歷史中卻只能看見一次記錄,這樣肯定是不可接受的。
解決方法:
方法一:前臺可以在按鈕或鏈接第一次點擊後立刻禁用。這樣可以有效的解決絕大部分的問題。但是由於操作端千變萬化,這種方式並不能夠完全解決問題。
方法二:後臺生成一個隨機數放在前臺,前臺在訪問後臺時,將隨機數傳輸到後臺進行驗證,第一次驗證通過即刻銷燬, 隨機數可以存在redis或session中,一般用於表單提交。但是這種方式還是存在缺陷,如果同一個頁面有多個請求,一個隨機數就完全不夠用了。
方法三:nginx可以控制ip在同一時間內對服務的訪問頻率。比如入庫時,如果進行了多次點擊,發送了多次請求,在這1秒中,系統只接收第一次請求。
三:總結
處理併發的最終原理其實就是:將用戶的並行操作變成串行操作。
在解決併發問題時,從操作端到服務端,再到數據庫,都需要進行處理,層層過濾。
前端:防止多次點擊。
服務端:對相同數據的操作寫在同一個服務中。
數據庫:樂觀鎖一定要使用。有需要的話,數據庫的聯合唯一索引也要準備。
四:擴展
到此爲止,有相關項目開發經驗(倉庫系統)的讀者可能會發現有些問題。問題在於:庫存的設計不夠優雅,才導致了很多併發情況的產生。比如,10個操作員在入庫時,爲什麼需要操作庫存中的A商品去增加庫存呢?其實,所有的入庫和出庫,包括盤點等,都是在爲了用戶能夠及時的看見庫存而已。既然知道庫存的計算方式,我們完全可以計算出對應的庫存,並且還能減少大量的併發操作。
接下來的文章,我將設計一個高可用,並且併發操作較少的倉庫系統。有興趣的同學可以關注我啊!
說的有什麼不對的地方,還請大家多多指正。
感謝!感謝!感謝!