面試官讓我5分鐘內寫一個搶紅包程序,我和他說了半小時原理!

微信搜「程序員檸檬」分享編程學習路線和學習資源,本文已收錄於Github:https://github.com/imcoderlemon/CodeClass
內含原創乾貨文章,千本計算機電子書,谷歌、阿里大神開源LeetCode題解,各類編程資源。

今年春節響應國家號召在家宅着抵抗疫情,拜年也改用微信紅包,春節發了很多也搶了很多微信紅包,也算支持了公司業務,微信支付融入生活,搶紅包已經是非常平常的事情。

搶紅包這一簡單的動作,每一次都是對紅包服務後臺的一次請求,在春節期間海量的服務請求下,其實是一個很典型的高併發編程模型。後臺開發程序員都有一個共識:實現一個功能很容易,難的是大量請求下提高服務性能

在程序員眼裏,大家搶的不是紅包,是紅包後臺服務的 !這裏的不是我們日常生活中的鎖,後臺服務編程中鎖的概念:

實現多個進程或線程互斥的訪問共享資源的一種機制

今天和大家聊聊後臺服務編程中的鎖。

業務模型

爲便於說明,我們簡化模型,約定搶紅包服務是多線程服務,搶紅包操作包含以下3個步驟:

  1. 查詢數據庫內紅包餘額
  2. 扣除搶到的紅包金額
  3. 更新紅包餘額到數據庫

image

假設你發了100塊錢紅包,1000個人1秒內同時來搶(高併發),如果不加鎖是這樣的情況:

  • 第一個人查餘額得到100元,他在此基礎上扣除搶到的假設2元,準備步驟3更新到數據庫。
  • 在第一個人更新進去之前,此時剩下的人查到的餘額也是100,他們各自扣除搶到的金額,準備按步驟3更新。
  • 導致最後的紅包餘額只記錄了最後一次更新的數據。
  • 很明顯,這就可能出現1000個人都搶到紅包,但是紅包餘額還沒分完的情況,這就亂了。

怎麼解決這個問題呢? 就用到我們上面說的加鎖來解決。

有哪些鎖

實現鎖的方式有很多,這裏列舉幾種常見的分類

悲觀鎖

顧名思義就是悲觀的做最壞打算的鎖機制,佔有鎖期間獨佔資源。

悲觀鎖把搶紅包這三個步驟打包成一個整體做成互斥操作,“在我搶了沒更新數據之前你別來查餘額,查到也不準確”。也可以類比數據庫的事務來理解。

事務必須具備以下四個屬性,簡稱ACID 屬性:
原子性(Atomicity):事務是一個完整的操作。事務的各步操作是不可分的(原子的);要麼都執 行,要麼都不執行
一致性(Consistency):當事務完成時,數據必須處於一致狀態
隔離性(Isolation):對數據進行修改的所有併發事務是彼此隔離的,這表明事務必須是獨立的,它不應以任何方式依賴於或影響其他事務
永久性(Durability):事務完成後,它對數據庫的修改被永久保持,事務日誌能夠保持事務的永久性

它悲觀的認爲你每次去搶紅包必然有其他人也同時在搶,所以你這條線程在搶的時候要獨佔資源,其他線程需要阻塞掛起等待你搶完才能進來搶,掛起的線程就幹不了其他事了。

魯迅先生說過,浪費CPU資源就是浪費生命!

image

而一旦你搶完紅包釋放了鎖,其他在等待中的線程又要搶佔資源、搶到了還要恢復線程上下文。

CPU不斷的切換線程上下文非常浪費服務器資源,嚴重的會導致不能及時處理後續搶紅包請求,需要想辦法提高效率,於是有了樂觀鎖

樂觀鎖

樂觀鎖是對悲觀鎖的改進,樂觀的認爲加鎖的時候沒有競爭,樂觀鎖不阻塞線程。

一種實現樂觀鎖的方法是數據庫內紅包餘額增加版本號,初始版本號是0,每次搶完紅包版本號加1後再去更新餘額,只有更新的版本號大於數據庫內的版本號才認爲是合法的,予以更新;否則不予更新,線程不阻塞可以稍後重試,避免頻繁切換線程上下文。

樂觀鎖在搶紅包的步驟1、2不做加鎖判斷,在步驟3的時候才做加鎖判斷版本號。

  • 第一個人搶到版本號是0的紅包,第二個人也搶到版本號是0的紅包
  • 第一個人更新紅包餘額並設置版本號爲1
  • 第二個人更新紅包餘額設置版本號爲1的時候發現餘額版本號已經爲1,更新失敗
  • 第二個人更新失敗後,線程不阻塞,繼續處理其他搶紅包搶請求,按一定策略重試(超時重試、有限次數重試)第二個人的更新操作
  • 其他請求以此類推

可以看到,樂觀鎖在加鎖失敗的時候不掛起線程等待,避免了線程上下文頻繁的切換,提高紅包服務處理性能。

分佈式鎖

上面兩種鎖的形式都是基於對數據庫的更新來做的,在大請求高併發的時候,頻繁的存取數據庫,尤其是樂觀鎖重試會對數據庫產生很大的衝擊,在實際生產環境要儘量減少對數據庫的訪問。

Redis 是一個開源(BSD許可)的,內存中的數據結構存儲系統,它可以用作數據庫、緩存和消息中間件。也可以用redis實現分佈式鎖,與數據庫交互兩次:第一次獲取紅包餘額,第二次搶完更新紅包狀態。搶紅包和中間過程更新操作都在內存中進行,這可比數據庫操作快了幾個數量級,顯著改善服務併發性能。

redis分佈式鎖:

利用Redis的SET操作在內存中保存key-value鍵值對,加鎖就是獲取這個鍵值對的值,解鎖就是刪除這個鍵值對。

分佈式鎖也不阻塞線程,關於這種分佈式鎖的實現不在這裏展開說明,可以參考我另一篇公衆號文章: redis分佈式鎖的3種實現方式分析詳細分析了幾種分佈式鎖特點和利弊。


原創不易,看到這裏動動手指,各位的「三連」是對我持續創作的最大支持,我們下篇文章再見。

微信搜「程序員檸檬」分享編程學習路線和學習資源,本文已收錄於Github:https://github.com/imcoderlemon/CodeClass
內含原創乾貨文章,千本計算機電子書,谷歌、阿里大神開源LeetCode題解,各類編程資源。

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