本文原始碼:GitHub·點這里 || GitEE·點這里
一、高并發簡介
在互聯網的業務架構中,高并發是最難處理的業務之一,常見的使用場景:秒殺,搶購,訂票系統;高并發的流程中需要處理的復雜問題非常多,主要涉及下面幾個方面:
- 流量管理,逐級承接削峰;
- 網關控制,路由請求,介面熔斷;
- 并發控制機制,資源加鎖;
- 分布式架構,隔離服務和資料庫;
高并發業務核心還是流量控制,控制流量下沉速度,或者控制承接流量的容器大小,多余的直接溢位,這是相對復雜的流程,其次就是多執行緒并發下訪問共享資源,該流程需要加鎖機制,避免資料寫出現錯亂情況,
二、秒殺場景
1、預搶購業務
活動未正式開始,先進行活動預約,先把一部分流量收集和控制起來,在真正秒殺的時間點,很多資料可能都已經預處理好了,可以很大程度上削減系統的壓力,有了一定預約流量還可以提前對庫存系統做好準備,一舉兩得,
場景:活動預約,定金預約,高鐵搶票預購,
2、分批搶購
分批搶購和搶購的場景實作的機制是一致的,只是在流量上緩解了很多壓力,秒殺10W件庫存和秒殺100件庫存系統的抗壓不是一個級別,如果秒殺10W件庫存,系統至少承擔多于10W幾倍的流量沖擊,秒殺100件庫存,體系可能承擔幾百或者上千的流量就結束了,下面流量削峰會詳解這里的策略機制,
場景:分時段多場次搶購,高鐵票分批放出,
3、實時秒殺
最有難度的場景就是準點實時的秒殺活動,假如10點整準時搶1W件商品,在這個時間點前后會涌入高并發的流量,重繪頁面,或者請求搶購的介面,這樣的場景處理起來是最復雜的,
- 首先系統要承接住流量的涌入;
- 頁面的不斷重繪要實時加載;
- 高并發請求的流量控制加鎖等;
- 服務隔離和資料庫設計的系統保護;
場景:618準點搶購,雙11準點秒殺,電商促銷秒殺,
三、流量削峰

1、Nginx代理
Nginx是一個高性能的HTTP和反向代理web服務器,經常用在集群服務中做統一代理層和負載均衡策略,也可以作為一層流量控制層,提供兩種限流方式,一是控制速率,二是控制并發連接數,
基于漏桶演算法,提供限制請求處理速率能力;限制IP的訪問頻率,流量突然增大時,超出的請求將被拒絕;還可以限制并發連接數,
高并發的秒殺場景下,經過Nginx層的各種限制策略,可以控制流量在一個相對穩定的狀態,
2、CDN節點
CDN靜態檔案的代理節點,秒殺場景的服務有這樣一個操作特點,活動倒計時開始之前,大量的用戶會不斷的重繪頁面,這時候靜態頁面可以交給CDN層面代理,分擔資料服務介面的壓力,
CDN層面也可以做一層限流,在頁面內置一層策略,假設有10W用戶點擊搶購,可以只放行1W的流量,其他的直接提示活動結束即可,這也是常用的手段之一,
話外之意:平時參與的搶購活動,可能你的請求根本沒有到達資料介面層面,就極速回應商品已搶完,自行意會吧,
3、網關控制
網關層面處理服務介面路由,一些校驗之外,最主要的是可以集成一些策略進入網關,比如經過上述層層的流量控制之后,請求已經接近核心的資料介面,這時在網關層面內置一些策略控制:如果活動是想激活老用戶,網關層面快速判斷用戶屬性,老用戶會放行請求;如果活動的目的是拉新,則放行更多的新用戶,
經過這些層面的控制,剩下的流量已經不多了,后續才真正開始執行搶購的資料操作,
話外之意:如果有10W人參加搶購活動,真正下沉到底層的搶購流量可能就1W,甚至更少,在分散到集群服務中處理,
4、并發熔斷
在分布式服務的介面中,還有最精細的一層控制,對于一個介面在單位之間內控制請求處理的數量,這個基于介面的回應時間綜合考慮,回應越快,單位時間內的并發量就越高,這里邏輯不難理解,
言外之意:流量經過層層控制,資料介面層面分擔的壓力已經不大,這時候就是面對秒殺業務中的加鎖問題了,
四、分布式加鎖
1、悲觀鎖
機制描述
所有請求的執行緒必須在獲取鎖之后,才能執行資料庫操作,并且基于序列化的模式,沒有獲取鎖的執行緒處于等待狀態,并且設定重試機制,在單位時間后再次嘗試獲取鎖,或者直接回傳,
程序圖解

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=https://www.cnblogs.com/cicada-smile/p/#{lockVersion}
WHERE id=#{id} AND lock_value <#{lockVersion}
說明:這里的更新操作,不但要求執行緒獲取鎖,還會判斷執行緒鎖的版本不能低于當前更新記錄中的最新鎖版本,
2、樂觀鎖
機制描述
樂觀鎖大多是基于資料記錄來控制,在更新資料庫的時候,基于前置的查詢條件判斷,如果查詢出來的資料沒有被修改,則更新操作成功,如果前置的查詢結果作為更新的條件不成立,則資料寫失敗,
程序圖解

代碼實作
業務流程,先查詢要更新的記錄,然后把讀取的列,作為更新條件,
@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>
五、分布式服務
1、服務保護
在處理高并發的秒殺場景時,經常出現服務掛掉場景,常見某些APP的營銷頁面,出現活動火爆頁面丟失的提示情況,但是不影響整體應用的運行,這就是服務的隔離和保護機制,
基于分布式的服務結構可以把高并發的業務服務獨立出來,不會因為秒殺服務掛掉影響整體的服務,導致服務雪崩的場景,
2、資料庫保護
資料庫保護和服務保護是相輔相成的,分布式服務架構下,服務和資料庫是對應的,理論上秒殺服務對應的就是秒殺資料庫,不會因為秒殺庫掛掉,導致整個資料庫宕機,
六、源代碼地址
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 | 架構設計:異步處理流程,多種實作模式詳解 |
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/158881.html
標籤:Java
