锁、事务和同步
關于樂觀鎖、悲觀鎖、事務、synchronized,網上介紹的文章很多。但是,在實際使用中,我們經常要遇到需要組合使用這幾種技術的場景。而這方面的文章卻非常少,本文將著重介紹各種組合使用情況下的行為和問題。
并發下讀寫沖突的問題
在開發中我們經常會遇到需要對某個字段做自增操作,比如說你向銀行存入一筆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 {@Autowiredprivate TestRepository repository;public Test updateMoney() {Test test = repository.findOne(1L);test.setMoney(test.getMoney() + 1);repository.save(test);return test;} }這段代碼并沒有什么問題,事實上在大部分情況下也能正常工作。但是如果遇到高并發情況,就會發生總消費金額少了的情況。這是由于出現了以下的并發沖突情況:
假設當前總消費金額為100元
| T1 | 讀取總金額,100元 | ? | |
| T2 | 總金額增加100,200元 | ? | |
| T3 | ? | 讀取總金額,100元 | |
| T4 | ? | 總金額增加100,200元 | |
| T5 | 寫入新的總金額,200元 | ? | |
| T6 | ? | 寫入新的總金額,200元 | ? |
明明執行了兩次自增操作,但是金額只增加了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;} }看上去很簡單,不是么? 但是現實永遠是殘酷的。很多時候讀取和寫入并不總在一起。比如讀取用戶信息,之后我們要檢查這個用戶是否可以存取,存錢是否需要支付手續費等??偠灾?#xff0c;同步可以解決這個問題,但是很多時候是以降低代碼性能為代價。
注意:這里說的性能損失是指代碼,因為同步鎖住的是代碼。
既然同步鎖住的是代碼,那么另一個更嚴重的問題是出現了:分布式場景下,多個程序實例同時運行,同步就失效了。那怎么辦呢?
使用數據庫鎖和事務解決讀寫沖突
鎖的概念
首先需要明確一下鎖的概念,本文中涉及到兩個鎖,一個是Java中的鎖。它鎖的是代碼,其作用等同于synchronized。 第二種是鎖數據的鎖,它鎖住的是數據庫里的數據。它并不是數據庫的一種機制,而是一種處理數據方式。當你使用hibernate來實現樂觀或者悲觀鎖時,hibernate會自動創建一個鎖的執行過程的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}所以,本質上樂觀鎖和悲觀鎖依舊是一段代碼,只是它們的目的是保證數據的同步。
樂觀鎖和悲觀鎖的使用
對于分布式環境,鎖代碼是沒有用的。那么我們就必須使用樂觀或者悲觀鎖去鎖住數據庫的數據。這樣不管誰的程序來讀取數據,都能保證數據不被其他程序篡改。
使用樂觀鎖
使用樂觀鎖,需要在數據庫中指定一個字段作為版本控制字段。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;@Versionprivate 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 {@Autowiredprivate TestRepository repository;@Transactionalpublic Test updateMoney() {Test test = repository.findOne(1L);test.setMoney(test.getMoney() + 1);repository.save(test);return test;} }這樣樂觀鎖就被激活了,從讀取數據開始到事務結束的代碼都會在樂觀鎖的控制之中。特別要在注意的是,這里必須為方法加上@Transactional事務。否則樂觀鎖不會生效。
關于樂觀鎖的原理和執行過程,網上有很多資料就不再贅述了。當讀寫數據發生沖突時,樂觀鎖檢測到版本沖突,會拋出異常
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
通過捕獲異常,我們可以讓程序再次嘗試修改數據,或者直接拋出給用戶。樂觀鎖性能較好,因為它并不是真正的鎖住數據,而只是檢測沖突,一旦發生沖突就會告知用戶。如果程序并不經常遇到并發讀寫沖突,可以使用樂觀鎖提高性能。
使用悲觀鎖
如果希望經常發生讀寫沖突,又希望修改提交成功概率更高,那么可以使用悲觀鎖。悲觀鎖是真正的鎖住數據。在釋放之前,是不允許其他程序訪問的。
要使用悲觀鎖,我們需要新增一個方法
@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); }使用悲觀鎖,除非發生死鎖,一般情況不會拋出異常。使用上較簡便,但是性能沒有樂觀鎖那么好,特別是在不經常發生讀寫沖突的情況下。
?
?
總結
- 上一篇: UNIX再学习 -- 用户 ID 和组
- 下一篇: UNIX再学习 -- 信号