引言
本文代碼已提交至Github(版本號:
dd8742a8348b4c64a8ca794d544a3271e94365a9
),有興趣的同學可以下載來看看:https://github.com/ylw-github/taodong-shop
秒殺系統的代碼在前面博客已經實現了,有興趣的同學可以參閱下:
- 《淘東電商項目(73) -秒殺系統(前端優化)》
- 《淘東電商項目(74) -秒殺系統(庫存超賣解決方案》
- 《淘東電商項目(75) -秒殺系統(用戶操作頻率限制)》
- 《淘東電商項目(76) -秒殺系統(完整代碼實現)》
- 《淘東電商項目(77) -秒殺系統(小結)》
秒殺的邏輯代碼完成了,剩下還有一個問題,就是關於服務的保護了,本文來講解。
本文目錄結構:
l____引言
l____ 1.服務保護
l________ 1.1 限流
l________ 1.2 降級、熔斷、限流概念
l____ 2.代碼
l________ 2.1 網關限流
l________ 2.2 服務保護
l____ 3.測試
l________ 3.1 網關限流測試
l________ 3.2 服務保護測試
1.服務保護
服務保護在《互聯網併發與安全》欄目有講解過,有興趣的同學可以參閱下:
- 《互聯網併發與安全系列教程(01) - 基於Hystrix實現服務隔離與降級》
- 《互聯網併發與安全系列教程(02) - 服務限流》
- 《互聯網併發與安全系列教程(03) - RateLimiter使用AOP方式實現限流》
下面來簡單的描述下。
1.1 限流
常見限流算法常用的限流算法有:令牌桶算法、漏桶算法
- 令牌桶算法:在秒殺活動中,用戶的請求速率是不固定的,這裏我們假定爲10r/s,令牌按照5個每秒的速率放入令牌桶,桶中最多存放20個令牌。仔細想想,是不是總有那麼一部分請求被丟棄。
- 漏桶算法:漏桶算法的主要目的是控制數據注入到網絡的速率,平滑網絡上的突發流量。漏桶算法提供了一種機制,通過它,突發流量可以被整形以便爲網絡提供一個穩定的流量
市面上常用實現限流框架:
- Nginx+Lua、Guava、hystrix等
1.2 降級、熔斷、限流概念
服務雪崩效應:服務雪崩效應產生與服務堆積在同一個線程池中,因爲所有的請求都是同一個線程池進行處理,這時候如果在高併發情況下,所有的請求全部訪問同一個接口,這時候可能會導致其他服務沒有線程進行接受請求,這就是服務雪崩效應效應。
服務降級:在高併發情況下,防止用戶一直等待,使用服務降級方式(直接返回一個友好的提示給客戶端,調用fallBack方法)。
服務熔斷:熔斷機制目的爲了保護服務,在高併發的情況下,如果請求達到一定極限(可以自己設置闊值)如果流量超出了設置閾值,讓後直接拒絕訪問,保護當前服務。使用服務降級方式返回一個友好提示,服務熔斷和服務降級一起使用。
服務隔離:因爲默認情況下,只有一個線程池會維護所有的服務接口,如果大量的請求訪問同一個接口,達到tomcat 線程池默認極限,可能會導致其他服務無法訪問。
解決服務雪崩效應:使用服務隔離機制(線程池方式和信號量),使用線程池方式實現隔離的原理: 相當於每個接口(服務)都有自己獨立的線程池,因爲每個線程池互不影響,這樣的話就可以解決服務雪崩效應。
- 線程池隔離:每個服務接口,都有自己獨立的線程池,每個線程池互不影響。
- 信號量隔離:使用一個原子計數器(或信號量)來記錄當前有多少個線程在運行,當請求進來時先判斷計數器的數值,若超過設置的最大線程個數則拒絕該請求,若不超過則通行,這時候計數器+1,請求返回成功後計數器-1。
2.代碼
2.1 網關限流
網關限流,淘東電商項目採用的是基於谷歌RateLimiter實現限流。Google的Guava工具包中就提供了一個限流工具類——RateLimiter,本文也是通過使用該工具類來實現限流功能,RateLimiter是基於“令牌桶算法”來實現限流的。之前的的網關是使用“責任鏈設計模式”來重新構造了,本文也使用責任鏈模式添加RateLimiter來實現限流。
①添加 guava maven依賴:
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
②添加限流Handler:
/**
* description: 服務限流
* create by: YangLinWei
* create time: 2020/5/26 3:00 下午
*/
@Component
@Slf4j
public class CurrentLimitHandler extends BaseHandler implements GatewayHandler {
private RateLimiter rateLimiter = RateLimiter.create(1);
@Autowired
private GenerateToken generateToken;
@Override
public Boolean service(RequestContext ctx, String ipAddres, HttpServletRequest request, HttpServletResponse response) {
// 1.用戶限流頻率設置 每秒中限制1個請求
boolean tryAcquire = rateLimiter.tryAcquire(0, TimeUnit.SECONDS);
if (!tryAcquire) {
resultError(ctx, "There are too many people snapping up goods now. Please wait a moment!");
return Boolean.FALSE;
}
// 2.使用redis限制用戶訪問頻率
String seckillId = request.getParameter("seckillId");
String seckillToken = generateToken.getListKeyToken(seckillId + "");
if (StringUtils.isEmpty(seckillToken)) {
log.info(">>>seckillId:{}, The second kill has sold out, please come again next time!", seckillId);
resultError(ctx, "The second kill has sold out, please come again next time!");
return Boolean.FALSE;
}
if (gatewayHandler != null) {
gatewayHandler.service(ctx, ipAddres, request, response);
}
return Boolean.TRUE;
}
}
③工廠定義網關過濾步驟:
public class FactoryHandler {
public static GatewayHandler getHandler() {
// 1.黑名單攔截
GatewayHandler handler1 = (GatewayHandler) SpringContextUtil.getBean("blackListHandler");
// 2.API接口參數接口驗籤
GatewayHandler handler2 = (GatewayHandler) SpringContextUtil.getBean("verifySignHandler");
handler1.setNextHandler(handler2);
// 3.參數過濾
GatewayHandler handler3 = (GatewayHandler) SpringContextUtil.getBean("filterParamHandler");
handler1.setNextHandler(handler3);
//4.服務限流
GatewayHandler handler4 = (GatewayHandler) SpringContextUtil.getBean("currentLimitHandler");
handler3.setNextHandler(handler4);
//5.驗證accessToken
GatewayHandler handler5 = (GatewayHandler) SpringContextUtil.getBean("apiAuthorityHandler");
handler4.setNextHandler(handler5);
return handler1;
}
}
2.2 服務保護
①添加Hystrix maven依賴:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
②啓動類開啓Hystrix
③服務接口保護
@Override
@Transactional
@HystrixCommand(fallbackMethod = "spikeFallback")
public BaseResponse<JSONObject> spike(String phone, Long seckillId) {
// 1.參數驗證
if (StringUtils.isEmpty(phone)) {
return setResultError("手機號碼不能爲空!");
}
if (seckillId == null) {
return setResultError("商品庫存id不能爲空!");
}
// 2.從redis從獲取對應的秒殺token
String seckillToken = generateToken.getListKeyToken(seckillId + "");
if (StringUtils.isEmpty(seckillToken)) {
log.info(">>>seckillId:{}, 親,該秒殺已經售空,請下次再來!", seckillId);
return setResultError("親,該秒殺已經售空,請下次再來!");
}
// 3.獲取到秒殺token之後,異步放入mq中實現修改商品的庫存
sendSeckillMsg(seckillId, phone);
return setResultSuccess("正在排隊中.......");
}
private BaseResponse<JSONObject> spikeFallback(String phone, Long seckillId) {
return setResultError("服務器忙,請稍後重試!");
}
3.測試
3.1 網關限流測試
瀏覽器訪問http://localhost/api-spike/spike?phone=13800000001&seckillId=100001,可以看到:
在1秒鐘之內繼續訪問,可以看到限流如下:
3.2 服務保護測試
服務保護測試採用JMeter來測試,由於測試條件的不允許,本文不再演示。如果要看演示的效果,可以參考之前寫的博客《微服務技術系列教程(22) - SpringCloud- 服務保護機制Hystrix》。