1. 程式人生 > >架構設計 | 高併發流量削峰,共享資源加鎖機制

架構設計 | 高併發流量削峰,共享資源加鎖機制

本文原始碼:[GitHub·點這裡](https://github.com/cicadasmile/data-manage-parent) || [GitEE·點這裡](https://gitee.com/cicadasmile/data-manage-parent) # 一、高併發簡介 在網際網路的業務架構中,高併發是最難處理的業務之一,常見的使用場景:秒殺,搶購,訂票系統;高併發的流程中需要處理的複雜問題非常多,主要涉及下面幾個方面: - 流量管理,逐級承接削峰; - 閘道器控制,路由請求,介面熔斷; - 併發控制機制,資源加鎖; - 分散式架構,隔離服務和資料庫; 高併發業務核心還是流量控制,控制流量下沉速度,或者控制承接流量的容器大小,多餘的直接溢位,這是相對複雜的流程。其次就是多執行緒併發下訪問共享資源,該流程需要加鎖機制,避免資料寫出現錯亂情況。 # 二、秒殺場景 ## 1、預搶購業務 活動未正式開始,先進行活動預約,先把一部分流量收集和控制起來,在真正秒殺的時間點,很多資料可能都已經預處理好了,可以很大程度上削減系統的壓力。有了一定預約流量還可以提前對庫存系統做好準備,一舉兩得。 場景:活動預約,定金預約,高鐵搶票預購。 ## 2、分批搶購 分批搶購和搶購的場景實現的機制是一致的,只是在流量上緩解了很多壓力,秒殺10W件庫存和秒殺100件庫存系統的抗壓不是一個級別。如果秒殺10W件庫存,系統至少承擔多於10W幾倍的流量衝擊,秒殺100件庫存,體系可能承擔幾百或者上千的流量就結束了。下面流量削峰會詳解這裡的策略機制。 場景:分時段多場次搶購,高鐵票分批放出。 ## 3、實時秒殺 最有難度的場景就是準點實時的秒殺活動,假如10點整準時搶1W件商品,在這個時間點前後會湧入高併發的流量,重新整理頁面,或者請求搶購的介面,這樣的場景處理起來是最複雜的。 - 首先系統要承接住流量的湧入; - 頁面的不斷重新整理要實時載入; - 高併發請求的流量控制加鎖等; - 服務隔離和資料庫設計的系統保護; 場景:618準點搶購,雙11準點秒殺,電商促銷秒殺。 # 三、流量削峰 ![](https://img2020.cnblogs.com/blog/1691717/202006/1691717-20200622213840487-296806334.png) ## 1、Nginx代理 Nginx是一個高效能的HTTP和反向代理web伺服器,經常用在叢集服務中做統一代理層和負載均衡策略,也可以作為一層流量控制層,提供兩種限流方式,一是控制速率,二是控制併發連線數。 基於漏桶演算法,提供限制請求處理速率能力;限制IP的訪問頻率,流量突然增大時,超出的請求將被拒絕;還可以限制併發連線數。 高併發的秒殺場景下,經過Nginx層的各種限制策略,可以控制流量在一個相對穩定的狀態。 ## 2、CDN節點 CDN靜態檔案的代理節點,秒殺場景的服務有這樣一個操作特點,活動倒計時開始之前,大量的使用者會不斷的重新整理頁面,這時候靜態頁面可以交給CDN層面代理,分擔資料服務介面的壓力。 CDN層面也可以做一層限流,在頁面內建一層策略,假設有10W使用者點選搶購,可以只放行1W的流量,其他的直接提示活動結束即可,這也是常用的手段之一。 話外之意:平時參與的搶購活動,可能你的請求根本沒有到達資料介面層面,就極速響應商品已搶完,自行意會吧。 ## 3、閘道器控制 閘道器層面處理服務介面路由,一些校驗之外,最主要的是可以整合一些策略進入閘道器,比如經過上述層層的流量控制之後,請求已經接近核心的資料介面,這時在閘道器層面內建一些策略控制:如果活動是想啟用老使用者,閘道器層面快速判斷使用者屬性,老使用者會放行請求;如果活動的目的是拉新,則放行更多的新使用者。 經過這些層面的控制,剩下的流量已經不多了,後續才真正開始執行搶購的資料操作。 話外之意:如果有10W人蔘加搶購活動,真正下沉到底層的搶購流量可能就1W,甚至更少,在分散到叢集服務中處理。 ## 4、併發熔斷 在分散式服務的介面中,還有最精細的一層控制,對於一個介面在單位之間內控制請求處理的數量,這個基於介面的響應時間綜合考慮,響應越快,單位時間內的併發量就越高,這裡邏輯不難理解。 言外之意:流量經過層層控制,資料介面層面分擔的壓力已經不大,這時候就是面對秒殺業務中的加鎖問題了。 # 四、分散式加鎖 ## 1、悲觀鎖 **機制描述** 所有請求的執行緒必須在獲取鎖之後,才能執行資料庫操作,並且基於序列化的模式,沒有獲取鎖的執行緒處於等待狀態,並且設定重試機制,在單位時間後再次嘗試獲取鎖,或者直接返回。 **過程圖解** ![](https://img2020.cnblogs.com/blog/1691717/202006/1691717-20200622213822938-1008298339.png) **Redis基礎命令** SETNX:加鎖的思路是,如果key不存在,將key設定為value如果key已存在,則 SETNX 不做任何動作。並且可以給key設定過期時間,過期後其他執行緒可以繼續嘗試鎖獲取機制。 藉助Redis的該命令模擬鎖的獲取動作。 **程式碼實現** 這裡基於Redis實現的鎖獲取和釋放機制。 ```java import org.springframework.stereotype.Component; import redis.clients.jedis.Jedis; import javax.annotation.Resource; @Component public class RedisLock { @Resource private Jedis jedis ; /** * 獲取鎖 */ public boolean getLock (String key,String value,long expire){ try { String result = jedis.set( key, value, "nx", "ex", expire); return result != null; } catch (Exception e){ e.printStackTrace(); }finally { if (jedis != null) jedis.close(); } return false ; } /** * 釋放鎖 */ public boolean unLock (String key){ try { Long result = jedis.del(key); return result > 0 ; } catch (Exception e){ e.printStackTrace(); }finally { if (jedis != null) jedis.close(); } return false ; } } ``` 這裡基於Jedis的API實現,這裡提供一份配置檔案。 ```java @Configuration public class RedisConfig { @Bean public JedisPoolConfig jedisPoolConfig (){ JedisPoolConfig jedisPoolConfig = new JedisPoolConfig() ; jedisPoolConfig.setMaxIdle(8); jedisPoolConfig.setMaxTotal(20); return jedisPoolConfig ; } @Bean public JedisPool jedisPool (@Autowired JedisPoolConfig jedisPoolConfig){ return new JedisPool(jedisPoolConfig,"127.0.0.1",6379) ; } @Bean public Jedis jedis (@Autowired JedisPool jedisPool){ return jedisPool.getResource() ; } } ``` **問題描述** 在實際的系統執行期間可能出現如下情況:執行緒01獲取鎖之後,程序被掛起,後續該執行的沒有執行,鎖失效後,執行緒02又獲取鎖,在資料庫更新後,執行緒01恢復,此時在持有鎖之後的狀態,繼續執行後就會容易導致資料錯亂問題。 這時候就需要引入鎖版本概念的,假設執行緒01獲取鎖版本1,如果沒有執行,執行緒02獲取鎖版本2,執行之後,通過鎖版本的比較,執行緒01的鎖版本過低,資料更新就會失敗。 ```sql CREATE TABLE `dl_data_lock` ( `id` INT (11) NOT NULL AUTO_INCREMENT COMMENT '主鍵ID', `inventory` INT (11) DEFAULT '0' COMMENT '庫存量', `lock_value` INT (11) NOT NULL DEFAULT '0' COMMENT '鎖版本', PRIMARY KEY (`id`) ) ENGINE = INNODB DEFAULT CHARSET = utf8 COMMENT = '鎖機制表'; ``` 說明:lock_value就是記錄鎖版本,作為控制資料更新的條件。 ```xml ``` 說明:這裡的更新操作,不但要求執行緒獲取鎖,還會判斷執行緒鎖的版本不能低於當前更新記錄中的最新鎖版本。 ## 2、樂觀鎖 **機制描述** 樂觀鎖大多是基於資料記錄來控制,在更新資料庫的時候,基於前置的查詢條件判斷,如果查詢出來的資料沒有被修改,則更新操作成功,如果前置的查詢結果作為更新的條件不成立,則資料寫失敗。 **過程圖解** ![](https://img2020.cnblogs.com/blog/1691717/202006/1691717-20200622213805146-1949592215.png) **程式碼實現** 業務流程,先查詢要更新的記錄,然後把讀取的列,作為更新條件。 ```java @Override public Boolean updateByInventory(Integer id) { DataLockEntity dataLockEntity = dataLockMapper.getById(id); if (dataLockEntity != null){ return dataLockMapper.updateByInventory(id,dataLockEntity.getInventory())>0 ; } return false ; } ``` 例如如果要把庫存更新,就把讀取的庫存資料作為更新條件,如果讀取庫存是100,在更新的時候庫存變了,則更新條件自然不能成立。 ```xml ``` # 五、分散式服務 ## 1、服務保護 在處理高併發的秒殺場景時,經常出現服務掛掉場景,常見某些APP的營銷頁面,出現活動火爆頁面丟失的提示情況,但是不影響整體應用的執行,這就是服務的隔離和保護機制。 基於分散式的服務結構可以把高併發的業務服務獨立出來,不會因為秒殺服務掛掉影響整體的服務,導致服務雪崩的場景。 ## 2、資料庫保護 資料庫保護和服務保護是相輔相成的,分散式服務架構下,服務和資料庫是對應的,理論上秒殺服務對應的就是秒殺資料庫,不會因為秒殺庫掛掉,導致整個資料庫宕機。 # 六、原始碼地址 ``` GitHub·地址 https://github.com/cicadasmile/data-manage-parent GitEE·地址 https://gitee.com/cicadasmile/data-manage-parent ``` ![](https://img2018.cnblogs.com/blog/1691717/201908/1691717-20190823075428183-1996768914.png) **推薦閱讀:《架構設計系列》,蘿蔔青菜,各有所需** |序號| 標題| |:---|:---| |00 | [架構設計:單服務.叢集.分散式,基本區別和聯絡](https://mp.weixin.qq.com/s/NGxI3rC-6mWMDnrClaOR3Q)| |01 | [架構設計:分散式業務系統中,全域性ID生成策略](https://mp.weixin.qq.com/s/1TKAwr99rKEHSxqXFixEhQ)| |02 | [架構設計:分散式系統排程,Zookeeper叢集化管理](https://mp.weixin.qq.com/s/Yr4A95poVjlFsQ-Q0dF7hA)| |03 | [架構設計:介面冪等性原則,防重複提交Token管理](https://mp.weixin.qq.com/s/o9sxN6GwxdNYTKZvRexwjg)| |04 | [架構設計:快取管理模式,監控和記憶體回收策略](https://mp.weixin.qq.com/s/jBu-OZ69DbXfmdIf5VC7kQ)| |05 | [架構設計:非同步處理流程,多種實現模式詳解](https://mp.weixin.qq.com/s/RQm1vPJak0rCGW8d