關于樂觀鎖、悲觀鎖缘揪、事務耍群、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)境中的使用。