SpringMVC併發請求線程安全問題案例分析

背景:一個人的成長在於你經歷了多少,正如古語有云“讀萬卷書,不如行萬里路。”做技術尤其如此,要想快速成長,必須先多寫代碼,多思考,多總結,當然還可以通過幫助別人解決問題來驗證或者激勵自己的成長。今天這篇文章主要是基於一個朋友在實際開發中出現的一個併發案例,在幫助其解決的過程中,發現自己也有很多的知識誤區,遂寫此篇以作記錄,同時也分享給大家。

一,案例描述

①,業務需求

用戶通過系統生成邀請二維碼圖片分享給朋友,朋友通過掃描二維碼關注公衆號,記錄掃碼關注人數,當邀請關注人數達到十個時,用戶將獲得平臺免費提供的學習電子書。

②,原代碼實現邏輯

每當用戶掃碼關注公衆號後,系統獲得消息通知,這是先查詢數據庫做判斷,如果目前通過掃碼關注的人數少於10人,則進行++自增操作,否者提示用戶,掃碼關注已經達到十人,贈送電子書給用戶。

③,問題描述

當出現多個人同時掃碼關注的時候,出現併發問題,系統累計關注人數統計出現誤差,實際關注人數大於10人,但是系統提示還是差一個人。如下是案例截圖:


④,問題分析

當多個用戶同時掃碼時,向服務發送請求,此時tomcat容器默認是NIO的運行模式,通過線程池創建了多個線程,並開始執行業務代碼,朋友的業務服務是基於SpringBoot實現的,當然SpringBoot Web就是基於SpringMVC來做的,通常在默認的情況下,SpringMVC的Controller控制器是單例模式,也就是說多線程併發的時候,控制器的成員變量都是共享的,此時如果產生併發的時候,業務代碼是數據庫的非原子操作,或者Java代碼裏面有非線程安全的代碼塊,沒有考慮線程安全問題的話就可能會出現如上Bug。(在此,我也反省一下,當時我第一反應就是,這個線程安全問題應該是 出現在++自增操作運算符,因爲在Java裏面自增運算符是非線程安全的,哎,發現自己還是 too young too simple ! )。

二,技術延伸

多線程併發安全問題一直困擾着廣大的程序員,但是其實如果,我們搞清楚了“背後的故事”其實也就那麼回事,所以在講解決方案之前,我還是拋磚迎玉給大家稍微科普一下基礎知識 。

名詞解釋

多線程:多線程是指,對於一個進程,同一時刻上下文環境中存在大於1的線程數量,通常我們就稱之爲多線程。

併發:併發是指,在同一時間區間內,有多個任務在同時進行,通常稱之爲併發。(當然這個代表個人觀點,參考資料源於黃文海老師的《多線程編程實戰指南》)。

並行:並行是指,在同一時刻,有多個任務同時進行,通常稱之爲並行。(當然這個代表個人觀點,參考資料源於黃文海老師的《多線程編程實戰指南》)。

線程安全:當多線程訪問時,採用了加鎖機制,當一個線程訪問該類的某個數據時,進行保護,其他線程不能進行訪問直到該線程讀取完成,其他線程纔可以使用。不會出現數據不一致或者數據污染。

原子操作:如果這個操作所處的層的更高層不能發現其內部實現與結構,那麼這個操作是一個原子操作。原子操作可以是一個步驟,也可以是多個操作步驟,但是其順序不可以被打亂,也不可以被切割而只是執行其中一部分。將整個操作視作一個整體是原子性的核心特徵。

知識科普:

SpringMVC:大家都知道依賴注入(IOC)和AOP是Spring的兩大核心特性,對於SpringMVC也是基於Spring的對象管理,SpringMVC是基於方法的攔截,當然這就可能存在非線程安全情況,通常默認情況下,控制器生成的是單例,如果要聲明多例可以通過註解 @Scope("prototype") 聲明。

數據庫事物的隔離級別:數據庫的隔離級別常常分爲四個層級,這個是SQL標準規定。分別如下:

SQL標準定義了四類隔離級別,包括了一些具體規則,用來限定事物內外的哪些改變是可見的,哪些事不可見的。低級別的隔離級一般支持更高的併發處理,並擁有更低的系統開銷。

Read Uncommited (讀取未提交內容)

  在該隔離級別,所有事物都可以看到其他未提交事物的執行結果。本隔離級別很少用於實際應用,因爲它的性能也不比其他級別好多少。讀取未提交的數據,也被稱之爲髒讀(Dirty Read)。

Read Commited (讀取提交內容)

這是大多數數據庫系統的默認隔離級別(但不是Mysql默認的)。它滿足了隔離的簡單定義:一個事物只能看見已經提交事物所做的改變。這種隔離級別也支持所謂的不可重複讀(Nonrepeatable Read),因爲同一事物的其他實例在該實例處理期間可能會有新的commit,所以同一select可能返回不同結果。

  Repeatable Read(可重複讀)

這是MySQL的默認事物隔離級別,它確保同一事物的多個實例在併發讀取數據時,會看到同樣的數據行。不過理論上這會導致另一個棘手的問題:幻讀(Phantom Read)。簡單的說,幻讀指當用戶讀取某一範圍的數據行時,另一個事物又在該範圍內插入了新行,當用戶再次讀取該範圍的數據行時,會發現有新的"幻影"行。InnoDB 儲存引擎和Falcon儲存引擎通過多版本併發控制(MVCC,Multiversion Concurrency Control)機制解決了改問題。


Serialiable(可串行化)

這是最高的隔離級別,他通過強制事物排序,使之不可能相互衝突,從而解決幻讀問題。簡言之,他是在每個讀的數據行上加上共享鎖。在這個級別,可能導致大量的超時現象和鎖競爭。

(更多內容可以參考:http://xm-king.iteye.com/blog/770721)


悲觀鎖:

在關係數據庫管理系統裏,悲觀併發控制(又名“悲觀鎖”,Pessimistic Concurrency Control,縮寫“PCC”),是一種併發控制的方法。它可以阻止一個事務以影響其他用戶的方式來修改數據。如果一個事務執行的操作對某行數據應用了鎖,那只有當這個事務把鎖釋放。其他事務才能夠執行與該鎖衝突的操作。悲觀併發控制主要用於數據競爭激烈的環境,以及發生併發衝突時使用鎖保護數據的成本要低於回滾事務的成本環境中。


樂觀鎖:

在關係型數據庫管理系統裏,樂觀鎖併發控制(又名“樂觀鎖”,Optimistic Concurrency Control,縮寫“OCC”)是一種併發控制的方法。它架設多用戶併發的事物在處理時不會彼此相互影響,各事物能夠在不產生鎖的情況下處理各自影響的那部分數據。在提交數據更新前,每個事物都會先檢查在該事務讀取數據後,有沒有其他事物修改了該數據。如果其他事物有更新的話,正在提交的事務會進行回滾。


三,解決方案


針對本文的併發案例,本質是因爲事物非原子操作先讀再判斷再讀再修改,並且SpringMVC控制器的單例模式,所以當多線程併發產生上線文切換的時候導致每個線程出現髒讀的情況,最終造成Bug的出現。解決辦法有如下三個:


①,把數據庫統計字段自增操作放到sql語句裏面,這樣本次事務遍成了原子操作。

②,採用jvm層面同步鎖,先讀再判斷,對之後的讀和修改用synchronized 關鍵字封裝成原子操作,進而保證數據安全。

③,建立緩衝阻塞隊列,封裝多個請求放入隊列,用單線程去處理隊列裏面的任務,最終達到多線程轉單線程的目的。

④,採用悲觀鎖,對於改操作,採用數據庫的悲觀鎖機制加上 for update 語句,通過數據庫鎖來保證數據的一致。

⑤,採用樂觀鎖,對操作表加上版本號,每次修改驗證版本號的一致性。


寫在最後,路漫漫其修遠兮,在知識的海洋我們就像大海中的浮萍,微不足道,但是隻要我們心存信仰,朝着我們夢想的方向 堅持前行,那麼至少我們在一天一天的成長,一天一天的變好,也希望大家在新的一年裏夢想成真。






發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章