MySQL 樂觀鎖與悲觀鎖

悲觀鎖

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

悲觀鎖:假定會(huì)發(fā)生并發(fā)沖突,屏蔽一切可能違反數(shù)據(jù)完整性的操作。

Java synchronized 就屬于悲觀鎖的一種實(shí)現(xiàn),每次線程要修改數(shù)據(jù)時(shí)都先獲得鎖,保證同一時(shí)刻只有一個(gè)線程能操作數(shù)據(jù),其他線程則會(huì)被block。

樂觀鎖

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

樂觀鎖:假設(shè)不會(huì)發(fā)生并發(fā)沖突,只在提交操作時(shí)檢查是否違反數(shù)據(jù)完整性。

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

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

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

MySQL隱式和顯示鎖定

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

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

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

實(shí)戰(zhàn)

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

假設(shè),MySQL數(shù)據(jù)庫中商品庫存表tb_product_stock 結(jié)構(gòu)定義如下:

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 '庫存數(shù)量',
  `create_time` DATETIME NOT NULL COMMENT '創(chuàng)建時(shí)間',
  `modify_time` DATETIME NOT NULL COMMENT '更新時(shí)間',
  PRIMARY KEY (`id`),
  UNIQUE KEY `index_pid` (`product_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='商品庫存表';

對(duì)應(yīng)的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;
    }
}

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

    /**
     * 更新庫存(不考慮并發(fā))
     * @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;
    }

多線程并發(fā)情況下,會(huì)存在超賣的可能。

悲觀鎖

/**
     * 更新庫存(使用悲觀鎖)
     * @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;
    }

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

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

樂觀鎖與悲觀鎖的區(qū)別

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

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

小結(jié)

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容