教你从0到1搭建秒杀系统-限流
本文是秒殺系統(tǒng)的第二篇,主要講解接口限流措施。接口限流其實(shí)定義也非常廣,接口限流本身也是系統(tǒng)安全防護(hù)的一種措施,在面臨高并發(fā)的請(qǐng)購請(qǐng)求時(shí),我們?nèi)绻粚?duì)接口進(jìn)行限流,可能會(huì)對(duì)后臺(tái)系統(tǒng)造成極大的壓力,尤其是對(duì)于下單的接口,過多的請(qǐng)求打到數(shù)據(jù)庫會(huì)對(duì)系統(tǒng)的穩(wěn)定性造成影響。所以對(duì)于秒殺系統(tǒng):
- 會(huì)盡量選擇獨(dú)立于公司其他后端系統(tǒng)之外進(jìn)行單獨(dú)部署,以免秒殺業(yè)務(wù)崩潰影響到其他系統(tǒng)
- 除了獨(dú)立部署秒殺業(yè)務(wù)之外,我們能夠做的就是盡量讓后臺(tái)系統(tǒng)穩(wěn)定優(yōu)雅的處理大量請(qǐng)求。
列舉幾種容易理解的接口限流的措施:
- 令牌桶限流
- 單用戶訪問頻率限流
- 搶購接口隱藏
因?yàn)槠鶗?huì)比較長,所以會(huì)分兩篇文章來進(jìn)行講解,本篇主要講令牌桶限流,后面兩種我們一并在后面的一篇文章介紹。
令牌桶限流
令牌桶限流算法
令牌桶算法最初來源于計(jì)算機(jī)網(wǎng)絡(luò)。在網(wǎng)絡(luò)傳輸數(shù)據(jù)時(shí),為了防止網(wǎng)絡(luò)擁塞,需限制流出網(wǎng)絡(luò)的流量,使流量以比較均勻的速度向外發(fā)送。令牌桶算法就實(shí)現(xiàn)了這個(gè)功能,可控制發(fā)送到網(wǎng)絡(luò)上數(shù)據(jù)的數(shù)目,并允許突發(fā)數(shù)據(jù)的發(fā)送。
如圖,大小固定的令牌桶可自行以恒定的速率源源不斷地產(chǎn)生令牌。如果令牌不被消耗,或者被消耗的速度小于產(chǎn)生的速度,令牌就會(huì)不斷地增多,直到把桶填滿。后面再產(chǎn)生的令牌就會(huì)從桶中溢出。最后桶中可以保存的最大令牌數(shù)永遠(yuǎn)不會(huì)超過桶的大小。如果請(qǐng)求需要被處理,則需要先從桶里獲取一個(gè)令牌,當(dāng)桶里沒有令牌可取時(shí),則拒絕服務(wù)。
令牌桶算法與漏桶算法
漏桶算法思路很簡(jiǎn)單,水(請(qǐng)求)先進(jìn)入到漏桶里,漏桶以一定的速度出水,當(dāng)水流入速度過大會(huì)直接溢出,可以看出漏桶算法能強(qiáng)行限制數(shù)據(jù)的傳輸速率。
漏桶算法與令牌桶算法在表面看起來類似,很容易將兩者混淆。但事實(shí)上,這兩者具有截然不同的特性,且為不同的目的而使用。漏桶算法與令牌桶算法的區(qū)別在于:
- 漏桶算法能夠強(qiáng)行限制數(shù)據(jù)的傳輸速率,令牌桶算法能夠在限制數(shù)據(jù)的平均傳輸速率的同時(shí)還允許某種程度的突發(fā)傳輸;
- 在某些情況下,漏桶算法不能夠有效地使用網(wǎng)絡(luò)資源。因?yàn)槁┩暗穆┏鏊俾适枪潭ǖ?#xff0c;所以即使網(wǎng)絡(luò)中沒有發(fā)生擁塞,漏桶算法也不能使某一個(gè)單獨(dú)的數(shù)據(jù)流達(dá)到端口速率。因此,漏桶算法對(duì)于存在突發(fā)特性的流量來說缺乏效率。而令牌桶算法則能夠滿足這些具有突發(fā)特性的流量。
并不能說明令牌桶一定比漏洞好,它們使用場(chǎng)景不一樣:
- 令牌桶可以用來保護(hù)自己,主要用來對(duì)調(diào)用者頻率進(jìn)行限流,為的是讓自己不被打垮。所以如果自己本身有處理能力的時(shí)候,如果流量突發(fā)(實(shí)際消費(fèi)能力強(qiáng)于配置的流量限制),那么實(shí)際處理速率可以超過配置的限制。
- 漏桶算法用來保護(hù)他人,也就是保護(hù)他所調(diào)用的系統(tǒng)。主要場(chǎng)景是,當(dāng)調(diào)用的第三方系統(tǒng)本身沒有保護(hù)機(jī)制,或者有流量限制的時(shí)候,我們的調(diào)用速度不能超過他的限制,由于我們不能更改第三方系統(tǒng),所以只有在主調(diào)方控制。這個(gè)時(shí)候,即使流量突發(fā),也必須舍棄。
限流工具類RateLimiter
Google開源工具包Guava提供了限流工具類RateLimiter,該類基于令牌桶算法來完成限流,非常易于使用。我們利用它在之前講過的樂觀鎖搶購接口上增加該令牌桶限流代碼:
@Controller public class OrderController {private static final Logger LOGGER = LoggerFactory.getLogger(OrderController.class);@Autowiredprivate StockService stockService;@Autowiredprivate OrderService orderService;//每秒放行10個(gè)請(qǐng)求RateLimiter rateLimiter = RateLimiter.create(10);/*** 樂觀鎖更新庫存 + 令牌桶限流* @param sid* @return*/@RequestMapping("/createOptimisticOrder/{sid}")@ResponseBodypublic String createOptimisticOrder(@PathVariable int sid) {// 阻塞式獲取令牌//LOGGER.info("等待時(shí)間" + rateLimiter.acquire());// 非阻塞式獲取令牌if (!rateLimiter.tryAcquire(1000, TimeUnit.MILLISECONDS)) {LOGGER.warn("你被限流了,真不幸,直接返回失敗");return "購買失敗,庫存不足";}int id;try {id = orderService.createOptimisticOrder(sid);LOGGER.info("購買成功,剩余庫存為: [{}]", id);} catch (Exception e) {LOGGER.error("購買失敗:[{}]", e.getMessage());return "購買失敗,庫存不足";}return String.format("購買成功,剩余庫存為:%d", id);} }在代碼中做了相關(guān)的解釋。使用RateLimiter rateLimiter = RateLimiter.create(10)初始化令牌桶類,每秒放行10個(gè)請(qǐng)求。使用rateLimiter 獲取令牌的方式主要有兩種:
- 阻塞式獲取令牌:使用rateLimiter.acquire()實(shí)現(xiàn)。請(qǐng)求進(jìn)來后,若令牌桶里沒有足夠的令牌,就在這里阻塞住,等待令牌的發(fā)放;
- 非阻塞式獲取令牌:使用rateLimiter.tryAcquire(1000, TimeUnit.MILLISECONDS)實(shí)現(xiàn)。請(qǐng)求進(jìn)來后,若令牌桶里沒有足夠的令牌,會(huì)嘗試等待設(shè)置好的時(shí)間(這里寫了1000ms),其會(huì)自動(dòng)判斷在1000ms后,這個(gè)請(qǐng)求能不能拿到令牌,如果不能拿到,直接返回?fù)屬徥?。如果timeout設(shè)置為0,則等于阻塞時(shí)獲取令牌。
我們使用JMeter設(shè)置200個(gè)線程,來同時(shí)搶購數(shù)據(jù)庫里庫存100個(gè)的iphone。相關(guān)結(jié)構(gòu)和數(shù)據(jù)可以看教你從0到1搭建秒殺系統(tǒng)-防超賣。
令牌桶算法實(shí)踐
首先使用非阻塞式獲取令牌的方式進(jìn)行操作,請(qǐng)求完以后來看看購買結(jié)果:
有數(shù)據(jù)可以看到,最終只有11個(gè)被賣出去了。在這種情況下請(qǐng)求能夠沒被限流的比率在11%左右。
可以看到,200個(gè)請(qǐng)求中沒有被限流的請(qǐng)求里,由于樂觀鎖的原因,會(huì)出現(xiàn)一些并發(fā)更新數(shù)據(jù)庫失敗的問題,導(dǎo)致商品沒有被賣出。我們?cè)僭囈辉嚵钆仆八惴ǖ淖枞绞褂?#xff0c;我們將代碼換成rateLimiter.acquire();,然后將數(shù)據(jù)庫恢復(fù)成100個(gè)庫存,訂單表清零。開始請(qǐng)求:
可以看到,100個(gè)全部賣出。這里首先看一下操作結(jié)果和打印日志:
對(duì)照著請(qǐng)求的打印日志,有幾個(gè)問題需要說明一下:
- 首先,所有請(qǐng)求進(jìn)入了處理流程,但是被限流成每秒處理10個(gè)請(qǐng)求。
- 在剛開始的請(qǐng)求里,令牌桶里一下子被取了10個(gè)令牌,所以出現(xiàn)了第二張圖中的,樂觀鎖并發(fā)更新失敗,然而在后面的請(qǐng)求中,由于令牌一旦生成就被拿走,所以請(qǐng)求進(jìn)來的很均勻,沒有再出現(xiàn)并發(fā)更新庫存的情況。這也符合“令牌桶”的定義,可以應(yīng)對(duì)突發(fā)請(qǐng)求(只是由于樂觀鎖,所以購買沖突了)。而非“漏桶”的永遠(yuǎn)恒定的請(qǐng)求限制。
- 200個(gè)請(qǐng)求,在樂觀鎖的情況下,賣出了全部100個(gè)商品,如果沒有該限流,而請(qǐng)求又過于集中的話,會(huì)賣不出去幾個(gè)。
令牌桶限流算法說完了,我們?cè)倩仡^思考超賣的問題,在海量請(qǐng)求的場(chǎng)景下使用樂觀鎖,會(huì)導(dǎo)致大量的請(qǐng)求返回?fù)屬徥?#xff0c;用戶體驗(yàn)極差。然而使用悲觀鎖,比如數(shù)據(jù)庫事務(wù),則可以讓數(shù)據(jù)庫一個(gè)個(gè)處理庫存數(shù)修改,修改成功后再迎接下一個(gè)請(qǐng)求,所以在不同情況下,應(yīng)該根據(jù)實(shí)際情況使用悲觀鎖和樂觀鎖。兩種鎖各有優(yōu)缺點(diǎn),不能單純的定義哪個(gè)好于哪個(gè):
- 樂觀鎖比較適合數(shù)據(jù)修改比較少,讀取比較頻繁的場(chǎng)景,即使出現(xiàn)了少量的沖突,這樣也省去了大量的鎖的開銷,故而提高了系統(tǒng)的吞吐量;
- 但是如果經(jīng)常發(fā)生沖突(寫數(shù)據(jù)比較多的情況下),上層應(yīng)用不不斷的retry,這樣反而降低了性能,對(duì)于這種情況使用悲觀鎖就更合適。
悲觀鎖實(shí)踐
我們?yōu)榱嗽诟吡髁肯?#xff0c;能夠更好更快的賣出商品,我們實(shí)現(xiàn)一個(gè)悲觀鎖(事務(wù)for update更新庫存),看看悲觀鎖的結(jié)果如何。在Controller中,增加一個(gè)悲觀鎖賣商品接口:
/*** 事務(wù)for update更新庫存* @param sid* @return*/ @RequestMapping("/createPessimisticOrder/{sid}") @ResponseBody public String createPessimisticOrder(@PathVariable int sid) {int id;try {id = orderService.createPessimisticOrder(sid);LOGGER.info("購買成功,剩余庫存為: [{}]", id);} catch (Exception e) {LOGGER.error("購買失敗:[{}]", e.getMessage());return "購買失敗,庫存不足";}return String.format("購買成功,剩余庫存為:%d", id); }在Service中,給該賣商品流程加上事務(wù):
@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED) @Override public int createPessimisticOrder(int sid){//校驗(yàn)庫存(悲觀鎖for update)Stock stock = checkStockForUpdate(sid);//更新庫存saleStock(stock);//創(chuàng)建訂單int id = createOrder(stock);return stock.getCount() - (stock.getSale()); }/*** 檢查庫存 ForUpdate* @param sid* @return*/ private Stock checkStockForUpdate(int sid) {Stock stock = stockService.getStockByIdForUpdate(sid);if (stock.getSale().equals(stock.getCount())) {throw new RuntimeException("庫存不足");}return stock; }/*** 更新庫存* @param stock*/ private void saleStock(Stock stock) {stock.setSale(stock.getSale() + 1);stockService.updateStockById(stock); }/*** 創(chuàng)建訂單* @param stock* @return*/ private int createOrder(Stock stock) {StockOrder order = new StockOrder();order.setSid(stock.getId());order.setName(stock.getName());int id = orderMapper.insertSelective(order);return id; }這里使用Spring的事務(wù),@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED),如果遇到回滾,則返回Exception,并且事務(wù)傳播使用PROPAGATION_REQUIRED–支持當(dāng)前事務(wù),如果當(dāng)前沒有事務(wù),就新建一個(gè)事務(wù)。
我們依然設(shè)置100個(gè)商品,清空訂單表,開始用JMeter更改請(qǐng)求的接口/createPessimisticOrder/1,發(fā)起200個(gè)請(qǐng)求:
可以看到,200個(gè)請(qǐng)求,100個(gè)返回了搶購成功,100個(gè)返回了搶購失敗。并且商品賣給了前100個(gè)進(jìn)來的請(qǐng)求,十分的有序。所以,悲觀鎖在大量請(qǐng)求的請(qǐng)求下,有著更好的賣出成功率。但是需要注意的是,如果請(qǐng)求量巨大,悲觀鎖會(huì)導(dǎo)致后面的請(qǐng)求進(jìn)行了長時(shí)間的阻塞等待,用戶就必須在頁面等待,很像是“假死”,可以通過配合令牌桶限流,或者是給用戶顯著的等待提示來優(yōu)化。
猜你感興趣:
教你從0到1搭建秒殺系統(tǒng)-防超賣
教你從0到1搭建秒殺系統(tǒng)-限流
教你從0到1搭建秒殺系統(tǒng)-搶購接口隱藏與單用戶限制頻率
教你從0到1搭建秒殺系統(tǒng)-緩存與數(shù)據(jù)庫雙寫一致
教你從0到1搭建秒殺系統(tǒng)-Canal快速入門(番外篇)
教你從0到1搭建秒殺系統(tǒng)-訂單異步處理
更多文章請(qǐng)點(diǎn)擊:更多…
總結(jié)
以上是生活随笔為你收集整理的教你从0到1搭建秒杀系统-限流的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 教你从0到1搭建秒杀系统-防超卖
- 下一篇: 教你从0到1搭建秒杀系统-抢购接口隐藏与