《阿裡Java開發規範》應該是衆多程式猿多年來,在使用Java的過程中,根據踩過的雷趟過的坑,總結出來的“血的教訓”或“踩坑手冊”。但就像《葵花寶典》,即便是讀過一百遍也成功自宮,也不見得能馬上能成為武林高手,因為沒練過。搞軟體工程就像練武功需要實操,沒有實戰經驗很難成為高手。評判是不是高手的标準是什麼?如果僅從軟體工程的角度看,很重要的一點就是傳遞的軟體在各種高業務壓力、異常情況下壓不垮、跑不死、業務正常運作。而新手往往很難做出這樣健壯的系統,因為健壯的系統需要規避無數的雷和坑。如果不知道這些可能存在的這樣或那樣的坑或雷,自然談不上填上這些坑、避開這些雷。而且,就算在書本或規範上看過這些“寶典”,也知道這些坑的存在,但沒有跌坑踩雷的親身經曆,在碰到跌坑踩雷的場景時,也可能忽略而忘記“寶典”中的“金玉良言”。為什麼會忽略、為什麼會不夠重視,因為你沒有真的痛過。痛過才能成熟,相信我,這是真的^o^
是以,聊一聊為什麼會有這樣的開發條例,如果不按規範來可能出現哪些問題,了解背後的故事,可能比讀一百遍規範來得更有效果。
> 【6.11】【強制】并發修改同一記錄時,避免更新丢失,需要加鎖。要麼在應用層加鎖,要麼在緩存 加鎖,要麼在資料庫層使用樂觀鎖,使用 version 作為更新依據”。
到底什麼是“更新丢失”呢?到底會導緻多嚴重的問題?初看這個描述,說實話一點感覺沒有,如果你沒有碰到過這類并發問題導緻的嚴重事故,看完這條規範後,大機率情況下該踩的雷還得踩。
從我碰到的出鏡率比較高的問題開始,分享一下開發系統中遇到過的場景。
在IOT場景下,不同的終端裝置需要配置設定不同的物聯網卡進行資料通信,最基本的限制就是一張卡隻能分給一個裝置。如果,一張卡分給多個裝置将會導緻卡被交替使用,不同的終端使用同一張卡交替上線下線,進一步導緻營運商停卡和客戶投訴。即便是少量的一卡多分在業務上也是很難接受的。而分卡服務必定需要部署多個服務節點,單個節點肯定無法滿足大業務量需要,并發/并行地選中并占用同一張卡是必然會出現的(并發/并行處理的概念請自修)。由于大量終端請求到達服務端後,會被後端無狀态服務的多個節點處理,不同的終端分卡請求會落在不同的節點上,這時不同的終端可能會被不同的服務節點配置設定同一張卡。比如節點A的終端TA請求占卡1,表示為1-TA,和節點B的終端請求TB占卡1,表示為1-TB。如果這2個不同節點上的請求請求以前後相差1毫秒的時間更新了資料庫,導緻的結果是TA分到了卡1,并傳回給終端,緊接着TB也被認為分到了卡1并傳回給終端,一卡多分。但系統中的最終記錄是1-TB,終端TB占有卡1。可一毫秒前從業務的角度看是終端TA占的卡1。于是在接下的一段時間,終端TA和終端TB輪流使用用卡1,卡1會被不斷地上線又踢下線又上線,循環反複。如下圖

是以“更新丢失”更準确的描述是“更新覆寫”,當一個程序修改某條業務資料後會被另一個完全不知情的程序覆寫資料,導緻在整個業務層面資料狀态的不一緻,是以“需要加鎖”。但是規範中并沒有明确應該怎麼選擇在什麼樣的場景下使用什麼樣的鎖。
為了應用服務能夠平滑橫向擴容,沒有對業務狀态有特定要求的場景,我們通常會把應用設計成無狀态應用,當業務壓力增大隻需要通過增加應用服務節點就能滿足提升處理能力的要求。對于上述占卡的應用場景,系統選擇多應用節點并行處理分卡請求,來提升系統吞吐量。如果僅僅選擇在單應用節點上加鎖,不能解決多節點多程序并行處理導緻的資料沖突。但如果選用緩存(Redis)鎖,把卡資源作為鎖對象,則需要将資料庫中的記錄和緩存做同步,本身就涉及資料同步和資料一緻性問題。在需要對大量資料加鎖或長時間加鎖時,不建議優先考慮使用悲觀鎖,因為一次分卡請求可能需要讀取大量的卡資源做排序、過濾,鎖住大量的卡會極大降低并發吞吐量。更好的方式是使用樂觀鎖,并且是直接在資料庫層使用樂觀鎖,用version(UUID)作為更新依據。更新SQL ,UPDATE table XXX where version = UUID。(資料庫樂觀鎖的原理請自修)。隻是在選卡和占卡時,需要做一些功課來降低沖突的可能性,但能保證在任何時候卡資料不被過期(髒)資料覆寫。當然,這種一緻性隻是在服務端達成的一緻性。終端和服務端對占用同一張卡達成的一緻性問題,需要另外的分布式一緻性方案才能解決。