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

2022-06-04 15:01:12

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

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

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

在程式設計師眼裡,大家搶的不是紅包,是紅包後臺服務的 !這裡的不是我們日常生活中的鎖,後臺服務程式設計中鎖的概念:

實現多個程序或執行緒互斥的存取共用資源的一種機制

今天和大家聊聊後臺服務程式設計中的鎖。

業務模型

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

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

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

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

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

有哪些鎖

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

悲觀鎖

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

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

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

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

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

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

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題解,各類程式設計資源。