悲觀鎖莲趣、樂(lè)觀鎖的區(qū)別及使用場(chǎng)景

原文作者: xingguang
原文鏈接:https://www.tiance.club/post/1011394230.html

悲觀鎖

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

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

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

樂(lè)觀鎖

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

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

樂(lè)觀鎖一般來(lái)說(shuō)有以下2種方式:

原文作者: xingguang
原文鏈接:https://www.tiance.club/post/1011394230.html

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

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

MySQL隱式和顯示鎖定
MySQL InnoDB采用的是兩階段鎖定協(xié)議(two-phase locking protocol)皂股。

在事務(wù)執(zhí)行過(guò)程中墅茉,隨時(shí)都可以執(zhí)行鎖定呜呐,鎖只有在執(zhí)行 COMMIT或者ROLLBACK的時(shí)候才會(huì)釋放,并且所有的鎖是在同一時(shí)刻被釋放蘑辑。

前面描述的鎖定都是隱式鎖定,InnoDB會(huì)根據(jù)事務(wù)隔離級(jí)別在需要的時(shí)候自動(dòng)加鎖绷旗。

原文作者: xingguang
原文鏈接:https://www.tiance.club/post/1011394230.html

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

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

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

假設(shè)匙隔,MySQL數(shù)據(jù)庫(kù)中商品庫(kù)存表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 '庫(kù)存數(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='商品庫(kù)存表';

對(duì)應(yīng)的POJO類:

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

    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ā)的情況下熏版,更新庫(kù)存代碼如下:

  /**
     * 更新庫(kù)存(不考慮并發(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} AND number=#{number}", productId, product.getNumber());
            if(updateCnt > 0){    //更新庫(kù)存成功
                return true;
            }
        }
        return false;
    }

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

悲觀鎖

/**
     * 更新庫(kù)存(使用悲觀鎖)
     * @param productId
     * @return
     */
    public boolean updateStock(Long productId){
        //先鎖定商品庫(kù)存記錄
        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){    //更新庫(kù)存成功
                return true;
            }
        }
        return false;
    }

樂(lè)觀鎖

   /**
     * 下單減庫(kù)存
     * @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){    //更新庫(kù)存成功
                    return true;
                }
            } else {    //賣完啦
                return false;
            }
        }
        return false;
    }

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

樂(lè)觀鎖與悲觀鎖的區(qū)別
<font color='red'> 樂(lè)觀鎖的思路一般是表中增加版本字段,更新時(shí)where語(yǔ)句中增加版本的判斷</font>艳吠,算是一種CAS(Compare And Swep)操作,商品庫(kù)存場(chǎng)景中number起到了版本控制(相當(dāng)于version)的作用( AND number=#{number})昭娩。

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

<font color='red'> 什么場(chǎng)景需要使用鎖融痛,什么場(chǎng)景不需要使用鎖神僵?</font>

從中我們也可以知道只要更新數(shù)據(jù)是依賴讀取的數(shù)據(jù)作為基礎(chǔ)條件的,就會(huì)有并發(fā)更新問(wèn)題保礼,需要樂(lè)觀鎖或者悲觀鎖取解決,特別實(shí)在計(jì)數(shù)表現(xiàn)明顯目派。

又比如在更新數(shù)據(jù)不依賴查詢的數(shù)據(jù)的就不會(huì)有問(wèn)題,例如修改用戶的名稱企蹭,多人同時(shí)修改,結(jié)果并不依賴于之前的用戶名字谅摄,這就不會(huì)有并發(fā)更新問(wèn)題系馆。

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

最后個(gè)人小小總結(jié)下:<font color='red'> 盡量少使用悲觀鎖和樂(lè)觀鎖各墨,建議直接使用redis的分布式鎖(即setnx命令)启涯,因?yàn)楸^鎖會(huì)鎖定數(shù)據(jù)庫(kù)造成數(shù)據(jù)庫(kù)的負(fù)擔(dān)贬堵,樂(lè)觀鎖也要多建一個(gè)字段不太劃算结洼。</font>

原文作者: xingguang
原文鏈接:https://www.tiance.club/post/1011394230.html

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市蒸殿,隨后出現(xiàn)的幾起案子鸣峭,更是在濱河造成了極大的恐慌,老刑警劉巖摊溶,帶你破解...
    沈念sama閱讀 211,265評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異盖腕,居然都是意外死亡浓镜,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,078評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門膛薛,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)补鼻,“玉大人雅任,你說(shuō)我怎么就攤上這事咨跌』γ矗” “怎么了锌半?”我有些...
    開封第一講書人閱讀 156,852評(píng)論 0 347
  • 文/不壞的土叔 我叫張陵刊殉,是天一觀的道長(zhǎng)殉摔。 經(jīng)常有香客問(wèn)我记焊,道長(zhǎng),這世上最難降的妖魔是什么遍膜? 我笑而不...
    開封第一講書人閱讀 56,408評(píng)論 1 283
  • 正文 為了忘掉前任捌归,我火速辦了婚禮,結(jié)果婚禮上惜索,老公的妹妹穿的比我還像新娘。我一直安慰自己巾兆,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,445評(píng)論 5 384
  • 文/花漫 我一把揭開白布蔫磨。 她就那樣靜靜地躺著圃伶,像睡著了一般堤如。 火紅的嫁衣襯著肌膚如雪窒朋。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,772評(píng)論 1 290
  • 那天榔至,我揣著相機(jī)與錄音欺劳,去河邊找鬼唧取。 笑死,一個(gè)胖子當(dāng)著我的面吹牛邢享,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播驼仪,決...
    沈念sama閱讀 38,921評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼袜漩,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了宙攻?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,688評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤递惋,失蹤者是張志新(化名)和其女友劉穎溢陪,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體形真,經(jīng)...
    沈念sama閱讀 44,130評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,467評(píng)論 2 325
  • 正文 我和宋清朗相戀三年邓馒,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了蛾坯。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,617評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡脉课,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出唱遭,到底是詐尸還是另有隱情,我是刑警寧澤胆萧,帶...
    沈念sama閱讀 34,276評(píng)論 4 329
  • 正文 年R本政府宣布俐东,位于F島的核電站,受9級(jí)特大地震影響虏辫,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜砌庄,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,882評(píng)論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望佩微。 院中可真熱鬧,春花似錦哺眯、人聲如沸扒俯。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,740評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至掌猛,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間荔茬,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,967評(píng)論 1 265
  • 我被黑心中介騙來(lái)泰國(guó)打工殖卑, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留坊萝,地道東北人孵稽。 一個(gè)月前我還...
    沈念sama閱讀 46,315評(píng)論 2 360
  • 正文 我出身青樓菩鲜,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親接校。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,486評(píng)論 2 348