本文源碼:GitHub·點這裏 || GitEE·點這裏java
在互聯網的業務架構中,高併發是最難處理的業務之一,常見的使用場景:秒殺,搶購,訂票系統;高併發的流程中須要處理的複雜問題很是多,主要涉及下面幾個方面:git
高併發業務核心仍是流量控制,控制流量下沉速度,或者控制承接流量的容器大小,多餘的直接溢出,這是相對複雜的流程。其次就是多線程併發下訪問共享資源,該流程須要加鎖機制,避免數據寫出現錯亂狀況。github
活動未正式開始,先進行活動預定,先把一部分流量收集和控制起來,在真正秒殺的時間點,不少數據可能都已經預處理好了,能夠很大程度上削減系統的壓力。有了必定預定流量還能夠提早對庫存系統作好準備,一箭雙鵰。web
場景:活動預定,定金預定,高鐵搶票預購。redis
分批搶購和搶購的場景實現的機制是一致的,只是在流量上緩解了不少壓力,秒殺10W件庫存和秒殺100件庫存系統的抗壓不是一個級別。若是秒殺10W件庫存,系統至少承擔多於10W幾倍的流量衝擊,秒殺100件庫存,體系可能承擔幾百或者上千的流量就結束了。下面流量削峯會詳解這裏的策略機制。算法
場景:分時段多場次搶購,高鐵票分批放出。spring
最有難度的場景就是準點實時的秒殺活動,假如10點整準時搶1W件商品,在這個時間點先後會涌入高併發的流量,刷新頁面,或者請求搶購的接口,這樣的場景處理起來是最複雜的。sql
場景:618準點搶購,雙11準點秒殺,電商促銷秒殺。數據庫
Nginx是一個高性能的HTTP和反向代理web服務器,常常用在集羣服務中作統一代理層和負載均衡策略,也能夠做爲一層流量控制層,提供兩種限流方式,一是控制速率,二是控制併發鏈接數。緩存
基於漏桶算法,提供限制請求處理速率能力;限制IP的訪問頻率,流量忽然增大時,超出的請求將被拒絕;還能夠限制併發鏈接數。
高併發的秒殺場景下,通過Nginx層的各類限制策略,能夠控制流量在一個相對穩定的狀態。
CDN靜態文件的代理節點,秒殺場景的服務有這樣一個操做特色,活動倒計時開始以前,大量的用戶會不斷的刷新頁面,這時候靜態頁面能夠交給CDN層面代理,分擔數據服務接口的壓力。
CDN層面也能夠作一層限流,在頁面內置一層策略,假設有10W用戶點擊搶購,能夠只放行1W的流量,其餘的直接提示活動結束便可,這也是經常使用的手段之一。
話外之意:平時參與的搶購活動,可能你的請求根本沒有到達數據接口層面,就極速響應商品已搶完,自行意會吧。
網關層面處理服務接口路由,一些校驗以外,最主要的是能夠集成一些策略進入網關,好比通過上述層層的流量控制以後,請求已經接近核心的數據接口,這時在網關層面內置一些策略控制:若是活動是想激活老用戶,網關層面快速判斷用戶屬性,老用戶會放行請求;若是活動的目的是拉新,則放行更多的新用戶。
通過這些層面的控制,剩下的流量已經很少了,後續才真正開始執行搶購的數據操做。
話外之意:若是有10W人蔘加搶購活動,真正下沉到底層的搶購流量可能就1W,甚至更少,在分散到集羣服務中處理。
在分佈式服務的接口中,還有最精細的一層控制,對於一個接口在單位之間內控制請求處理的數量,這個基於接口的響應時間綜合考慮,響應越快,單位時間內的併發量就越高,這裏邏輯不難理解。
言外之意:流量通過層層控制,數據接口層面分擔的壓力已經不大,這時候就是面對秒殺業務中的加鎖問題了。
機制描述
全部請求的線程必須在獲取鎖以後,才能執行數據庫操做,而且基於序列化的模式,沒有獲取鎖的線程處於等待狀態,而且設定重試機制,在單位時間後再次嘗試獲取鎖,或者直接返回。
過程圖解
Redis基礎命令
SETNX:加鎖的思路是,若是key不存在,將key設置爲value若是key已存在,則 SETNX 不作任何動做。而且能夠給key設置過時時間,過時後其餘線程能夠繼續嘗試鎖獲取機制。
藉助Redis的該命令模擬鎖的獲取動做。
代碼實現
這裏基於Redis實現的鎖獲取和釋放機制。
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實現,這裏提供一份配置文件。
@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的鎖版本太低,數據更新就會失敗。
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就是記錄鎖版本,做爲控制數據更新的條件。
<update id="updateByLock"> UPDATE dl_data_lock SET inventory=inventory-1,lock_value=#{lockVersion} WHERE id=#{id} AND lock_value <#{lockVersion} </update>
說明:這裏的更新操做,不但要求線程獲取鎖,還會判斷線程鎖的版本不能低於當前更新記錄中的最新鎖版本。
機制描述
樂觀鎖大可能是基於數據記錄來控制,在更新數據庫的時候,基於前置的查詢條件判斷,若是查詢出來的數據沒有被修改,則更新操做成功,若是前置的查詢結果做爲更新的條件不成立,則數據寫失敗。
過程圖解
代碼實現
業務流程,先查詢要更新的記錄,而後把讀取的列,做爲更新條件。
@Override public Boolean updateByInventory(Integer id) { DataLockEntity dataLockEntity = dataLockMapper.getById(id); if (dataLockEntity != null){ return dataLockMapper.updateByInventory(id,dataLockEntity.getInventory())>0 ; } return false ; }
例如若是要把庫存更新,就把讀取的庫存數據做爲更新條件,若是讀取庫存是100,在更新的時候庫存變了,則更新條件天然不能成立。
<update id="updateByInventory"> UPDATE dl_data_lock SET inventory=inventory-1 WHERE id=#{id} AND inventory=#{inventory} </update>
在處理高併發的秒殺場景時,常常出現服務掛掉場景,常見某些APP的營銷頁面,出現活動火爆頁面丟失的提示狀況,可是不影響總體應用的運行,這就是服務的隔離和保護機制。
基於分佈式的服務結構能夠把高併發的業務服務獨立出來,不會由於秒殺服務掛掉影響總體的服務,致使服務雪崩的場景。
數據庫保護和服務保護是相輔相成的,分佈式服務架構下,服務和數據庫是對應的,理論上秒殺服務對應的就是秒殺數據庫,不會由於秒殺庫掛掉,致使整個數據庫宕機。
GitHub·地址 https://github.com/cicadasmile/data-manage-parent GitEE·地址 https://gitee.com/cicadasmile/data-manage-parent
推薦閱讀:《架構設計系列》,蘿蔔青菜,各有所需
序號 | 標題 |
---|---|
00 | 架構設計:單服務.集羣.分佈式,基本區別和聯繫 |
01 | 架構設計:分佈式業務系統中,全局ID生成策略 |
02 | 架構設計:分佈式系統調度,Zookeeper集羣化管理 |
03 | 架構設計:接口冪等性原則,防重複提交Token管理 |
04 | 架構設計:緩存管理模式,監控和內存回收策略 |
05 | 架構設計:異步處理流程,多種實現模式詳解 |