亚洲中字慕日产2020,大陆极品少妇内射AAAAAA,无码av大香线蕉伊人久久,久久精品国产亚洲av麻豆网站

資訊專欄INFORMATION COLUMN

Spring Boot+SQL/JPA實(shí)戰(zhàn)悲觀鎖和樂觀鎖

Keven / 995人閱讀

摘要:所以悲觀鎖是限制其他線程,而樂觀鎖是限制自己,雖然他的名字有鎖,但是實(shí)際上不算上鎖,只是在最后操作的時(shí)候再判斷具體怎么操作。悲觀鎖和樂觀鎖比較悲觀鎖適合寫多讀少的場(chǎng)景。

最近在公司的業(yè)務(wù)上遇到了并發(fā)的問題,并且還是很常見的并發(fā)問題,算是低級(jí)的失誤了。由于公司業(yè)務(wù)相對(duì)比較復(fù)雜且不適合公開,在此用一個(gè)很常見的業(yè)務(wù)來還原一下場(chǎng)景,同時(shí)介紹悲觀鎖和樂觀鎖是如何解決這類并發(fā)問題的。

公司業(yè)務(wù)就是最常見的“訂單+賬戶”問題,在解決完公司問題后,轉(zhuǎn)頭一想,我的博客項(xiàng)目Fame中也有同樣的問題(雖然訪問量根本完全不需要考慮并發(fā)問題...),那我就拿這個(gè)來舉例好了。

業(yè)務(wù)還原

首先環(huán)境是:Spring Boot 2.1.0 + data-jpa + mysql + lombok

數(shù)據(jù)庫設(shè)計(jì)

對(duì)于一個(gè)有評(píng)論功能的博客系統(tǒng)來說,通常會(huì)有兩個(gè)表:1.文章表 2.評(píng)論表。其中文章表除了保存一些文章信息等,還有個(gè)字段保存評(píng)論數(shù)量。我們?cè)O(shè)計(jì)一個(gè)最精簡的表結(jié)構(gòu)來還原該業(yè)務(wù)場(chǎng)景。

article 文章表

字段 類型 備注
id INT 自增主鍵id
title VARCHAR 文章標(biāo)題
comment_count INT 文章的評(píng)論數(shù)量

comment 評(píng)論表

字段 類型 備注
id INT 自增主鍵id
article_id INT 評(píng)論的文章id
content VARCHAR 評(píng)論內(nèi)容

當(dāng)一個(gè)用戶評(píng)論的時(shí)候,1. 根據(jù)文章id獲取到文章 2. 插入一條評(píng)論記錄 3. 該文章的評(píng)論數(shù)增加并保存

代碼實(shí)現(xiàn)

首先在maven中引入對(duì)應(yīng)的依賴


    org.springframework.boot
    spring-boot-starter-parent
    2.1.0.RELEASE
     



    
        org.springframework.boot
        spring-boot-starter-web
    
    
        org.springframework.boot
        spring-boot-starter-data-jpa
    
    
        mysql
        mysql-connector-java
    
    
        org.projectlombok
        lombok
        true
    
    
        org.springframework.boot
        spring-boot-starter-test
        test
    

然后編寫對(duì)應(yīng)數(shù)據(jù)庫的實(shí)體類

@Data
@Entity
public class Article {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String title;

    private Long commentCount;
}
@Data
@Entity
public class Comment {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private Long articleId;

    private String content;
}

接著創(chuàng)建這兩個(gè)實(shí)體類對(duì)應(yīng)的Repository,由于spring-jpa-data的CrudRepository已經(jīng)幫我們實(shí)現(xiàn)了最常見的CRUD操作,所以我們的Repository只需要繼承CrudRepository接口其他啥都不用做。

public interface ArticleRepository extends CrudRepository {
}
public interface CommentRepository extends CrudRepository {
}

接著我們就簡單的實(shí)現(xiàn)一下Controller接口和Service實(shí)現(xiàn)類。

@Slf4j
@RestController
public class CommentController {

    @Autowired
    private CommentService commentService;

    @PostMapping("comment")
    public String comment(Long articleId, String content) {
        try {
            commentService.postComment(articleId, content);
        } catch (Exception e) {
            log.error("{}", e);
            return "error: " + e.getMessage();
        }
        return "success";
    }
}
@Slf4j
@Service
public class CommentService {
    @Autowired
    private ArticleRepository articleRepository;

    @Autowired
    private CommentRepository commentRepository;

    public void postComment(Long articleId, String content) {
        Optional
articleOptional = articleRepository.findById(articleId); if (!articleOptional.isPresent()) { throw new RuntimeException("沒有對(duì)應(yīng)的文章"); } Article article = articleOptional.get(); Comment comment = new Comment(); comment.setArticleId(articleId); comment.setContent(content); commentRepository.save(comment); article.setCommentCount(article.getCommentCount() + 1); articleRepository.save(article); } }
并發(fā)問題分析

從剛才的代碼實(shí)現(xiàn)里可以看出這個(gè)簡單的評(píng)論功能的流程,當(dāng)用戶發(fā)起評(píng)論的請(qǐng)求時(shí),從數(shù)據(jù)庫找出對(duì)應(yīng)的文章的實(shí)體類Article,然后根據(jù)文章信息生成對(duì)應(yīng)的評(píng)論實(shí)體類Comment,并且插入到數(shù)據(jù)庫中,接著增加該文章的評(píng)論數(shù)量,再把修改后的文章更新到數(shù)據(jù)庫中,整個(gè)流程如下流程圖。

在這個(gè)流程中有個(gè)問題,當(dāng)有多個(gè)用戶同時(shí)并發(fā)評(píng)論時(shí),他們同時(shí)進(jìn)入步驟1中拿到Article,然后插入對(duì)應(yīng)的Comment,最后在步驟3中更新評(píng)論數(shù)量保存到數(shù)據(jù)庫。只是由于他們是同時(shí)在步驟1拿到的Article,所以他們的Article.commentCount的值相同,那么在步驟3中保存的Article.commentCount+1也相同,那么原來應(yīng)該+3的評(píng)論數(shù)量,只加了1。

我們用測(cè)試用例代碼試一下

@RunWith(SpringRunner.class)
@SpringBootTest(classes = LockAndTransactionApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class CommentControllerTests {
    @Autowired
    private TestRestTemplate testRestTemplate;

    @Test
    public void concurrentComment() {
        String url = "http://localhost:9090/comment";
        for (int i = 0; i < 100; i++) {
            int finalI = i;
            new Thread(() -> {
                MultiValueMap params = new LinkedMultiValueMap<>();
                params.add("articleId", "1");
                params.add("content", "測(cè)試內(nèi)容" + finalI);
                String result = testRestTemplate.postForObject(url, params, String.class);
            }).start();
        }

    }
}

這里我們開了100個(gè)線程,同時(shí)發(fā)送評(píng)論請(qǐng)求,對(duì)應(yīng)的文章id為1。

在發(fā)送請(qǐng)求前,數(shù)據(jù)庫數(shù)據(jù)為

select * from article

select count(*) comment_count from comment

發(fā)送請(qǐng)求后,數(shù)據(jù)庫數(shù)據(jù)為

select * from article

select count(*) comment_count from comment

明顯的看到在article表里的comment_count的值不是100,這個(gè)值不一定是我圖里的14,但是必然是不大于100的,而comment表的數(shù)量肯定等于100。

這就展示了在文章開頭里提到的并發(fā)問題,這種問題其實(shí)十分的常見,只要有類似上面這樣評(píng)論功能的流程的系統(tǒng),都要小心避免出現(xiàn)這種問題。

下面就用實(shí)例展示展示如何通過悲觀鎖和樂觀鎖防止出現(xiàn)并發(fā)數(shù)據(jù)問題,同時(shí)給出SQL方案和JPA自帶方案,SQL方案可以通用“任何系統(tǒng)”,甚至不限語言,而JPA方案十分快捷,如果你恰好用的也是JPA,那就可以簡單的使用上樂觀鎖或悲觀鎖。最后也會(huì)根據(jù)業(yè)務(wù)比較一下樂觀鎖和悲觀鎖的一些區(qū)別
悲觀鎖解決并發(fā)問題

悲觀鎖顧名思義就是悲觀的認(rèn)為自己操作的數(shù)據(jù)都會(huì)被其他線程操作,所以就必須自己獨(dú)占這個(gè)數(shù)據(jù),可以理解為”獨(dú)占鎖“。在java中synchronizedReentrantLock等鎖就是悲觀鎖,數(shù)據(jù)庫中表鎖、行鎖、讀寫鎖等也是悲觀鎖。

利用SQL解決并發(fā)問題

行鎖就是操作數(shù)據(jù)的時(shí)候把這一行數(shù)據(jù)鎖住,其他線程想要讀寫必須等待,但同一個(gè)表的其他數(shù)據(jù)還是能被其他線程操作的。只要在需要查詢的sql后面加上for update,就能鎖住查詢的行,特別要注意查詢條件必須要是索引列,如果不是索引就會(huì)變成表鎖,把整個(gè)表都鎖住。

現(xiàn)在在原有的代碼的基礎(chǔ)上修改一下,先在ArticleRepository增加一個(gè)手動(dòng)寫sql查詢方法。

public interface ArticleRepository extends CrudRepository {
    @Query(value = "select * from article a where a.id = :id for update", nativeQuery = true)
    Optional
findArticleForUpdate(Long id); }

然后把CommentService中使用的查詢方法由原來的findById改為我們自定義的方法

public class CommentService {
    ...
    
    public void postComment(Long articleId, String content) {
        // Optional
articleOptional = articleRepository.findById(articleId); Optional
articleOptional = articleRepository.findArticleForUpdate(articleId); ... } }

這樣我們查出來的Article在我們沒有將其提交事務(wù)之前,其他線程是不能獲取修改的,保證了同時(shí)只有一個(gè)線程能操作對(duì)應(yīng)數(shù)據(jù)。

現(xiàn)在再用測(cè)試用例測(cè)一下,article.comment_count的值必定是100。

利用JPA自帶行鎖解決并發(fā)問題

對(duì)于剛才提到的在sql后面增加for update,JPA有提供一個(gè)更優(yōu)雅的方式,就是@Lock注解,這個(gè)注解的參數(shù)可以傳入想要的鎖級(jí)別。

現(xiàn)在在ArticleRepository中增加JPA的鎖方法,其中LockModeType.PESSIMISTIC_WRITE參數(shù)就是行鎖。

public interface ArticleRepository extends CrudRepository {
    ...
    
    @Lock(value = LockModeType.PESSIMISTIC_WRITE)
    @Query("select a from Article a where a.id = :id")
    Optional
findArticleWithPessimisticLock(Long id); }

同樣的只要在CommentService里把查詢方法改為findArticleWithPessimisticLock(),再測(cè)試用例測(cè)一下,肯定不會(huì)有并發(fā)問題。而且這時(shí)看一下控制臺(tái)打印信息,發(fā)現(xiàn)實(shí)際上查詢的sql還是加了for update,只不過是JPA幫我們加了而已。

樂觀鎖解決并發(fā)問題

樂觀鎖顧名思義就是特別樂觀,認(rèn)為自己拿到的資源不會(huì)被其他線程操作所以不上鎖,只是在插入數(shù)據(jù)庫的時(shí)候再判斷一下數(shù)據(jù)有沒有被修改。所以悲觀鎖是限制其他線程,而樂觀鎖是限制自己,雖然他的名字有鎖,但是實(shí)際上不算上鎖,只是在最后操作的時(shí)候再判斷具體怎么操作。

樂觀鎖通常為版本號(hào)機(jī)制或者CAS算法
利用SQL實(shí)現(xiàn)版本號(hào)解決并發(fā)問題

版本號(hào)機(jī)制就是在數(shù)據(jù)庫中加一個(gè)字段當(dāng)作版本號(hào),比如我們加個(gè)字段version。那么這時(shí)候拿到Article的時(shí)候就會(huì)帶一個(gè)版本號(hào),比如拿到的版本是1,然后你對(duì)這個(gè)Article一通操作,操作完之后要插入到數(shù)據(jù)庫了。發(fā)現(xiàn)哎呀,怎么數(shù)據(jù)庫里的Article版本是2,和我手里的版本不一樣啊,說明我手里的Article不是最新的了,那么就不能放到數(shù)據(jù)庫了。這樣就避免了并發(fā)時(shí)數(shù)據(jù)沖突的問題。

所以我們現(xiàn)在給article表加一個(gè)字段version

article 文章表

字段 類型 備注
version INT DEFAULT 0 版本號(hào)

然后對(duì)應(yīng)的實(shí)體類也增加version字段

@Data
@Entity
public class Article {
    ...
    
    private Long version;
}

接著在ArticleRepository增加更新的方法,注意這里是更新方法,和悲觀鎖時(shí)增加查詢方法不同。

public interface ArticleRepository extends CrudRepository {
    @Modifying
    @Query(value = "update article set comment_count = :commentCount, version = version + 1 where id = :id and version = :version", nativeQuery = true)
    int updateArticleWithVersion(Long id, Long commentCount, Long version);
}

可以看到update的where有一個(gè)判斷version的條件,并且會(huì)set version = version + 1。這就保證了只有當(dāng)數(shù)據(jù)庫里的版本號(hào)和要更新的實(shí)體類的版本號(hào)相同的時(shí)候才會(huì)更新數(shù)據(jù)。

接著在CommentService里稍微修改一下代碼。

// CommentService
public void postComment(Long articleId, String content) {
    Optional
articleOptional = articleRepository.findById(articleId); ... int count = articleRepository.updateArticleWithVersion(article.getId(), article.getCommentCount() + 1, article.getVersion()); if (count == 0) { throw new RuntimeException("服務(wù)器繁忙,更新數(shù)據(jù)失敗"); } // articleRepository.save(article); }

首先對(duì)于Article的查詢方法只需要普通的findById()方法就行不用上任何鎖。

然后更新Article的時(shí)候改用新加的updateArticleWithVersion()方法??梢钥吹竭@個(gè)方法有個(gè)返回值,這個(gè)返回值代表更新了的數(shù)據(jù)庫行數(shù),如果值為0的時(shí)候表示沒有符合條件可以更新的行。

這之后就可以由我們自己決定怎么處理了,這里是直接回滾,spring就會(huì)幫我們回滾之前的數(shù)據(jù)操作,把這次的所有操作都取消以保證數(shù)據(jù)的一致性

現(xiàn)在再用測(cè)試用例測(cè)一下

select * from article

select count(*) comment_count from comment

現(xiàn)在看到Article里的comment_count和Comment的數(shù)量都不是100了,但是這兩個(gè)的值必定是一樣的了。因?yàn)閯偛盼覀兲幚淼臅r(shí)候假如Article表的數(shù)據(jù)發(fā)生了沖突,那么就不會(huì)更新到數(shù)據(jù)庫里,這時(shí)拋出異常使其事務(wù)回滾,這樣就能保證沒有更新Article的時(shí)候Comment也不會(huì)插入,就解決了數(shù)據(jù)不統(tǒng)一的問題。

這種直接回滾的處理方式用戶體驗(yàn)比較差,通常來說如果判斷Article更新條數(shù)為0時(shí),會(huì)嘗試重新從數(shù)據(jù)庫里查詢信息并重新修改,再次嘗試更新數(shù)據(jù),如果不行就再查詢,直到能夠更新為止。當(dāng)然也不會(huì)是無線的循環(huán)這樣的操作,會(huì)設(shè)置一個(gè)上線,比如循環(huán)3次查詢修改更新都不行,這時(shí)候才會(huì)拋出異常。

利用JPA實(shí)現(xiàn)版本現(xiàn)解決并發(fā)問題

JPA對(duì)悲觀鎖有實(shí)現(xiàn)方式,樂觀鎖自然也是有的,現(xiàn)在就用JPA自帶的方法實(shí)現(xiàn)樂觀鎖。

首先在Article實(shí)體類的version字段上加上@Version注解,我們進(jìn)注解看一下源碼的注釋,可以看到有部分寫到:

The following types are supported for version properties: int, Integer, short, Short, long, Long, java.sql.Timestamp.

注釋里面說版本號(hào)的類型支持int, short, long三種基本數(shù)據(jù)類型和他們的包裝類以及Timestamp,我們現(xiàn)在用的是Long類型。

@Data
@Entity
public class Article {
    ...
    
    @Version
    private Long version;
}

接著只需要在CommentService里的評(píng)論流程修改回我們最開頭的“會(huì)觸發(fā)并發(fā)問題”的業(yè)務(wù)代碼就行了。說明JPA的這種樂觀鎖實(shí)現(xiàn)方式是非侵入式的。

// CommentService
public void postComment(Long articleId, String content) {
    Optional
articleOptional = articleRepository.findById(articleId); ... article.setCommentCount(article.getCommentCount() + 1); articleRepository.save(article); }

和前面同樣的,用測(cè)試用例測(cè)試一下能否防止并發(fā)問題的出現(xiàn)。

select * from article

select count(*) comment_count from comment

同樣的Article里的comment_count和Comment的數(shù)量也不是100,但是這兩個(gè)數(shù)值肯定是一樣的。看一下IDEA的控制臺(tái)會(huì)發(fā)現(xiàn)系統(tǒng)拋出了ObjectOptimisticLockingFailureException的異常。

這和剛才我們自己實(shí)現(xiàn)樂觀鎖類似,如果沒有成功更新數(shù)據(jù)則拋出異?;貪L保證數(shù)據(jù)的一致性。如果想要實(shí)現(xiàn)重試流程可以捕獲ObjectOptimisticLockingFailureException這個(gè)異常,通常會(huì)利用AOP+自定義注解來實(shí)現(xiàn)一個(gè)全局通用的重試機(jī)制,這里就是要根據(jù)具體的業(yè)務(wù)情況來拓展了,想要了解的可以自行搜索一下方案。

悲觀鎖和樂觀鎖比較

悲觀鎖適合寫多讀少的場(chǎng)景。因?yàn)樵谑褂玫臅r(shí)候該線程會(huì)獨(dú)占這個(gè)資源,在本文的例子來說就是某個(gè)id的文章,如果有大量的評(píng)論操作的時(shí)候,就適合用悲觀鎖,否則用戶只是瀏覽文章而沒什么評(píng)論的話,用悲觀鎖就會(huì)經(jīng)常加鎖,增加了加鎖解鎖的資源消耗。

樂觀鎖適合寫少讀多的場(chǎng)景。由于樂觀鎖在發(fā)生沖突的時(shí)候會(huì)回滾或者重試,如果寫的請(qǐng)求量很大的話,就經(jīng)常發(fā)生沖突,經(jīng)常的回滾和重試,這樣對(duì)系統(tǒng)資源消耗也是非常大。

所以悲觀鎖和樂觀鎖沒有絕對(duì)的好壞,必須結(jié)合具體的業(yè)務(wù)情況來決定使用哪一種方式。另外在阿里巴巴開發(fā)手冊(cè)里也有提到:

如果每次訪問沖突概率小于 20%,推薦使用樂觀鎖,否則使用悲觀鎖。樂觀鎖的重試次
數(shù)不得小于 3 次。

阿里巴巴建議以沖突概率20%這個(gè)數(shù)值作為分界線來決定使用樂觀鎖和悲觀鎖,雖然說這個(gè)數(shù)值不是絕對(duì)的,但是作為阿里巴巴各個(gè)大佬總結(jié)出來的也是一個(gè)很好的參考。

原文地址:Spring Boot+SQL/JPA實(shí)戰(zhàn)悲觀鎖和樂觀鎖

文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請(qǐng)注明本文地址:http://www.ezyhdfw.cn/yun/72750.html

相關(guān)文章

  • Java開發(fā) 大廠面試整理

    摘要:用戶態(tài)不能干擾內(nèi)核態(tài)所以指令就有兩種特權(quán)指令和非特權(quán)指令不同的狀態(tài)對(duì)應(yīng)不同的指令。非特權(quán)指令所有程序均可直接使用。用戶態(tài)常態(tài)目態(tài)執(zhí)行非特權(quán)指令。 這是我今年從三月份開始,主要的大廠面試經(jīng)過,有些企業(yè)面試的還沒來得及整理,可能有些沒有帶答案就發(fā)出來了,還請(qǐng)各位先思考如果是你怎么回答面試官?這篇文章會(huì)持續(xù)更新,請(qǐng)各位持續(xù)關(guān)注,希望對(duì)你有所幫助! 面試清單 平安產(chǎn)險(xiǎn) 飛豬 上汽大通 浩鯨科...

    Scorpion 評(píng)論0 收藏0
  • 美團(tuán)實(shí)習(xí)Java崗面經(jīng),已拿offer

    摘要:作者鏈接來源??途W(wǎng)今天剛剛收到的電話,開心,簡單記錄一下美團(tuán)的面經(jīng)。當(dāng)時(shí)面試官評(píng)價(jià)基礎(chǔ)不是很好,其他還行。的三次握手四次揮手。整體感覺美團(tuán)的面試比較基礎(chǔ),但是各個(gè)方面都有涉及到。 作者:icysnowgx鏈接:https://www.nowcoder.com/disc...來源:牛客網(wǎng) 今天剛剛收到hr的電話,開心,簡單記錄一下美團(tuán)的面經(jīng)。時(shí)間隔的比較久了,簡單回憶下,最后會(huì)給出我之前...

    OnlyMyRailgun 評(píng)論0 收藏0
  • 悲觀樂觀以及CAS機(jī)制

    摘要:加鎖才能保證線程安全使用之后,不加鎖,也是線程安全的。確保不出現(xiàn)線程安全問題。一般在數(shù)據(jù)庫中使用樂觀鎖都會(huì)拿版本號(hào)作為對(duì)比值,因?yàn)榘姹咎?hào)會(huì)一直增加,沒有重復(fù)的,所以不會(huì)出現(xiàn)這個(gè)問題。 悲觀鎖: 認(rèn)為每次獲取數(shù)據(jù)的時(shí)候數(shù)據(jù)一定會(huì)被人修改,所以它在獲取數(shù)據(jù)的時(shí)候會(huì)把操作的數(shù)據(jù)給鎖住,這樣一來就只有它自己能夠操作,其他人都堵塞在那里。 樂觀鎖: 認(rèn)為每次獲取數(shù)據(jù)的時(shí)候數(shù)據(jù)不會(huì)被別人修改,所以...

    levius 評(píng)論0 收藏0

發(fā)表評(píng)論

0條評(píng)論

最新活動(dòng)
閱讀需要支付1元查看
<