樂觀鎖

一涨岁、樂觀鎖理論基礎(chǔ)

所謂的樂觀鎖仍翰,其實(shí)主要就是一種思想,因?yàn)闃酚^鎖的操作過程中其實(shí)沒有沒有任何鎖的參與,樂觀鎖只是和悲觀鎖相對胃夏,嚴(yán)格的說樂觀鎖不能稱之為鎖轴或。所以要了解樂觀鎖的概念,通常與悲觀鎖對比起來看才更好理解仰禀,下面我們就通過樂觀鎖與悲觀鎖的對比來更好的理解樂觀鎖照雁。

1.1樂觀鎖與悲觀鎖的概念

  • 樂觀鎖:總是假設(shè)最好的情況,每次去拿數(shù)據(jù)的時候都認(rèn)為別人不會修改答恶,所以不會上鎖饺蚊,只在更新的時候會判斷一下在此期間別人有沒有去更新這個數(shù)據(jù)。

注意“在此期間”的含義是拿到數(shù)據(jù)到更新數(shù)據(jù)的這段時間悬嗓。因?yàn)闆]有加鎖污呼,所以別的線程可能會更改。還有一點(diǎn)那就是樂觀鎖其實(shí)是不加鎖的來保證某個變量一系列操作原子性的一種方法包竹。

  • 悲觀鎖:總是假設(shè)最壞的情況燕酷,每次去拿數(shù)據(jù)的時候都認(rèn)為別人會修改,所以每次在拿數(shù)據(jù)的時候都會上鎖周瞎,這樣別人想拿這個數(shù)據(jù)就會阻塞苗缩,直到它拿到鎖(共享資源每次只給一個線程使用,其它線程阻塞声诸,用完后再把資源轉(zhuǎn)讓給其它線程)酱讶。傳統(tǒng)的關(guān)系型數(shù)據(jù)庫里邊就用到了很多這種鎖機(jī)制,比如行鎖彼乌,表鎖等泻肯,讀鎖,寫鎖等囤攀,都是在做操作之前先上鎖软免。Java中synchronizedReentrantLock等獨(dú)占鎖就是悲觀鎖思想的實(shí)現(xiàn)。

1.2 兩種鎖的使用場景

從上面對兩種鎖的介紹焚挠,我們知道兩種鎖各有優(yōu)缺點(diǎn)膏萧,不可認(rèn)為一種好于另一種,像樂觀鎖適用于寫比較少的情況下(多讀場景)蝌衔,即沖突真的很少發(fā)生的時候榛泛,這樣可以省去了鎖的開銷,加大了系統(tǒng)的整個吞吐量噩斟。但如果是多寫的情況曹锨,一般會經(jīng)常產(chǎn)生沖突,這就會導(dǎo)致上層應(yīng)用會不斷的進(jìn)行retry剃允,這樣反倒是降低了性能沛简,所以一般多寫的場景下用悲觀鎖就比較合適齐鲤。

1.3 樂觀鎖常見的兩種實(shí)現(xiàn)方式

樂觀鎖可以通過版本號機(jī)制或者CAS算法實(shí)現(xiàn)。

1.3.1 版本號機(jī)制

版本號機(jī)制實(shí)現(xiàn)的方式常用的也有兩種:

  • 使用數(shù)據(jù)版本(Version)記錄機(jī)制實(shí)現(xiàn)椒楣,這是樂觀鎖最常用的一種實(shí)現(xiàn)方式给郊。何謂數(shù)據(jù)版本?即為數(shù)據(jù)增加一個版本標(biāo)識捧灰,一般是通過為數(shù)據(jù)庫表增加一個數(shù)字類型的 “version” 字段來實(shí)現(xiàn)淆九。當(dāng)讀取數(shù)據(jù)時,將version字段的值一同讀出毛俏,數(shù)據(jù)每更新一次炭庙,對此version值加一。
    當(dāng)我們提交更新的時候煌寇,判斷數(shù)據(jù)庫表對應(yīng)記錄 的當(dāng)前版本信息與第一次取出來的version值進(jìn)行比對焕蹄,如果數(shù)據(jù)庫表當(dāng)前版本號與第一次取出來的version值相等,則予以更新唧席,否則認(rèn)為是過期數(shù) 據(jù)擦盾。用下面的一張圖來說明:

    image.png

    如上圖所示,如果更新操作順序執(zhí)行淌哟,則數(shù)據(jù)的版本(version)依次遞增,不會產(chǎn)生沖突辽故。但是如果發(fā)生有不同的業(yè)務(wù)操作對同一版本的數(shù)據(jù)進(jìn)行修 改徒仓,那么,先提交的操作(圖中B)會把數(shù)據(jù)version更新為2誊垢,當(dāng)A在B之后提交更新時發(fā)現(xiàn)數(shù)據(jù)的version已經(jīng)被修改了掉弛,那么A的更新操作會失敗。

  • 使用時間戳(timestamp)喂走。這種實(shí)現(xiàn)方式和第一種差不多殃饿,同樣是在需要樂觀鎖控制的table中增加一個字段,名稱無所謂芋肠,字段類型使用時間戳(timestamp), 和上面的version類似乎芳,也是在更新提交的時候檢查當(dāng)前數(shù)據(jù)庫中數(shù)據(jù)的時間戳和自己更新前取到的時間戳進(jìn)行對比,如果一致則OK帖池,否則就是版本沖突奈惑。

1.3.2 CAS算法

即Compare And Swap(比較與交換),是一種有名的無鎖算法睡汹。無鎖編程肴甸,即不使用鎖的情況下實(shí)現(xiàn)多線程之間的變量同步,也就是在沒有線程被阻塞的情況下實(shí)現(xiàn)變量的同步囚巴,所以也叫非阻塞同步(Non-blocking Synchronization)原在。CAS操作包含三個操作數(shù)——內(nèi)存位置的值(V)友扰、預(yù)期原值(A)和新值(B)。執(zhí)行CAS操作的時候庶柿,將內(nèi)存位置的值與預(yù)期原值比較焕檬,如果相匹配,那么處理器會自動將該位置值更新為新值澳泵,否則实愚,處理器不做任何操作。
我們使用一個例子來解釋相信你會更加的清楚兔辅。
1.在內(nèi)存地址V當(dāng)中腊敲,存儲著值為10的變量。


image.png

2.此時線程1想要把變量的值增加1维苔。對線程1來說碰辅,舊的預(yù)期值A(chǔ)=10,要修改的新值B=11介时。


image.png

3.在線程1要提交更新之前没宾,另一個線程2搶先一步,把內(nèi)存地址V中的變量值率先更新成了11沸柔。
image.png

4.線程1開始提交更新循衰,首先進(jìn)行A和地址V的實(shí)際值比較(Compare),發(fā)現(xiàn)A不等于V的實(shí)際值褐澎,提交失敗会钝。
image.png

5.線程1重新獲取內(nèi)存地址V的當(dāng)前值,并重新計(jì)算想要修改的新值工三。此時對線程1來說迁酸,A=11,B=12俭正。這個重新嘗試的過程被稱為自旋奸鬓。


image.png

6.這一次比較幸運(yùn),沒有其他線程改變地址V的值掸读。線程1進(jìn)行Compare串远,發(fā)現(xiàn)A和地址V的實(shí)際值是相等的。
image.png

7.線程1進(jìn)行SWAP寺枉,把地址V的值替換為B抑淫,也就是12。
image.png

注:CAS算法的缺點(diǎn)
【1】循環(huán)時間長開銷很大:自旋 CAS 如果長時間不成功姥闪,會給 CPU 帶來非常大的執(zhí)行開銷始苇。
【2】只能保證一個共享變量的原子操作:只能保證一個共享變量的原子操作。當(dāng)對一個共享變量執(zhí)行操作時筐喳,我們可以使用循環(huán) CAS 的方式來保證原子操作催式,但是對多個共享變量操作時函喉,循環(huán) CAS 就無法保證操作的原子性,這個時候就可以用鎖荣月,或者有一個取巧的辦法管呵,就是把多個共享變量合并成一個共享變量來操作。比如有兩個共享變量 i=2,j=a哺窄,合并一下 ij=2a捐下,然后用CAS 來操作 ij。從 Java1.5 開始 JDK 提供了 AtomicReference 類來保證引用對象之間的原子性萌业,你可以把多個變量放在一個對象里來進(jìn)行 CAS 操作坷襟。
【3】ABA 問題:因?yàn)?CAS 需要在操作值的時候檢查下值有沒有發(fā)生變化,如果沒有發(fā)生變化則更新生年,但是如果一個值原來是A婴程,變成了B,又變成了A抱婉,那么使用 CAS 進(jìn)行檢查時會發(fā)現(xiàn)它的值沒有發(fā)生變化档叔,但是實(shí)際上卻變化了。ABA 問題的解決思路就是使用版本號蒸绩。在變量前面追加上版本號衙四,每次變量更新的時候把版本號加1,那么A-B-A 就會變成1A-2B-3A侵贵。

二届搁、樂觀鎖兩種方式的實(shí)現(xiàn)實(shí)例

2.1 利用版本號機(jī)制的解決實(shí)際問題

2.1.1 實(shí)際遇到的問題

使用 MySQL 5.7 做測試,數(shù)據(jù)庫引擎為 InnoDB窍育,數(shù)據(jù)庫隔離級別為可重復(fù)讀(REPEATABLE-READ),讀讀共享宴胧,讀寫互斥漱抓。在這個隔離級別下,在多事務(wù)并發(fā)的情況下恕齐,還是會出現(xiàn)數(shù)據(jù)更新的沖突問題乞娄。
先分析一下更新沖突的問題是如何產(chǎn)生的。
假設(shè)我們有一張商品表 goods显歧,表結(jié)構(gòu)如下:

字段 數(shù)據(jù)類型 說明
goods_id varchar(32) 商品 id
count int(11) 銷量

比如在某一時刻事務(wù) A 和事務(wù) B仪或,在同時操作表 goods_id = 213214324 的數(shù)據(jù),當(dāng)前銷量為 100士骤。

goods_id count
213214324 100

兩個事務(wù)的內(nèi)容一樣范删,都是先讀取的數(shù)據(jù),count +100 后更新拷肌。

我們這里只討論樂觀鎖的實(shí)現(xiàn)到旦,為了便于描述旨巷,假設(shè)項(xiàng)目已經(jīng)集成 Spring 框架,使用 MyBatis 做 ORM添忘,Service 類的所有方法都使用了事務(wù)采呐,事務(wù)傳播級別使用 PROPAGATION_REQUIRED ,在事務(wù)失敗會自動回滾搁骑。
Service 為 GoodsService 斧吐,更新數(shù)量的方法為 addCount()

@Service
@Transaction
pubic class GoodsService{
    
    @Autowire
    private GoodsDao dao;
    
    public void addCount(String goodsId, Integer count) {
        Goods goods = dao.selectByGoodsId(goodsId);
        if (goodsSale == null) {
            throw new Execption("數(shù)據(jù)不存在");
        }
        int count = goods.getCount() + count;
        goods.setCount(count);
        int count = dao.updateCount(goods);
        if (count == 0) {
            throw new Exception("添加數(shù)量失敗");
        }
    }
}

使用的 Dao 為GoodsDao 仲器,有兩個方法:

public interface GoodsSaleDao {
    Goods selectByGoodsId(@Param("goodsId") String goodsId);

    int updateCount(@Param("record") Goods goods);
}

mapper 文件對應(yīng)的 sql 操作為:

<!-- 查詢 -->
<select id="selectByGoodsId" resultMap="BaseResultMap">
    select
    <include refid="Base_Column_List"/>
    from goods
    where goods_id = #{goodsId}
</select>

<!-- 更新 -->
<update id="updateCount">
    update
    goods
    set count = #{record.count},
    where goods_id = #{record.goodsId}
</update>

好了煤率,假設(shè)現(xiàn)在有兩個線程同時調(diào)用了 GoodsServiceaddCount() ,操作同一行數(shù)據(jù)娄周,會有什么問題涕侈?
是否可能會出現(xiàn)兩個線程更新時出現(xiàn)了沖突!兩次 addCount(100) 煤辨,結(jié)果應(yīng)該是 300裳涛,但結(jié)果還是 200。因?yàn)樯厦鎸σ粋€變量的查詢與更新這一系列操作并不是原子性的众辨,是有可能出現(xiàn)并發(fā)問題的端三。

image.png

2.1.2 如何解決問題

該如何處理上面的問題,有一個簡單粗暴的方法鹃彻,既然這里多線程訪問會有線程安全問題郊闯,那就上鎖,方法加上 synchronized 進(jìn)行互斥蛛株。

public synchronized void addCount(String goodsId, Integer count) {
     Goods goods = dao.selectByGoodsId(goodsId);
        if (goodsSale == null) {
            throw new Execption("數(shù)據(jù)不存在");
        }
        int count = goods.getCount() + count;
        goods.setCount(count);
        int count = dao.updateCount(goods);
        if (count == 0) {
            throw new Exception("添加數(shù)量失敗");
        }
}

這個方案確實(shí)也可以解決問題团赁,但是這種簡單互斥的做法,鎖的粒度太高谨履,事務(wù)排隊(duì)執(zhí)行欢摄,并發(fā)度低,性能低笋粟。但如果是分布式應(yīng)用怀挠,還得考慮應(yīng)用分布式鎖,性能就更低了害捕。
考慮到這些更新沖突發(fā)生的概率其實(shí)并不高绿淋。這里討論另一種解決方案,在數(shù)據(jù)庫表中新增版本號字段來實(shí)現(xiàn)樂觀鎖從而保證該變量操作的原子性尝盼。
接下來我們來討論如何實(shí)現(xiàn)它吞滞。
第一步:數(shù)據(jù)庫表 goods 新增一行 data_version 來記錄數(shù)據(jù)更新的版本號。新的表結(jié)構(gòu)如下:

字段 數(shù)據(jù)類型 說明
goods_id varchar(32) 商品 id
count int(11) 銷量
data_version int(11) 版本號

第二步:GoodsDaoupdateCount() 對應(yīng)的 mapper 的 SQL 語句進(jìn)行調(diào)整东涡,數(shù)據(jù)更新的時候同時進(jìn)行 data_version = data_version + 1 冯吓,執(zhí)行這個 sql 時候已經(jīng)對數(shù)據(jù)上行鎖了倘待,所以這個 data_version 加 1 的操作為原子操作。

<!-- 樂觀鎖更新 -->
<update id="updateCount">
    update
    goods_sale
    set count = #{record.count}, data_version = data_version + 1
    where goods_sale_id = #{record.goodsSaleId}
    and data_version = #{record.dataVersion}
</update>

Dao 調(diào)整之后组贺,事務(wù) A 和事務(wù) B 的變化如下:


image.png

有了發(fā)現(xiàn)沖突快速失敗的方案凸舵,要想讓更新成功,可以在 GoodsService 中加入自旋失尖,重新開始事務(wù)業(yè)務(wù)邏輯的執(zhí)行啊奄,直到?jīng)]有發(fā)生沖突,更新成功掀潮。自旋的實(shí)現(xiàn)有兩種菇夸,一種是使用循環(huán),一種是使用遞歸仪吧。

  • 循環(huán)實(shí)現(xiàn):
public void addCount(String goodsId, Integer count) {
    while(true) {
        Goods goods = dao.selectByGoodsId(goodsId);
        if (goods == null) {
            throw new Execption("數(shù)據(jù)不存在");
        }
        int count = goods.getCount() + count;
        goods.setCount(count);
        int count = dao.updateCount(goods);
        if (count > 0) {
            return;
        }   
    }
}
  • 遞歸實(shí)現(xiàn):
public void addCount(String goodsId, Integer count) {
     Goods goods = dao.selectByGoodsId(goodsId);
        if (goods == null) {
            throw new Execption("數(shù)據(jù)不存在");
        }
        int count = goods.getCount() + count;
        goods.setCount(count);
        int count = dao.updateCount(goods);
         if (count == 0) {
        addCount(goodsId, count)
    }
}

通過樂觀鎖+自旋的方式庄新,解決數(shù)據(jù)更新的線程安全問題,而且鎖粒度比互斥鎖低薯鼠,并發(fā)性能好择诈。
使用時間戳(timestamp),這種解決方式和上面的差不多出皇,這里就不展開介紹了羞芍。

2.2 利用CAS算法解決實(shí)際問題

Java中java.util.concurrent.atomic 并發(fā)包下的所有原子類都是基于 CAS 來實(shí)現(xiàn)的。
這里涉及到j(luò)ava的源碼分析郊艘,這里就不展開了荷科。

關(guān)于CAS相關(guān)連接推薦:

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市纱注,隨后出現(xiàn)的幾起案子畏浆,更是在濱河造成了極大的恐慌,老刑警劉巖狞贱,帶你破解...
    沈念sama閱讀 217,542評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件全度,死亡現(xiàn)場離奇詭異,居然都是意外死亡斥滤,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,822評論 3 394
  • 文/潘曉璐 我一進(jìn)店門勉盅,熙熙樓的掌柜王于貴愁眉苦臉地迎上來佑颇,“玉大人,你說我怎么就攤上這事草娜√粜兀” “怎么了?”我有些...
    開封第一講書人閱讀 163,912評論 0 354
  • 文/不壞的土叔 我叫張陵宰闰,是天一觀的道長茬贵。 經(jīng)常有香客問我簿透,道長,這世上最難降的妖魔是什么解藻? 我笑而不...
    開封第一講書人閱讀 58,449評論 1 293
  • 正文 為了忘掉前任老充,我火速辦了婚禮,結(jié)果婚禮上螟左,老公的妹妹穿的比我還像新娘啡浊。我一直安慰自己,他們只是感情好胶背,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,500評論 6 392
  • 文/花漫 我一把揭開白布巷嚣。 她就那樣靜靜地躺著,像睡著了一般钳吟。 火紅的嫁衣襯著肌膚如雪廷粒。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,370評論 1 302
  • 那天红且,我揣著相機(jī)與錄音坝茎,去河邊找鬼。 笑死直焙,一個胖子當(dāng)著我的面吹牛景东,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播奔誓,決...
    沈念sama閱讀 40,193評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼斤吐,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了厨喂?” 一聲冷哼從身側(cè)響起和措,我...
    開封第一講書人閱讀 39,074評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎蜕煌,沒想到半個月后派阱,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,505評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡斜纪,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,722評論 3 335
  • 正文 我和宋清朗相戀三年贫母,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片盒刚。...
    茶點(diǎn)故事閱讀 39,841評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡腺劣,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出因块,到底是詐尸還是另有隱情橘原,我是刑警寧澤,帶...
    沈念sama閱讀 35,569評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站趾断,受9級特大地震影響拒名,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜芋酌,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,168評論 3 328
  • 文/蒙蒙 一增显、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧隔嫡,春花似錦甸怕、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,783評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至秸滴,卻和暖如春武契,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背荡含。 一陣腳步聲響...
    開封第一講書人閱讀 32,918評論 1 269
  • 我被黑心中介騙來泰國打工咒唆, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人释液。 一個月前我還...
    沈念sama閱讀 47,962評論 2 370
  • 正文 我出身青樓全释,卻偏偏與公主長得像,于是被迫代替她去往敵國和親误债。 傳聞我的和親對象是個殘疾皇子浸船,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,781評論 2 354

推薦閱讀更多精彩內(nèi)容