<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          硬核講解秒殺設(shè)計(jì)

          共 7010字,需瀏覽 15分鐘

           ·

          2021-05-24 21:40


          1 秒殺場景

          秒殺場景

          1. 登陸12306進(jìn)行火車票搶座 

          2. 1599元購入飛天茅臺

          3. 周董演唱會(huì)的門票

          4. 雙十一秒殺活動(dòng)

          秒殺場景關(guān)注點(diǎn)

          1. 嚴(yán)格防止超賣:庫存1000件賣了1020件,要?dú)€(gè)碼農(nóng)祭天了!防止超賣是秒殺系統(tǒng)設(shè)計(jì)最核心的部分。

          2. 防止黑產(chǎn):防止不懷好意的羊毛黨薅羊毛。

          3. 保證用戶體驗(yàn):高并發(fā)下,給用戶提供友善的購物體驗(yàn),盡可能支持比較高的QPS等等。

          接下來就讓我們按照關(guān)注點(diǎn),不斷細(xì)化秒殺場景。

          2 第1版-裸奔

          裸奔秒殺

          不加思考,上來直接按照 SpringBoot + MyBatis 模式進(jìn)行秒殺系統(tǒng)的設(shè)計(jì),流程如下:

          1. Controller層獲得用戶秒殺請求后調(diào)用Service層。

          2. Service層獲得請求后要要檢查已售數(shù)據(jù)跟庫存總量是否一致,一致說明商品賣沒了,不一致說明還有庫存,那就調(diào)用DAO層對已售數(shù)量進(jìn)行加1。

          3. DAO層獲得請求后直接通過MyBatis操作數(shù)據(jù)庫實(shí)現(xiàn)已售數(shù)量加1跟訂單創(chuàng)建。

          如果你用Postman去測試會(huì)發(fā)現(xiàn)是OK的,但如果你用專業(yè)的并發(fā)測試工具JMeter模式多用戶并發(fā)請求會(huì)發(fā)現(xiàn)訂單創(chuàng)建數(shù)量 > 庫存量 - 已售量。原因解釋下,比如用戶A、B并發(fā)進(jìn)行秒殺請求,此時(shí)庫存=100,已售=64。

          1. A用戶進(jìn)行描述請求,此時(shí)調(diào)用到了Service層,發(fā)現(xiàn)已售不等于庫存,此時(shí)拿到庫存數(shù)是64,A將庫存更新為63,然后創(chuàng)建訂單。

          2. B用戶進(jìn)行描述請求,此時(shí)調(diào)用到了Service層,發(fā)現(xiàn)已售不等于庫存,此時(shí)拿到庫存數(shù)是64,B將庫存更新為63,然后創(chuàng)建訂單。

          3. 此時(shí)庫存減少了1個(gè)但是訂單創(chuàng)建多個(gè),賣超了!

            無鎖并發(fā)請求,賣超了

          3 第2版-悲觀鎖

          syn悲觀鎖

          遇見 并發(fā)問題 很容易想到以前學(xué)過并發(fā)編程嘛,既然Controller默認(rèn)是單例模式,那我用 synchronizedController層調(diào)用Service層的代碼進(jìn)行加鎖同步即可。

          這樣就可以解決賣超問題了,但是須知,既然是悲觀鎖,如果有1000個(gè)并發(fā)請求,那只有1個(gè)拿到鎖了。有999個(gè)會(huì)去競爭這個(gè)鎖的。

          @Transactional
          @Service
          @Transactional
          @Slf4j
          public class OrderServiceImpl implements OrderService
          {
              //校驗(yàn)庫存
              Stock stock = checkStock(id);
              //更新庫存
              updateSale(stock);
              //創(chuàng)建訂單
              return createOrder(stock);
          }

          當(dāng)然了你也可以用Spring自帶的事務(wù)注解來實(shí)現(xiàn)悲觀鎖的操作,因?yàn)橛昧?code style="font-size: inherit;line-height: inherit;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;color: rgb(248, 35, 117);background: rgb(248, 248, 248);">@Transactional就可以實(shí)現(xiàn)通過事務(wù)來控制,要么全部成功,要么全部失敗,用事務(wù)時(shí)有兩點(diǎn)需注意:

          1. 盡可能將MySQL執(zhí)行語句往方法體后面靠,因?yàn)镸ySQL事務(wù)的commit語句是在第一次執(zhí)行MySQL相關(guān)語句開始,一直到方法的結(jié)束。

          2. 設(shè)置事務(wù)的超時(shí)時(shí)間,如果不設(shè)置默認(rèn)是-1是無限長。并且事務(wù)中設(shè)置的耗時(shí)timeout = 最后一個(gè)MySQL語句耗時(shí) + 以及最后一個(gè)MySQL之前的所有耗時(shí)。

          需注意:悲觀鎖狀態(tài)下會(huì)保證商品賣出去,如果沒拿到鎖的線程會(huì)阻塞的等待拿鎖。但是他的阻塞也會(huì)給用戶帶來非常不良好的體驗(yàn)。

          4 第3版-樂觀鎖

          MySQL版本號

          我們?yōu)槊總€(gè)數(shù)量的已售數(shù)據(jù)配備個(gè)版本號,在Service層調(diào)用時(shí)獲得用戶的已售數(shù)跟對應(yīng)版本號,然后更新時(shí)將已售數(shù)跟版本號同時(shí)更新。因?yàn)?nbsp;MySQL在更新時(shí)會(huì)自帶樂觀加速機(jī)制,如果更新成功則表示搶購成功,更新失敗則表示搶購失敗,此時(shí)你會(huì)發(fā)現(xiàn)不是手速越快就一定能搶到的哦,但起碼保證了不會(huì)超賣,
          update 庫存表 set
             已售數(shù)=已售數(shù)+1,版本號=版本號+1
          where 秒殺id =#{id} and 版本號 = #{version}

          需注意:樂觀鎖狀態(tài)下,由于是隨機(jī)性的秒殺失敗,所以可能活動(dòng)結(jié)束后還會(huì)有幾個(gè)沒售出去的!

          5 第4版-限流

          最核心的超賣問題已經(jīng)解決了,接下來就是各種優(yōu)化手段了。在高并發(fā)請求中如果不對接口限流會(huì)對后臺服務(wù)器造成極大壓力,所以一般秒殺系統(tǒng)為了不影響其他業(yè)務(wù)會(huì)單獨(dú)部署到個(gè)某個(gè)服務(wù)器上,同時(shí)還會(huì)設(shè)置好限流。

          常用的限流方法有我們在 Redis 中曾經(jīng)說過,主要有漏桶算法、令牌桶算法。而Google開源項(xiàng)目GuavaRateLimiter使用的就是令牌桶控制算法。在開發(fā)高并發(fā)系統(tǒng)時(shí)有三把利器用來保護(hù)系統(tǒng):緩存、降級、限流

          1. 緩存:緩存的目的是提升系統(tǒng)訪問速度和增大系統(tǒng)處理容量。

          2. 降級:降級是當(dāng)服務(wù)器壓力劇增的情況下,根據(jù)當(dāng)前業(yè)務(wù)情況及流量對一些服務(wù)和頁面有策略的降級,以此釋放服務(wù)器資源以保證核心任務(wù)的正常運(yùn)行。

          3. 限流:限流的目的是通過對并發(fā)訪問/請求進(jìn)行限速,或者對一個(gè)時(shí)間窗口內(nèi)的請求進(jìn)行限速來保護(hù)系統(tǒng),一旦達(dá)到限制速率則可以拒絕服務(wù)、排隊(duì)或等待、降級等處理。

          5.1 漏桶算法

          漏桶算法思路:把水比作是請求,漏桶比作是系統(tǒng)處理能力極限,水先進(jìn)入到漏桶里,漏桶里的水按一定速率流出,當(dāng)流出的速率小于流入的速率時(shí),由于漏桶容量有限,后續(xù)進(jìn)入的水直接溢出(拒絕請求),以此實(shí)現(xiàn)限流。

          5.2 令牌桶算法

          令牌桶算法原理:可以理解成醫(yī)院的掛號看病,只有拿到號以后才可以進(jìn)行診病。


          流程大致
          1. 所有的請求在處理之前都需要拿到一個(gè)可用的令牌才會(huì)被處理。

          2. 根據(jù)限流大小,設(shè)置按照一定的速率往桶里添加令牌。

          3. 設(shè)置桶最大可容納值,當(dāng)桶滿時(shí)新添加的令牌就被丟棄或者拒絕。

          4. 請求達(dá)到后首先要獲取令牌桶中的令牌,拿著令牌才可以進(jìn)行其他的業(yè)務(wù)邏輯,處理完業(yè)務(wù)邏輯之后,將令牌直接刪除。

          5. 如果用戶無法獲得令牌可以選擇一直阻塞等待,也可以選擇設(shè)置好timeout機(jī)制。

          6. 令牌桶有最低限額,當(dāng)桶中的令牌達(dá)到最低限額的時(shí)候,請求處理完之后將不會(huì)刪除令牌,以此保證足夠的限流。

          工程中一般用令牌桶算法為多,一般用GoogleGuavaRateLimiter 即可。

          //創(chuàng)建令牌桶實(shí)例
          private RateLimiter rateLimiter = RateLimiter.create(20);
          // 阻塞式獲得令牌才繼續(xù)往下執(zhí)行
          rateLimiter.acquire();
          // 就等3秒看是否可以獲得令牌,返回Boolean值。
          rateLimiter.tryAcquire(3, TimeUnit.SECONDS) 

          6 第5版- 細(xì)節(jié)優(yōu)化

          有了樂觀鎖跟限流,接下來再思考寫細(xì)節(jié)問題。

          1. 秒殺要有時(shí)間范圍限制的,不能再任意時(shí)刻都可以接受秒殺請求,要實(shí)行限時(shí)搶購。

          2. 如果有懂IT人員通過抓包獲取了秒殺接口地址,在秒殺開始時(shí),不通過按鈕,直接通過腳本秒殺咋辦?要實(shí)行秒殺接口隱藏。

          3. 每個(gè)用戶單位時(shí)間內(nèi)訪問次數(shù)要做頻率限制。

          6.1 限時(shí)搶購

          很簡單,將秒殺商品放入Redis并設(shè)置超時(shí),比如我們以kill + 商品id作為key,以商品id作為value,設(shè)置180秒超時(shí)。

          127.0.0.1:6379> set kill1 1 EX 180
          OK

          加入時(shí)間校驗(yàn):

          public Integer createOrder(Integer id) {
              //redis校驗(yàn)搶購時(shí)間
              if(!stringRedisTemplate.hasKey("kill" + id)){
                  throw new RuntimeException("秒殺超時(shí),活動(dòng)已經(jīng)結(jié)束啦!!!");
              }
              //校驗(yàn)庫存
              Stock stock = checkStock(id);
              //扣庫存
              updateSale(stock);
              //下訂單
              return createOrder(stock);
          }
          6.2 秒殺接口隱藏
          接口隱藏

          1. 用戶秒殺前先通過getMd5方法獲得一個(gè)請求秒殺URL的MD5值。

          2. 請求getMd5算法,Key = 商品id + 用戶id,value = 商品id + 用戶id + 鹽 。將KV存入redis并且設(shè)置過期時(shí)間,最終返回value作為md5值。

          3. 用戶請求秒殺URL的時(shí)候需攜帶MD5值,然后Service層會(huì)根據(jù)商品id + 用戶id從redis中獲取下對應(yīng)的value,看這個(gè)value跟MD5值是否一致,絕對下一步操作。

          // 根據(jù)商品id 跟 用戶id生成個(gè)md5。
          @Override
          public String getMd5(Integer id, Integer userid) {
            //檢驗(yàn)用戶的合法性
            User user = userDAO.findById(userid);
            if(user==null)throw new RuntimeException("用戶信息不存在!");

            //檢驗(yàn)商品的合法行
            Stock stock = stockDAO.checkStock(id);
            if(stock==nullthrow new RuntimeException("商品信息不合法!");

            String hashKey = "KEY_" + userid + "_" + id;
            //生成md5,此處的 !AW# 是一個(gè)鹽,可以跟找個(gè)Random隨機(jī)生成。
            String key = DigestUtils.md5DigestAsHex((userid + id + "!AW#").getBytes());
            stringRedisTemplate.opsForValue().set(hashKey, key, 3600, TimeUnit.SECONDS);
            return key;
          }

          此時(shí)如果用戶直接請求秒殺接口就會(huì)被限制了,但如果黑客技術(shù)升級,將請求MD5跟請求秒殺接口寫到一起,還是無法防止被薅羊毛!咋辦呢?再限制下用戶訪問頻率。

          6.3 訪問頻率限制
          1. 通過前面請求后根據(jù)用戶id生成個(gè)redis中的key,value為訪問次數(shù),默認(rèn)為0,并且設(shè)置好該KV的過期時(shí)間。

          2. 用戶在驗(yàn)證是否通過秒殺隱藏接口驗(yàn)證前,先看下他的單位時(shí)間內(nèi)訪問次數(shù)是多少,如果超過閾值則直接拒絕,沒超過再進(jìn)行隱藏接口的驗(yàn)證。

          3. 這里只是舉例為用戶訪問次數(shù)限制,IP訪問次數(shù)限制類似。

          4. 秒殺源碼公眾號回復(fù)秒殺獲取。

          訪問頻率限制

          7 第6版-眾多細(xì)節(jié)優(yōu)化

          1. CDN加速:為何京東物流快,因?yàn)槿嗽谌珖鞯嘏渲昧硕鄠€(gè)倉庫。同理,我們可以將前端的一些靜態(tài)東西配置在全國各個(gè)不同的地方,用戶請求時(shí),直接請求距離自己最近的前端資源即可。

          2. 前端按鈕灰色化:如果參與過秒殺活動(dòng)會(huì)發(fā)現(xiàn),沒到秒殺時(shí)間時(shí)秒殺按鈕是灰色狀態(tài)的,只有時(shí)間到了才是可點(diǎn)擊狀態(tài)。并且秒殺開始咯也不是一直可以點(diǎn)的,可能只允許1秒內(nèi)點(diǎn)10次那種的。

          3. Nginx負(fù)載均衡:一個(gè)tomcat的QPS一般在200~1000左右,如果淘寶或京東性質(zhì)的秒殺,就需要搞個(gè)Nginx負(fù)載均衡來支持幾萬級別的并發(fā)了。

          4. 信息存儲Redis化:單獨(dú)的MySQL是無法支撐上萬的QPS的,既然Redis號稱可支持10W級的QPS,我們把數(shù)據(jù)信息存到Redis中就好咯嘛!有人可能會(huì)說MySQL有樂觀鎖跟事務(wù)性啊,Redis不是沒有事務(wù)性么,其實(shí)我們可以通過 Lua 腳本來實(shí)現(xiàn)并發(fā)情況下Redis的事務(wù)性操作。

          5. 消息中間件-流量削峰:秒殺成功后,如果秒殺的成功量過大,全部訂單直接寫入MySQL也是不太恰當(dāng)?shù)模梢园衙霘⒊晒Φ挠脩粜畔懭胂⒅虚g件。比如RabbitMQKafka,給用戶返回?fù)屬彸晒π畔?,然后專門代碼消費(fèi)中間件信息(生成訂單,數(shù)據(jù)持久化),因?yàn)槭钱惒较M(fèi),為防止用戶秒殺成功后無法看到訂單信息,在訂單生成前給用戶提示訂單提交排隊(duì)中,啥時(shí)候訂單異步消費(fèi)成功了再告知用戶成功。

          6. 輔助手段:秒殺前做個(gè)預(yù)演練是必須的吧,系統(tǒng)上線后QPS監(jiān)控、CPU監(jiān)控、IO監(jiān)控、緩存監(jiān)控也是必須要搞的。同時(shí)一旦服務(wù)真的扛不住了熔斷跟限流也要考慮進(jìn)去。

          7. 短URL:有時(shí)你別人發(fā)給你個(gè)超短的URL你打開后就直接跳轉(zhuǎn)為日常看到的購物頁面了,這就涉及到短URL映射了,大致思路就是做個(gè)鏈接映射,在此基礎(chǔ)上也可以玩出各種花樣,反正挺有趣的(有興趣可以水一篇)。

            秒殺大致流程圖
          8. 工業(yè)化秒殺:真正工業(yè)化的秒殺絕對不止我前面說的那么簡單哦,起碼你會(huì)接觸到 MQ、SpringBoot、RedisDubbo、ZK 、Mavenlua等知識點(diǎn)

          8 參考

          1. B站:https://b23.tv/IsifGk

          2. github:https://github.com/qiurunze123/miaosha


          推薦閱讀:
          你管這破玩意兒叫 Token?
          一舉拿下高可用與分布式協(xié)調(diào)系統(tǒng)設(shè)計(jì)!

          一文讀懂微內(nèi)核架構(gòu)


          關(guān)互聯(lián)網(wǎng)全棧架構(gòu),價(jià)。


          瀏覽 53
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  18操逼毛片 | 久久百万精品 | 豆花传剧高清在线看 | 韩国精品无码 | 亚洲人xxxxc |