MySQL 樂觀鎖與悲觀鎖

悲觀鎖

悲觀鎖(Pessimistic Lock),顧名思義,就是很悲觀,每次去拿數據的時候都認為別人會修改,所以每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會block直到它拿到鎖。

悲觀鎖:假定會發生并發沖突,屏蔽一切可能違反數據完整性的操作。

Java synchronized 就屬于悲觀鎖的一種實現,每次線程要修改數據時都先獲得鎖,保證同一時刻只有一個線程能操作數據,其他線程則會被block。

樂觀鎖

樂觀鎖(Optimistic Lock),顧名思義,就是很樂觀,每次去拿數據的時候都認為別人不會修改,所以不會上鎖,但是在提交更新的時候會判斷一下在此期間別人有沒有去更新這個數據。樂觀鎖適用于讀多寫少的應用場景,這樣可以提高吞吐量。

樂觀鎖:假設不會發生并發沖突,只在提交操作時檢查是否違反數據完整性。

樂觀鎖一般來說有以下2種方式:

  1. 使用數據版本(Version)記錄機制實現,這是樂觀鎖最常用的一種實現方式。何謂數據版本?即為數據增加一個版本標識,一般是通過為數據庫表增加一個數字類型的 “version” 字段來實現。當讀取數據時,將version字段的值一同讀出,數據每更新一次,對此version值加一。當我們提交更新的時候,判斷數據庫表對應記錄的當前版本信息與第一次取出來的version值進行比對,如果數據庫表當前版本號與第一次取出來的version值相等,則予以更新,否則認為是過期數據。
  2. 使用時間戳(timestamp)。樂觀鎖定的第二種實現方式和第一種差不多,同樣是在需要樂觀鎖控制的table中增加一個字段,名稱無所謂,字段類型使用時間戳(timestamp), 和上面的version類似,也是在更新提交的時候檢查當前數據庫中數據的時間戳和自己更新前取到的時間戳進行對比,如果一致則OK,否則就是版本沖突。

Java JUC中的atomic包就是樂觀鎖的一種實現,AtomicInteger 通過CAS(Compare And Set)操作實現線程安全的自增。

MySQL隱式和顯示鎖定

MySQL InnoDB采用的是兩階段鎖定協議(two-phase locking protocol)。在事務執行過程中,隨時都可以執行鎖定,鎖只有在執行 COMMIT或者ROLLBACK的時候才會釋放,并且所有的鎖是在同一時刻被釋放。前面描述的鎖定都是隱式鎖定,InnoDB會根據事務隔離級別在需要的時候自動加鎖。

另外,InnoDB也支持通過特定的語句進行顯示鎖定,這些語句不屬于SQL規范:

  • SELECT ... LOCK IN SHARE MODE
  • SELECT ... FOR UPDATE

實戰

接下來,我們通過一個具體案例來進行分析:考慮電商系統中的下單流程,商品的庫存量是固定的,如何保證商品數量不超賣? 其實需要保證數據一致性:某個人點擊秒殺后系統中查出來的庫存量和實際扣減庫存時庫存量的一致性就可以。

假設,MySQL數據庫中商品庫存表tb_product_stock 結構定義如下:

CREATE TABLE `tb_product_stock` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增ID',
  `product_id` bigint(32) NOT NULL COMMENT '商品ID',
  `number` INT(8) NOT NULL DEFAULT 0 COMMENT '庫存數量',
  `create_time` DATETIME NOT NULL COMMENT '創建時間',
  `modify_time` DATETIME NOT NULL COMMENT '更新時間',
  PRIMARY KEY (`id`),
  UNIQUE KEY `index_pid` (`product_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='商品庫存表';

對應的POJO類:

class ProductStock {
    private Long productId; //商品id
    private Integer number; //庫存量

    public Long getProductId() {
        return productId;
    }

    public void setProductId(Long productId) {
        this.productId = productId;
    }

    public Integer getNumber() {
        return number;
    }

    public void setNumber(Integer number) {
        this.number = number;
    }
}

不考慮并發的情況下,更新庫存代碼如下:

    /**
     * 更新庫存(不考慮并發)
     * @param productId
     * @return
     */
    public boolean updateStockRaw(Long productId){
        ProductStock product = query("SELECT * FROM tb_product_stock WHERE product_id=#{productId}", productId);
        if (product.getNumber() > 0) {
            int updateCnt = update("UPDATE tb_product_stock SET number=number-1 WHERE product_id=#{productId}", productId);
            if(updateCnt > 0){    //更新庫存成功
                return true;
            }
        }
        return false;
    }

多線程并發情況下,會存在超賣的可能。

悲觀鎖

/**
     * 更新庫存(使用悲觀鎖)
     * @param productId
     * @return
     */
    public boolean updateStock(Long productId){
        //先鎖定商品庫存記錄
        ProductStock product = query("SELECT * FROM tb_product_stock WHERE product_id=#{productId} FOR UPDATE", productId);
        if (product.getNumber() > 0) {
            int updateCnt = update("UPDATE tb_product_stock SET number=number-1 WHERE product_id=#{productId}", productId);
            if(updateCnt > 0){    //更新庫存成功
                return true;
            }
        }
        return false;
    }

樂觀鎖

    /**
     * 下單減庫存
     * @param productId
     * @return
     */
    public boolean updateStock(Long productId){
        int updateCnt = 0;
        while (updateCnt == 0) {
            ProductStock product = query("SELECT * FROM tb_product_stock WHERE product_id=#{productId}", productId);
            if (product.getNumber() > 0) {
                updateCnt = update("UPDATE tb_product_stock SET number=number-1 WHERE product_id=#{productId} AND number=#{number}", productId, product.getNumber());
                if(updateCnt > 0){    //更新庫存成功
                    return true;
                }
            } else {    //賣完啦
                return false;
            }
        }
        return false;
    }

使用樂觀鎖更新庫存的時候不加鎖,當提交更新時需要判斷數據是否已經被修改(AND number=#{number}),只有在 number等于上一次查詢到的number時 才提交更新。

** 注意** :UPDATE 語句的WHERE 條件字句上需要建索引

樂觀鎖與悲觀鎖的區別

樂觀鎖的思路一般是表中增加版本字段,更新時where語句中增加版本的判斷,算是一種CAS(Compare And Swep)操作,商品庫存場景中number起到了版本控制(相當于version)的作用( AND number=#{number})。

悲觀鎖之所以是悲觀,在于他認為本次操作會發生并發沖突,所以一開始就對商品加上鎖(SELECT ... FOR UPDATE),然后就可以安心的做判斷和更新,因為這時候不會有別人更新這條商品庫存。

小結

這里我們通過 MySQL 樂觀鎖與悲觀鎖 解決并發更新庫存的問題,當然還有其它解決方案,例如使用 分布式鎖。目前常見分布式鎖實現有兩種:基于Redis和基于Zookeeper,基于這兩種 業界也有開源的解決方案,例如 Redisson Distributed locks Apache Curator Shared Lock ,這里就不細說,網上Google 一下就有很多資料。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,882評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,208評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,746評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,666評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,477評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,960評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,047評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,200評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,726評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,617評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,807評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,327評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,049評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,425評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,674評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,432評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,769評論 2 372

推薦閱讀更多精彩內容