一、前言
高併發的經驗是許多網絡開發人員追求的夢想,有了這樣的經驗在哪裏找工作都有優勢,同樣的資質,有這個硬指標,那你就自信很多。而在實際工作中一些中小型公司很難碰到高併發場景,在這種場景下,如何提高吞吐量?如何降低響應時間?從這兩個指標開始折騰到底。所以要獲得高併發的經驗:首先要有場景,有了場景,需要自己去了解方方面面相關的知識,在架構調優的同時,自己也能跟着成長了。
二、高併發設計核心
2.1 硬件層面
- 服務器加配(內存,硬盤)
2.2 軟件層面
- LSV,nginx負載均衡(分流)
- 數據庫分庫分表,讀寫分離
- 緩存:空間換時間(解決讀)
- 異步:慢慢處理(解決寫)
2.3 架構方面
- 動靜資源分離:圖片,js,css資源文件
- 前後臺分離
- 頁面靜態化(html進行緩存)
2.4 保證服務穩定性
- 限流
- 控制併發 Semaphone
- 控制訪問速率(令牌桶RateLimiter,漏桶)
- 降級
- 熔斷(保險絲)
- 重試
三、實際項目如何落地
那我們瞭解要設計一個高併發項目大概需要的核心要點後,那在真實項目中是如何落地的呢?有沒有一些比較成熟的中間件來幫助我們快速實現?這裏分2種服務:dubbo,http(網關)
3.1 網關-Hystrix
1.協議轉換:dubbo協議轉換爲http協議
2.統一api管理:環境隔離,api分組,api文檔
3.提高效率:管理,開發
功能分析
1.簽名鑑權
2.登錄驗證
3.數據Mock
4.參數校驗
5.國際化翻譯
6.熔斷
7.降級
8.限流
9.擴展點
總結:網關統一對服務進行管理,我們通過網關後臺配置,就可以簡單對我們服務配置超時,限流,降級,熔斷策略。(Hystrix實現原理後面再分析)
3.2 Dubbo-Sentinel
在複雜的生產環境下可能部署着成千上萬的服務實例,當流量持續不斷地湧入,服務之間相互調用頻率陡增時,會產生系統負載過高、網絡延遲等一系列問題,從而導致某些服務不可用。如果不進行相應的流量控制,可能會導致級聯故障,並影響到服務的可用性,因此如何對高流量進行合理控制,成爲保障服務穩定性的關鍵。Sentinel是面向分佈式服務架構的輕量級限流降級框架,以流量爲切入點,從流量控制、熔斷降級和系統負載保護等多個維度來幫助用戶保障服務的穩定性。
dubbo服務怎麼引入sentinel?
Sentinel 提供了與 Dubbo 適配的模塊 – Sentinel Dubbo Adapter,包括針對服務提供方的過濾器和服務消費方的過濾器。使用時用戶只需引入相關模塊,Dubbo的服務接口和方法(包括調用端和服務端)就會成爲 Sentinel 中的資源,在配置了規則後就可以自動享受到 Sentinel 的防護能力。同時提供了靈活的配置選項,例如若不希望開啓Sentinel Dubbo Adapter中的某個Filter,可以手動關閉對應的Filter。
sentinel控制檯?
Sentinel的控制檯(Dashboard)是流量控制、熔斷降級規則統一配置和管理的入口,同時它爲用戶提供了多個維度的監控功能。在Sentinel控制檯上,我們可以配置規則並實時查看流量控制效果。
總結:通過dubbo整合sentinel後,我們只需要通過sentinel控制檯進行配置就可以輕鬆實現流控,降級等規則。
四、實際項目分析-搶霸王餐
我們要知道設計一個高併發系統更多的是依賴於我們所在公司的架構,比如是否有分庫分表(讀寫分離),是否使用CDN,是否是分佈式服務,是否實現負載均衡等,然後加上我們一個好的架構設計,更大程度上的提高系統的併發量(流量肖峯,緩存預熱);最後通過限流,降級,熔斷保證服務的穩定性。
4.1 項目背景
前有淘寶“雙十一”、京東“618”、小紅書“66.過平臺的影響力,來玩七點一刻搶霸王餐的活動,活動每晚七點一刻準時開始,手速最快的可以搶到霸王餐或贏免單機會,以此刺激消費者湧向我們平臺,同時,分享活動給好友還可以提升中獎機會,刺激用戶分享,爲我們帶來裂變式的傳播;商家參與此活動,一來可以降低獲客成本,爲之帶來私域流量,二來“贏免單機會”打的是心理戰,可以促進消費。
4.2 項目預覽
每晚進行直播,然後7.15進行搶霸王餐活動,領取現金紅包。
1)創建霸王餐券
2)主播端
3)觀衆端
4.3 簡要講述一下設計要點
其實搶券和搶票,秒殺等都是同類問題。(搶券會少了個下單的過程)
- Lvs負載均衡,nginx負載均衡【分流】
- 券靜態數據放CDN,動靜分離
- 緩存存放數據
1.助力數據初始化:set(activityId+customerId,value)
2.券總量:set(activityId,total)
3.是否搶過:set(activityId+customerId,1)
4.用戶搶的頻率:set(activityId+customerId,valueCount,expireTime)- 算法:(抽獎結束時間戳-抽獎時間戳)a% + 助力次數b% = 抽獎概率
- 庫存預減->發送搶券消息->計算中獎概率->庫存扣減->記錄領券信息
五、限流
5.1 Guava
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>19.0</version>
</dependency>
public class GuavaRateLimiter {
public static void main(String[] args) {
// 1.創建限流器,每秒5個令牌(令牌桶算法)
RateLimiter limiter = RateLimiter.create(5);
while (true){
new Thread(()->{
limiter.acquire();
System.out.println("*****"+Thread.currentThread().getName());
}).start();
}
}
}
5.2 信號量
public class SemaphoreTest {
private static Semaphore semaphore = new Semaphore(5);
private static ExecutorService executorService = Executors.newFixedThreadPool(20);
public static void main(String[] args) {
while (true){
executorService.execute(()->{
try {
semaphore.acquire();
System.out.println("*****"+Thread.currentThread().getName());
TimeUnit.MICROSECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
semaphore.release();
}
});
}
}
}
5.3 sentinel
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-core</artifactId>
<version>1.7.1</version>
</dependency>
private static final String RESOURCE_NAME = "lxh";
// 限流規則 10個QPS/s
private static void loadLimitingRules(){
List<FlowRule> rules = new ArrayList<>();
FlowRule rule = new FlowRule();
rule.setResource(RESOURCE_NAME);
rule.setLimitApp("default");
rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
rule.setCount(10);
rules.add(rule);
FlowRuleManager.loadRules(rules);
}
private static void limiting(){
Entry entry = null;
try {
entry = SphU.entry(RESOURCE_NAME);
System.out.println("返回結果");
}catch (BlockException e1){
System.out.println("接口限流,返回爲空");
}
catch (Exception e){
e.printStackTrace();
}finally {
if (entry!=null){
entry.exit();
}
}
}
六、降級
6.1 sentinel
// 異常數量大於4進行降級
private static void initDegradeRule() {
List<DegradeRule> rules = new ArrayList<DegradeRule>();
DegradeRule rule = new DegradeRule();
rule.setResource(KEY);
// set limit exception count to 4
rule.setCount(4);
rule.setGrade(RuleConstant.DEGRADE_GRADE_EXCEPTION_COUNT);
rule.setTimeWindow(10); // 熔斷窗口時間爲10s
rules.add(rule);
DegradeRuleManager.loadRules(rules);
}
private static void limiting(){
Entry entry = null;
try {
entry = SphU.entry(RESOURCE_NAME);
System.out.println("返回結果");
int i = 1/0;
}catch (BlockException e1){
System.out.println("接口降級,返回爲空");
}
catch (Exception e){
e.printStackTrace();
}finally {
if (entry!=null){
entry.exit();
}
}
}
七、重試
<dependency>
<groupId>com.github.rholder</groupId>
<artifactId>guava-retrying</artifactId>
<version>2.0.0</version>
</dependency>
public class RetryerTest {
public static void main(String[] args) throws Exception {
Retryer<Boolean> retryer = RetryerBuilder.<Boolean>newBuilder()
.retryIfExceptionOfType(Exception.class)
.retryIfRuntimeException()
.retryIfResult(Predicates.equalTo(false))
// 嘗試請求6次
.withStopStrategy(StopStrategies.stopAfterAttempt(6))
// 等待策略
.withWaitStrategy(WaitStrategies.fixedWait(1, TimeUnit.SECONDS))
// 時間限制
.withAttemptTimeLimiter(AttemptTimeLimiters.fixedTimeLimit(2, TimeUnit.SECONDS))
.withRetryListener(new RetryListener() {
@Override
public <V> void onRetry(Attempt<V> attempt) {
System.out.println("----------------start---------------");
System.out.println("重試次數:"+attempt.getAttemptNumber());
if (attempt.hasException()){
System.out.println("重試發生異常:" + attempt.getExceptionCause());
}
if (attempt.hasResult()){
System.out.println("結果:" + attempt.getResult());
}
System.out.println("-----------------end--------------");
}
})
.build();
Boolean call = retryer.call(new Callable<Boolean>() {
int times = 1;
@Override
public Boolean call() throws Exception {
times++;
String auditStatus = getAuditStatus();
boolean result = "SUCESS".equals(auditStatus);
if (times == 2) {
System.out.println("NullPointerException...");
throw new NullPointerException();
} else if (times == 3) {
System.out.println("Exception...");
throw new Exception();
} else if (times == 4) {
System.out.println("RuntimeException...");
throw new RuntimeException();
} else if (times == 5) {
System.out.println("false...");
return result;
} else {
System.out.println("true...");
return result;
}
}
});
System.out.println("結果:"+call);
}
private static String getAuditStatus(){
return "SUCESS";
}
}