高並發和高性能系統中鎖的影響
來自專欄猿論14 人贊了文章
【為什麼需要鎖呢?】
在單核時代,在不需要並行運算的情況下,鎖確實是不需要的。
那麼多核並行運算的時候,為什麼需要鎖呢?
我們先來看一個例子:
古代有一家錢莊,掌柜一個人記賬,一個人更新錢莊的總賬戶餘額,那麼總賬戶餘額的更新不存在並行運算的情況,也就不會有數據更新的問題。
而隨著錢莊業務量的不斷增加,掌柜一個人越來越忙不過來,於是請了2個夥計,這時候就有3個人一起來記賬了。
這一天,來了兩個客戶,客戶A找夥計A存50兩銀子,客戶B找夥計B取50兩銀子。
夥計A和夥計B在自己的賬本上分別記錄上自己操作的這筆業務,然後同時去看了一下總賬戶餘額是1200兩。
夥計A算了下,1200+50=1250兩,於是把1250更新為總賬戶餘額了。
夥計B算了下,1200-50=1150兩,於是把1150更新為總賬戶餘額了。
大家是不是發現問題了,兩筆業務,一個+50,一個-50,最後應該是不變,但是在夥計B更新完成後,總賬戶餘額卻少了50兩。
上面就是因為並行運算導致數據更新異常的情況。
要怎麼解決這個問題呢?
掌柜給總賬戶餘額的本子上面加了一個鎖。誰要看和寫總賬戶餘額之前都需要上鎖,操作完成之後再解開鎖。
於是上面的兩個業務操作就有些變化了。如下:
夥計A去看查看總賬戶餘額,先上鎖,看到餘額是1200,然後回來計算下1200+50=1250,把1250更新為總賬戶餘額,然後解開鎖。
夥計B也來查看總賬戶餘額,一看上鎖了,就只好先等著,等鎖解開了,他才能去看餘額是1250,回來計算下1250-50=1200,把1200更新為總賬戶餘額,然後解開鎖。
這樣,數據更新也就安全了。
鎖,是為了解決並行運算時,數據並發讀寫的安全性問題。
【增加鎖的操作,帶來的影響】
首先,增加鎖的操作,夥計的工作流程中多了上鎖解鎖,雖然這個上鎖解鎖的速度很快,但畢竟是增加了操作和開銷。
另外,夥計B看到已經上鎖,意味著有衝突時,需要增加額外的等待,對總賬戶餘額的更新變成串列化。
從上面可以看出來,如果工作流程中,有一個大循環,循環裡面不斷有對數據操作,大量的上鎖解鎖,也是很影響性能。
如果不只2個夥計,如果是20個夥計,非常頻繁的去更新總賬戶餘額,衝突太頻繁,夥計的工作效率會急劇下降。
所以,我們知道了,在並發編程中,鎖帶來了安全性,同時也帶來了性能瓶頸。
鎖和並發,貌似有一種相剋相生的關係。
【怎麼減少鎖的負面影響,同時保證數據安全呢】
下面幾種方法大家可以考慮:
方法1:
對單個數據的更新,可以使用CAS(Compare-and-Swap)指令。
夥計們的操作變成下面這個過程:
夥計A看了下總賬戶餘額是1200,然後記住這個數字,回來計算1200+50=1250,回去修改,一看總賬戶餘額還是1200,於是成功修改為1250;
夥計B看了下總賬戶餘額是1200,然後記住這個數字,回來計算1200-50=1150,回去修改,一看總賬戶餘額是1250,不是原來的1200,說明數據被修改了,需要重新計算,於是記住新的值1250,回去重新計算1250-50=1200,在回去修改,一看總賬戶餘額是1250,於是成功修改為1200。
上面的操作過程就是運用了CAS指令,修改之前先對比,數據沒變化說明沒有被修改過,這時候才能進行更新。
但是CAS操作時,還有一個ABA的問題。
例如:夥計A動作很快,改了一筆1200->1250,又改了一筆1250->1200;這時候夥計B回來改,看到的1200雖然數量沒變,但是已經被改動兩次了。
雖然上面的這種情況ABA問題不會有什麼影響,但是有時候還是會出問題。
例如:總賬戶餘額是每天記錄一個數。
夥計A在今天的總賬戶餘額上面改了一筆1200->1250,掌柜過來翻了一下總賬戶,翻到了昨天那頁,而那頁的總賬戶餘額剛好是1200;
夥計B過來更新數據,一看還是1200,就改成了1150,這個改動是有問題的,因為夥計B改的是昨天的總賬戶餘額。
要怎麼解決這種ABA問題呢?
只需要在之前的數據基礎上,再增加一個日期數據,檢查的時候,需要同時檢查總賬戶餘額的數據和日期的數據,都沒有變化才可以成功更新。
方法2:
引入隊列,讓一個任務專門來做數據的更新,避免並行運算。
這時候,就是讓掌柜一個人來更新總賬戶餘額,夥計們只需要把自己的每一筆業務結果記錄在總賬戶下面。
夥計A有一筆業務是存50兩,則在總賬戶下面記錄上+50;
夥計B有一筆業務是取50兩,則在總賬戶下面記錄上-50;
而掌柜只需要從總賬戶裡面,順序的把+50,-50的操作更新到總賬戶餘額就可以了。
這種方法的好處是,避免夥計並行更新總賬戶餘額,發生衝突時的等待,既提高了夥計的效率也保證了數據更新的安全。
方法3:
還是需要有鎖的操作,但是讓運算的過程更快,減少鎖衝突的頻率和時間。
前面是夥計先上鎖,然後看總賬戶餘額,再回去運算,再回來更新和解鎖。
把這個過程改一下,夥計帶著算盤過來,先上鎖,然後現場運算,更新後解鎖,再回去。
這樣一來,整個的讀寫時間變短了,鎖的衝突時間也就減少了,效率和性能也就能有所提高。
方法4:
能否進一步減少鎖的衝突時間,比如:將讀、寫的鎖分開考慮,畢竟大部分業務中讀的次數會遠多於寫的次數。
A 使用讀寫鎖而不是互斥鎖,可以提高並發讀的效率,減少讀時候的鎖衝突。
例如:大量的業務都需要查看總賬餘額才可以做決定,那麼就會有大量的讀需求。而多個人一起讀不衝突,只是在需要寫的時候才獨佔總賬餘額的鎖。
B 將大的數據分拆為多段的小數據,這樣通過多個鎖分別作用在小數據上,避免一個鎖作用在大的數據中,減少衝突的概率。
例如:對總賬餘額的每一位設置單獨的鎖,而不是對整個數設置一個鎖。這樣的話,+50隻需要得到十位數的鎖,-3隻需要得到個位的鎖,如果是+55則要個位和十位兩個鎖。
C 讀的時候不需要鎖,而寫入的時候串列化同時只能一個人更新
例如:總賬餘額對所有人公開的,大家可以隨便看,但是修改的時候,只能由掌柜來操作。
【死鎖是怎麼造成的?怎麼避免?】
鎖有兩個操作,一個是加鎖,一個是解鎖。
鎖衝突的時候,所有需要加鎖的人就要等待,而無限等待就是死鎖的狀態。
造成的情況有下面2種:
1 如果夥計A對總賬餘額加鎖,但是被客戶叫走了,就會導致夥計B陷入死鎖,一直等待。
這種情況就是夥計A出現了異常,沒能及時的操作完,進行解鎖。
2 如果夥計A對今天的總賬餘額加鎖,同時夥計B對昨天的總賬餘額加鎖,然後夥計A又想要獲取昨天總賬餘額的鎖,同時夥計B要今天的總賬餘額鎖。
那這個時候,夥計A、B就會因為今天、昨天的總賬餘額兩個鎖互相依賴,都無法成功完成操作,造成死鎖。
要怎麼避免呢?
針對情況1,就需要對異常情況做更周密的考慮,增加超時處理,鎖佔用時間最多1分鐘,超時就自動解鎖。
針對情況2,可以考慮把鎖的粒度設置粗一些,鎖的效率低些總比造成死鎖的情況要好很多。
【單機的鎖是怎麼實現的?】
鎖匯流排,只能有一個CPU核心可以通過數據匯流排訪問主存,其他CPU等待,保證多CPU並行時數據讀寫的一致性。效率低,CPU閑置。
緩存鎖,針對單個緩存行中的數據地址,緩存讀寫只能發生在一個CPU核心,其他CPU緩存全部失效。數據少,只能是一個緩存行的數據。
因此,只對一個變數做更新的時候,能用緩存鎖就盡量不要使用鎖匯流排的方式。
【分散式鎖的實現】
多伺服器多進程之間需要做到數據安全更新的時候,就需要用到分散式鎖。
而分散式鎖的實現,還是需要有獨立的線程安全的服務來提供鎖的實現。
比如:
redis,實現簡單,吞吐量十分可觀,對於高並發情況應付自如,自帶超時保護,對於網路抖動的情況也可以利用超時刪除策略保證不會阻塞所有流程。
zookeeper,實現分散式鎖雖然是比較重量級的,但實現的鎖功能十分健全,由於Zookeeper本身需要維護自己的一致性,所以性能上較Redis還是有一定差距的。
【寫在最後】
並發編程是非常複雜,充滿挑戰的一項工作,可為什麼大部分感覺不到它的難點呢?
互聯網應用、Web應用,都是並發編程的典型應用,大家寫著業務代碼,卻體會不到其中的難點呢?
我個人理解,是不是以為工作任務只是測試通過,功能跑通而已呢?
是不是以為幾次幾十次的操作,數據寫入成功,返回正常就可以呢?
是不是以為偶爾出個錯,排查後修正解決了就完事了呢?
我們的應用如果是作為一個系統,那麼就不能是片面去看待和思考它,要有系統化的思維,從很小的點開始,認真面對這一切。
關於高並發和高性能的系列文章,到這算是一個階段性尾篇,之前的文章希望大家能再翻出來仔細學習和了解。
大家有更多希望了解的內容,也可以聯繫我。
後續,我再針對性寫一些關於高可用的文章,敬請期待~
在實戰課程 《PHP秒殺系統 高並發高性能的極致挑戰》中,也是針對這類高並發的業務場景做了特定的性能優化以及分散式方案,大家可以參考學習。
作者:一凡Sir
鏈接:http://www.imooc.com/article/40870
來源:慕課網
本文原創發佈於慕課網 ,轉載請註明出處,謝謝合作
推薦閱讀:
【官方】手記欄目認證作者招募,長期有效,隨時報名!_慕課手記
有獎徵文005期 |人生路上得一良師,是何感受?
java裡面i++與++i到底哪一種寫法的效率高?
許可權菜單設計
區塊鏈技術公司老生常談區塊鏈技術如何工作?
推薦閱讀:
※解決無IISXP系統無IIS如何安裝IIS
※alsa移植到arm linux嵌入式系統中
※重新安裝系統「必須備份」的十類數據
※消化系統14中常見疾病經典藥方
※腦補人類演化(八)總結——人類演化是個系統工程