鎖抖僵、事務和同步看這篇就夠了(1)

關于樂觀鎖、悲觀鎖缘揪、事務耍群、synchronized,網上介紹的文章很多找筝。但是蹈垢,在實際使用中,我們經常要遇到需要組合使用這幾種技術的場景呻征。而這方面的文章卻非常少耘婚,本文將著重介紹各種組合使用情況下的行為和問題。

并發(fā)下讀寫沖突的問題

在開發(fā)中我們經常會遇到需要對某個字段做自增操作陆赋,比如說你向銀行存入一筆100元的沐祷,那么你的總金額就要增加100元。那么程序中就會使用如下代碼

@Entity
@Table(name="test")
public class Test extends BaseModel{
    @Id
    @GeneratedValue(strategy= GenerationType.AUTO)
    @Column(name = "id",unique = true, nullable = false)
    private Long id;

    private Integer count;

    public Test() {
    }

    public Integer getCount() {
        return count;
    }

    public void setCount(Integer count) {
        this.count = count;
    }
}
@Transactionalpublic interface TestRepository extends BaseRepository<Test, Long> { 
}   
@RestController
@RequestMapping(value = "/test", produces = "application/json")
public class TestController {
    @Autowired
    private TestRepository repository;

    public Test updateMoney() {
            Test test = repository.findOne(1L);
            test.setMoney(test.getMoney() + 1);
            repository.save(test);
            return test;
    }
}

這段代碼并沒有什么問題攒岛,事實上在大部分情況下也能正常工作赖临。但是如果遇到高并發(fā)情況,就會發(fā)生總消費金額少了的情況灾锯。這是由于出現(xiàn)了以下的并發(fā)沖突情況:
假設當前總消費金額為100元

時間 線程1 線程2
T1 讀取總金額兢榨,100元
T2 總金額增加100,200元
T3 讀取總金額顺饮,100元
T4 總金額增加100吵聪,200元
T5 寫入新的總金額,200元
T6 寫入新的總金額兼雄,200元

明明執(zhí)行了兩次自增操作吟逝,但是金額只增加了100元。線程1的自增操作丟失了赦肋。

使用同步解決讀寫沖突

這個問題有很多解決方法块攒,最簡單的解決方案是在方法上增加一個同步:

    public synchronized Test updateMoney() {
            Test test = repository.findOne(1L);
            test.setMoney(test.getMoney() + 1);
            repository.save(test);
            return test;
    }

這樣做的缺點也很明顯:在實際代碼中,一個方法可能要進行很多操作佃乘,直接對方法進行同步對性能的影響會比較大囱井。我們可以將這個操作單獨拆分成一個獨立的方法,或者單獨對這一段代碼加同步:

public Test updateScore() {
  synchronized(this){
            Test test = repository.findOne(1L);
            test.setMoney(test.getMoney() + 1);
            repository.save(test);
            return test;
  }
}

看上去很簡單趣避,不是么庞呕? 但是現(xiàn)實永遠是殘酷的。很多時候讀取和寫入并不總在一起鹅巍。比如讀取用戶信息千扶,之后我們要檢查這個用戶是否可以存取料祠,存錢是否需要支付手續(xù)費等∨煨撸總而言之髓绽,同步可以解決這個問題,但是很多時候是以降低代碼性能為代價妆绞。

注意:這里說的性能損失是指代碼顺呕,因為同步鎖住的是代碼。

既然同步鎖住的是代碼括饶,那么另一個更嚴重的問題是出現(xiàn)了:分布式場景下株茶,多個程序實例同時運行,同步就失效了图焰。那怎么辦呢启盛?

使用數據庫鎖和事務解決讀寫沖突

鎖的概念

首先需要明確一下鎖的概念,本文中涉及到兩個鎖技羔,一個是Java中的鎖僵闯。它鎖的是代碼,其作用等同于synchronized藤滥。 第二種是鎖數據的鎖鳖粟,它鎖住的是數據庫里的數據。它并不是數據庫的一種機制拙绊,而是一種處理數據方式向图。當你使用hibernate來實現(xiàn)樂觀或者悲觀鎖時,hibernate會自動創(chuàng)建一個鎖的執(zhí)行過程的SQL語句(類似于存儲過程)

- SELECT iD, val1, val2 FROM theTable WHERE iD = @theId; 
- {code that calculates new values} 
- UPDATE theTable SET val1 = @newVal1, val2 = @newVal2 WHERE iD = @theId AND val1 = @oldVal1 AND val2 = @oldVal2; 
- {if AffectedRows == 1 } 
- {go on with your other code} 
- {else} 
- {decide what to do since it has gone bad... in your code} 
- {endif}

所以标沪,本質上樂觀鎖和悲觀鎖依舊是一段代碼榄攀,只是它們的目的是保證數據的同步。

樂觀鎖和悲觀鎖的使用

對于分布式環(huán)境金句,鎖代碼是沒有用的航攒。那么我們就必須使用樂觀或者悲觀鎖去鎖住數據庫的數據。這樣不管誰的程序來讀取數據趴梢,都能保證數據不被其他程序篡改。

使用樂觀鎖
使用樂觀鎖币他,需要在數據庫中指定一個字段作為版本控制字段坞靶。hibernate中提供了@Version注解用于指定某個或某幾個字段用于版本控制。

我們在數據庫中增加一個version字段

@Entity
@Table(name="test")
public class Test extends BaseModel{
    @Id
    @GeneratedValue(strategy= GenerationType.AUTO)
    @Column(name = "id",unique = true, nullable = false)
    private Long id;

    private Integer money;

    @Version
    private Integer version;

    public Test() {
    }

    public Integer getMoney() {
        return money;
    }

    public void setMoney(Integer money) {
        this.money = money;
    }

    public Integer getVersion() {
        return version;
    }

    public void setVersion(Integer version) {
        this.version = version;
    }
}

并且在controller中增加@Transactional

@RestController
@RequestMapping(value = "/test", produces = "application/json")
public class TestController {
    @Autowired
    private TestRepository repository;

    @Transactional
    public Test updateMoney() {
            Test test = repository.findOne(1L);
            test.setMoney(test.getMoney() + 1);
            repository.save(test);
            return test;
    }
}

這樣樂觀鎖就被激活了蝴悉,從讀取數據開始到事務結束的代碼都會在樂觀鎖的控制之中彰阴。特別要在注意的是,這里必須為方法加上@Transactional事務拍冠。否則樂觀鎖不會生效尿这。

關于樂觀鎖的原理和執(zhí)行過程簇抵,網上有很多資料就不再贅述了。當讀寫數據發(fā)生沖突時射众,樂觀鎖檢測到版本沖突碟摆,會拋出異常

2016-12-29 14:29:06.806 ERROR 56302 --- [io-8089-exec-11] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.orm.ObjectOptimisticLockingFailureException: Object of class [com.baojinsuo.springboot.test.Test] with identifier [1]: optimistic locking failed; nested exception is org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [com.baojinsuo.springboot.test.Test#1]] with root cause

通過捕獲異常,我們可以讓程序再次嘗試修改數據叨橱,或者直接拋出給用戶典蜕。樂觀鎖性能較好,因為它并不是真正的鎖住數據罗洗,而只是檢測沖突愉舔,一旦發(fā)生沖突就會告知用戶。如果程序并不經常遇到并發(fā)讀寫沖突伙菜,可以使用樂觀鎖提高性能轩缤。

使用悲觀鎖
如果希望經常發(fā)生讀寫沖突,又希望修改提交成功概率更高贩绕,那么可以使用悲觀鎖火的。悲觀鎖是真正的鎖住數據。在釋放之前丧叽,是不允許其他程序訪問的卫玖。

要使用悲觀鎖,我們需要新增一個方法

@Transactional
public interface TestRepository extends BaseRepository<Test, Long> {
    @Lock(LockModeType.PESSIMISTIC_WRITE)
    @Query("select cb from Test as cb where cb.id = :id")
    Test findOneWithLock(@Param("id") Long id);
}

使用悲觀鎖踊淳,除非發(fā)生死鎖假瞬,一般情況不會拋出異常。使用上較簡便迂尝,但是性能沒有樂觀鎖那么好脱茉,特別是在不經常發(fā)生讀寫沖突的情況下。

本篇介紹了鎖垄开、事務和同步的基本用法琴许,后面會著重介紹在更復雜的代碼環(huán)境中的使用。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末溉躲,一起剝皮案震驚了整個濱河市榜田,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌锻梳,老刑警劉巖箭券,帶你破解...
    沈念sama閱讀 222,807評論 6 518
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異疑枯,居然都是意外死亡辩块,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,284評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來废亭,“玉大人国章,你說我怎么就攤上這事《勾澹” “怎么了液兽?”我有些...
    開封第一講書人閱讀 169,589評論 0 363
  • 文/不壞的土叔 我叫張陵,是天一觀的道長你画。 經常有香客問我抵碟,道長,這世上最難降的妖魔是什么坏匪? 我笑而不...
    開封第一講書人閱讀 60,188評論 1 300
  • 正文 為了忘掉前任拟逮,我火速辦了婚禮,結果婚禮上适滓,老公的妹妹穿的比我還像新娘敦迄。我一直安慰自己,他們只是感情好凭迹,可當我...
    茶點故事閱讀 69,185評論 6 398
  • 文/花漫 我一把揭開白布罚屋。 她就那樣靜靜地躺著,像睡著了一般嗅绸。 火紅的嫁衣襯著肌膚如雪脾猛。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,785評論 1 314
  • 那天鱼鸠,我揣著相機與錄音猛拴,去河邊找鬼。 笑死蚀狰,一個胖子當著我的面吹牛愉昆,可吹牛的內容都是我干的。 我是一名探鬼主播麻蹋,決...
    沈念sama閱讀 41,220評論 3 423
  • 文/蒼蘭香墨 我猛地睜開眼跛溉,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了扮授?” 一聲冷哼從身側響起芳室,我...
    開封第一講書人閱讀 40,167評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎刹勃,沒想到半個月后渤愁,有當地人在樹林里發(fā)現(xiàn)了一具尸體,經...
    沈念sama閱讀 46,698評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡深夯,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,767評論 3 343
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片咕晋。...
    茶點故事閱讀 40,912評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡雹拄,死狀恐怖,靈堂內的尸體忽然破棺而出掌呜,到底是詐尸還是另有隱情滓玖,我是刑警寧澤,帶...
    沈念sama閱讀 36,572評論 5 351
  • 正文 年R本政府宣布质蕉,位于F島的核電站势篡,受9級特大地震影響,放射性物質發(fā)生泄漏模暗。R本人自食惡果不足惜禁悠,卻給世界環(huán)境...
    茶點故事閱讀 42,254評論 3 336
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望兑宇。 院中可真熱鬧碍侦,春花似錦、人聲如沸隶糕。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,746評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽枚驻。三九已至濒旦,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間再登,已是汗流浹背尔邓。 一陣腳步聲響...
    開封第一講書人閱讀 33,859評論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留霎冯,地道東北人铃拇。 一個月前我還...
    沈念sama閱讀 49,359評論 3 379
  • 正文 我出身青樓,卻偏偏與公主長得像沈撞,于是被迫代替她去往敵國和親慷荔。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,922評論 2 361

推薦閱讀更多精彩內容