深入理解高併發超賣一系列問題與解決方案(近7萬字詳解,跳槽漲薪必備寶藏珍藏級分享)

小松聊PHP进阶發表於2024-04-20

破除困境帶你飛

能遇上高併發的,基本都是有點規模的公司,小公司基本都是CRUD。
想去一線城市跳槽,想去有高併發的公司,但是沒有高併發經驗,沒有高併發的經驗,就去不了高併發的公司,去不了這樣的公司,就沒有高併發經驗,前狼後虎兩頭堵的困境,幹就完了。

一語道破

超賣問題是屬於併發安全問題,在併發情況下出現資料一致性的問題的表現,據有代表性。
這是個機率問題,不是一定發生或一定不發生。
核心問題就兩個:

  • 併發引起的資源競爭卻沒有加鎖,導致執行時序不可控(MySQL超賣)。
  • 多個讀寫操作存在間隙,導致併發請求透過間隙插隊引發的時序不可控問題(Redis超賣)。

解決方案也很簡單,上鎖或者保證無間隙執行就行了。

併發問題僅僅只是一種,至於併發帶來的,大資料儲存、檢索、以及安全問題都是需要考慮進去的。

MySQL超賣原理分析

假設無併發情況下程式碼邏輯沒問題。這個問題主要出現在獲取庫存資料的方式上,併發過來時,多個請求獲取到的庫存一致,然後都在這個一致的庫存基礎上扣庫存,自然要出錯。

MySQL的解決方案

併發存在資源爭奪問題,時序是不可控的,所以要上鎖,強制在短時間內讓資料序列更改。

  • 樂觀鎖:MySQL樂觀鎖與悲觀鎖
  • 悲觀鎖:MySQL鎖(讀鎖、共享鎖、寫鎖、S鎖、排它鎖、獨佔鎖、X鎖、表鎖、意向鎖、自增鎖、MDL鎖、RL鎖、GL鎖、NKL鎖、插入意向鎖、間隙鎖、頁鎖、悲觀鎖、樂觀鎖、隱式鎖、顯示鎖、全域性鎖、死鎖)
  • 分散式鎖:深入理解PHP+Redis實現分散式鎖的相關問題

Redis超賣原理分析

Redis超賣,主要是開發者沒有考慮到併發下資源爭奪的間隙問題。
redis get('stock')是5,然後decr('stock'),想讓庫存減到4,看起來沒毛病。
但是get和decr是兩條語句,因此存在間隙,get('stock')是5只能代表執行的那個時刻是5,decr在執行時不能保證redis是以5的基礎上自減的,可能已經被秒成0了。

Redis的解決方案

  • 笨方法:
    由於Redis是單執行緒的,利用Redis雙向連結串列的特性可以完成,利用左推右拉的單向佇列完成對庫存的扣減。笨方法,笨就笨在要是有1000個商品,一共50000個庫存,難道要存50000條資料嗎。
    虛擬碼如下:
    實現透過快取預熱,將商品id快取進redis佇列中,例如:lPush('goods_ids' , 商品id)	
    然後搶購時:在逐個取出來,利用這些資料,做其它邏輯操作rPush('goods_ids', 商品id),知道這個動作返回false,證明庫存全部扣完。
    
  • Redis+lua:
    利用Redis+Lua指令碼的方式,讓扣庫存的動作無間隙執行,超賣問題,用redis操作string,或者hash型別都行。
    //用字串型別操作,單商品庫存
    $lua = <<<EOF
        local stock_key   = KEYS[1]                                -- Redis中儲存庫存數量的鍵名
        local input_stock = tonumber(ARGV[1])                      -- 要扣除的庫存數量
        local redis_stock = tonumber(redis.call('GET', stock_key)) -- 獲取當前庫存數量
        if redis_stock >= input_stock then
            redis.call('DECRBY', stock_key, input_stock)           -- 如果庫存充足,則扣除庫存數量
            return redis_stock - input_stock                       -- 返回扣除後的庫存數量
        else
            return -1                                              -- 庫存不足,返回標記值,別返回0,有歧義
        end
    EOF;
    
    $redis = new Redis();
    $redis->connect('127.0.0.1', 6379);
    //stock為string名,2為要扣庫存的數量
    $res = $redis->eval($lua, ['stock' , 2], 1);
    if($res == -1) {
        //庫存不足
    }
    
    //庫存充足,其它下游流程...
    
    用hash型別操作多商品庫存
    $script = <<<EOF
        local hash_key    = KEYS[1]
        local goods_id    = KEYS[2]
        local input_stock = - tonumber(ARGV[1])
        if input_stock >= 0 then
            return -1 -- 表單驗證
        end
        
        local redis_stock = tonumber(redis.call('HGET', hash_key, goods_id))
        if redis_stock == nil then
            return -2 -- 商品不存在
        end
    
        local stock_res = redis_stock + input_stock
        if stock_res < 0 then
            return -3 -- 庫存不足
        end
    
        redis.call('HSET', hash_key, goods_id, stock_res)
        return stock_res
    EOF;
    //扣減商品id為50的3個庫存。
    $res = $redis->eval($script, ['stock', 50, 3], 2);
    if($res == -1) {
        echo '庫存資料不合法';
        return;
    }
    if($res == -2) {
        echo '商品不存在';
        return;
    }
    if($res == -3) {
        echo '庫存不足';
        return;
    }
    
    echo "庫存扣減成功,當前庫存為:{$res}";
    

關於Redis+Lua是否是原子性執行的爭議問題

https://redis.io/docs/latest/develop/interact/programmability/eval-intro/
對Redis官網進行搜尋,出現了原子性的字眼。
原話是:
Blocking semantics that ensure the script's atomic execution.
Lua lets you run part of your application logic inside Redis. Such scripts can perform conditional updates across multiple keys, possibly combining several different data types atomically.

但是我想了想有矛盾的地方:
MySQL使用了undo log來保證原子性,要麼成功全部執行,要麼失敗全部回滾。
眾所周知,Redis不支援回滾的,那麼ACID的A就沒辦法全部保證,最多是沒有執行期間沒有間隙,不被其它過來的請求影響,引起併發問題。

然後我又看了看阿里某架構師對此的剖析,跟我設想的一樣:
Redis會把Lua指令碼當做一個整體去執行,中間不會被其它的命令插入,但是如果執行過程中出現了錯誤,事務是不會回滾的。
也就意味著執行Lua指令碼的過程不可被拆分,不可被中斷,但是遇到錯誤不會回滾。

併發情況下MySQL與Redis快取一致性問題詳解

併發情況單個MySQL或單個Redis本身都有讀寫不一致的問題,更何況MySQL與Redis兩個元件間通訊又沒有事務的約束,或者鎖的加持,同樣會出現一致性問題。
深入理解高併發下的MySQL與Redis快取一致性問題(增刪改查資料快取的一致性、Canal、分散式系統CAP定理、BASE理論、強、弱一致性、順序、線性、因果、最終一致性)

高併發帶來的資料冪等性問題

庫存一致性是一方面,庫存保證好了,不代表,其它地方就一定不會出現庫存一致性問題:
高併發下資料冪等問題的9種解決方案

高併發帶來海量資料MySQL查詢問題

MySQL索引底層原理相關問題自總結(難度對標18K-25K薪資,已總結80+,持續更新中)

MySQL查詢最佳化方案彙總(索引相關)

高併發帶來的億級大資料檢索問題

萬字詳解PHP+Sphinx中文億級資料全文檢索實戰(實測億級資料0.1秒搜尋耗時)

高併發從側面帶來的安全問題

深入理解PHP+Redis實現布隆過濾器(億級大資料處理和駭客攻防必備)

其它文章持續更新中……

相關文章