Spring Boot2+JPA之悲觀鎖和樂觀鎖實戰


在咱們開發的項目中,大量的請求,或者同時的操做,很容易致使系統在業務上發生併發的問題。一般講到併發,解決方案無非就是前端限制重複提交,後臺進行悲觀鎖或者樂觀鎖限制。html


悲觀鎖與併發


悲觀鎖(Pessimistic Lock),顧名思義,就是很悲觀,每次去拿數據的時候都認爲別人會修改,因此每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會block直到解鎖,能夠理解爲獨佔鎖。在java中synchronized和ReentrantLock重入鎖等鎖就是悲觀鎖,數據庫中表鎖、行鎖、讀寫鎖等也是悲觀鎖。


利用SQL的for update解決併發問題


行鎖就是操做數據的時候把這一行數據鎖住,其餘線程想要讀寫必須等待,但同一個表的其餘數據仍是能被其餘線程操做的。只要在須要查詢的sql後面加上for update,就能鎖住查詢的行,特別要注意查詢條件必需要是索引列,若是不是索引就會變成表鎖,把整個表都鎖住。

public interface ArticleRepository extends JpaRepository<Article, Long> {
    @Query(value = "select * from article a where a.id = :id for update", nativeQuery = true)
    Optional<Article> findArticleForUpdate(Long id);
}
複製代碼

利用JPA的@Lock行鎖註解解決併發問題
前端


若是說for update的作法太原始,那麼JPA有提供一個更加優雅的方法,就是@Lock註解 。
爲Repository添加JPA的鎖方法,其中LockModeType.PESSIMISTIC_WRITE參數就是行鎖。
關於LockModeType這個類型,能夠在這找到文檔 https://docs.oracle.com/javaee/7/api/javax/persistence/LockModeType.html
  • NONE: No lock.java

  • OPTIMISTIC: Optimistic lock.算法

  • OPTIMISTIC_FORCE_INCREMENT: Optimistic lock, with version update.sql

  • PESSIMISTIC_FORCE_INCREMENT: Pessimistic write lock, with version update.數據庫

  • PESSIMISTIC_READ: Pessimistic read lock.api

  • PESSIMISTIC_WRITE: Pessimistic write lock.bash

  • READ: Synonymous with OPTIMISTIC.微信

  • WRITE: Synonymous with OPTIMISTIC_FORCE_INCREMENT.併發


public interface ArticleRepository extends JpaRepository<Article, Long> {

    @Lock(value = LockModeType.PESSIMISTIC_WRITE)
    @Query("select a from Article a where a.id = :id")
    Optional<Article> findArticleWithPessimisticLock(Long id);
}複製代碼

若是是@NameQuery,則能夠

@NamedQuery(name="lockArticle",query="select a from Article a where a.id = :id",lockMode = PESSIMISTIC_READ)
public class Article複製代碼


若是用entityManager的方式,則能夠設置LocakMode:

Query query = entityManager.createQuery("from Article where articleId = :id");
  query.setParameter("id", id);
  query.setLockMode(LockModeType.PESSIMISTIC_WRITE);
  query.getResultList();複製代碼

樂觀鎖與併發

樂觀鎖(Optimistic Lock),顧名思義,就是很樂觀,每次去拿數據的時候都認爲別人不會修改,因此不會上鎖,可是在提交更新的時候會判斷一下在此期間別人有沒有去修改。因此悲觀鎖是限制其餘線程,而樂觀鎖是限制本身,雖然他的名字有鎖,可是實際上不算上鎖,一般爲version版本號機制,還有CAS算法。

利用version字段解決併發問題

版本號機制就是在數據庫中加一個字段version看成版本號。那麼獲取Article的時候就會帶一個版本號,好比version=1,而後你對這個Article一波操做,操做完以後要插入到數據庫了。校驗一下version版本號,發如今數據庫裏對應Article記錄的version=2,這和我手裏的版本不同啊,說明提交的Article不是最新的,那麼就不能update到數據庫了,進行報錯把,這樣就避免了併發時數據衝突的問題。

public interface ArticleRepository extends JpaRepository<Article, Long> {
    @Modifying
    @Query(value = "update article set content= :content, version = version + 1 where id = :id and version = :version", nativeQuery = true)
    int updateArticleWithVersion(Long id, String content, Long version);
}複製代碼
public void postComment(Long articleId, String content) {
  //get article
    Optional<Article> articleOptional = articleRepository.findById(articleId);
    //update with Optimistic Lock
    int count = articleRepository.updateArticleWithVersion(article.getId(), content, article.getVersion());
   
    if (count == 0) {
        throw new RuntimeException("更新數據失敗,請刷新重試");
    }else{
      articleRepository.save(article);
    }
}複製代碼
利用JPA的@Version版本機制解決併發問題


有沒有更優雅的方式?固然,必須有,那就是JPA自帶的@Version方式實現樂觀鎖。
  • each entity class must have only one version attribute。每一個實體類只能有一個@Version字段,不能多

  • it must be placed in the primary table for an entity mapped to several tables。對於映射到多個表的實體,必須將其放置在主表中

  • type of a version attribute must be one of the following: int, Integer, long, Long, short, Short, java.sql.Timestamp

@Version支持的類型必須是如下類型:
  • int

  • Integer

  • long

  • Long

  • short

  • Short

  • java.sql.Timestamp

首先在Article實體類的version字段上加上@Version註解
@Data
@Entity
public class Article{
    @Id
    private Long id;
   //......
    @Version
    private Integer version;
}複製代碼
Article article = entityManager.find(Article.class, id);
entityManager.lock(article , LockModeType.OPTIMISTIC);
entityManager.refresh(article , LockModeType.READ);複製代碼

何時用悲觀鎖或者樂觀鎖

悲觀鎖適合寫多讀少的場景。由於在使用的時候該線程會獨佔這個資源,就適合用悲觀鎖,不然用戶只是瀏覽文章的話,用悲觀鎖就會常常加鎖,增長了加鎖解鎖的資源消耗。
樂觀鎖適合寫少讀多的場景。因爲樂觀鎖在發生衝突的時候會回滾或者重試,若是寫的請求量很大的話,就常常發生衝突,結合事務會有常常的回滾和重試,這樣對系統資源消耗也是很是大。

因此悲觀鎖和樂觀鎖沒有絕對的好壞,必須結合具體的業務狀況來決定使用哪種方式。另外在阿里巴巴開發手冊裏也有提到:

若是每次訪問衝突機率小於 20%,推薦使用樂觀鎖,不然使用悲觀鎖。樂觀鎖的重試次數不得小於3次。

阿里巴巴建議以衝突機率20%這個數值做爲分界線來決定使用樂觀鎖和悲觀鎖,雖說這個數值不是絕對的,可是做爲阿里巴巴各個大佬總結出來的也是一個很好的參考。

BLOG地址www.liangsonghua.com

關注微信公衆號:松花皮蛋的黑板報,獲取更多精彩!

公衆號介紹:分享在京東工做的技術感悟,還有JAVA技術和業內最佳實踐,大部分都是務實的、能看懂的、可復現的

相關文章
相關標籤/搜索