歡迎光臨
每天分享高質量文章

聊聊資料庫樂觀鎖和悲觀鎖

作者:黃青石

連結:https://www.cnblogs.com/huangqingshi/p/10165409.html

 

在寫入資料庫的時候需要有鎖,比如同時寫入資料庫的時候會出現丟資料,那麼就需要鎖機制。

資料鎖分為樂觀鎖和悲觀鎖

它們使用的場景如下:

  • 樂觀鎖適用於寫少讀多的情景,因為這種樂觀鎖相當於JAVA的CAS,所以多條資料同時過來的時候,不用等待,可以立即進行傳回。

  • 悲觀鎖適用於寫多讀少的情景,這種情況也相當於JAVA的synchronized,reentrantLock等,大量資料過來的時候,只有一條資料可以被寫入,其他的資料需要等待。執行完成後下一條資料可以繼續。

他們實現的方式上有所不同。

樂觀鎖採用版本號的方式,即當前版本號如果對應上了就可以寫入資料,如果判斷當前版本號不一致,那麼就不會更新成功,

比如

  1. update table set column = value
  2. where version=${version} and otherKey = ${otherKey}

悲觀鎖實現的機制一般是在執行更新陳述句的時候採用for update方式,

比如

  1. update table set column='value' for update

這種情況where條件呢一定要涉及到資料庫對應的索引欄位,這樣才會是行級鎖,否則會是表鎖,這樣執行速度會變慢。

下麵我就弄一個spring boot(springboot 2.1.1 + mysql + lombok + aop + jpa)工程,然後逐漸的實現樂觀鎖和悲觀鎖。

假設有一個場景,有一個catalog商品目錄表,然後還有一個browse瀏覽表,假如一個商品被瀏覽了,那麼就需要記錄下瀏覽的user是誰,並且記錄訪問的總數。

表的結構非常簡單:

  1. create table catalog  (
  2. id int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主鍵',
  3. name varchar(50) NOT NULL DEFAULT '' COMMENT '商品名稱',
  4. browse_count int(11) NOT NULL DEFAULT 0 COMMENT '瀏覽數',
  5. version int(11) NOT NULL DEFAULT 0 COMMENT '樂觀鎖,版本號',
  6. PRIMARY KEY(id)
  7. ) ENGINE=INNODB DEFAULT CHARSET=utf8;
  8.  
  9. CREATE table browse (
  10. id int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主鍵',
  11. cata_id int(11) NOT NULL COMMENT '商品ID',
  12. user varchar(50) NOT NULL DEFAULT '' COMMENT '',
  13. create_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '建立時間',
  14. PRIMARY KEY(id)
  15. ) ENGINE=INNODB DEFAULT CHARSET=utf8;

POM.XML的依賴如下:

  1. "1.0" encoding="UTF-8"?>
  2. xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  3.    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  4.    4.0.0
  •    
  •        org.springframework.boot
  •        spring-boot-starter-parent
  •        2.1.1.RELEASE
  •        
  •    
  •    com.hqs
  •    dblock
  •    1.0-SNAPSHOT
  •    dblock
  •    Demo project for Spring Boot
  •  
  •    
  •        1.8
  •    
  •  
  •    
  •        
  •            org.springframework.boot
  •            spring-boot-starter-web
  •        
  •  
  •        
  •            org.springframework.boot
  •            spring-boot-devtools
  •            runtime
  •        
  •        
  •            mysql
  •            mysql-connector-java
  •            runtime
  •        
  •        
  •            org.springframework.boot
  •            spring-boot-starter-test
  •            test
  •        
  •        
  •            org.springframework.boot
  •            spring-boot-starter-data-jpa
  •        
  •        
  •            mysql
  •            mysql-connector-java
  •        
  •        
  •            org.projectlombok
  •            lombok
  •            true
  •        
  •  
  •        
  •        
  •            org.aspectj
  •            aspectjweaver
  •            1.8.4
  •        
  •  
  •    
  •  
  •    
  •        
  •            
  •                org.springframework.boot
  •                spring-boot-maven-plugin
  •            
  •        
  •    
  •  

專案的結構如下:

介紹一下專案的結構的內容:

  • entity包: 物體類包。

  • repository包:資料庫repository

  • service包: 提供服務的service

  • controller包: 控制器寫入用於編寫requestMapping。相關請求的入口類

  • annotation包: 自定義註解,用於重試。

  • aspect包: 用於對自定義註解進行切麵。

  • DblockApplication: springboot的啟動類。

  • DblockApplicationTests: 測試類。

咱們看一下核心程式碼的實現,參考如下,使用dataJpa非常方便,集成了CrudRepository就可以實現簡單的CRUD,非常方便,有興趣的同學可以自行研究。

實現樂觀鎖的方式有兩種:

1、更新的時候將version欄位傳過來,然後更新的時候就可以進行version判斷,如果version可以匹配上,那麼就可以更新(方法:updateCatalogWithVersion)。

2、在物體類上的version欄位上加入version,可以不用自己寫SQL陳述句就可以它就可以自行的按照version匹配和更新,是不是很簡單。

 

  1. public interface CatalogRepository extends CrudRepository<Catalog, Long> {
  2.  
  3.    @Query(value = "select * from Catalog a where a.id = :id for update", nativeQuery = true)
  4.    Optional<Catalog> findCatalogsForUpdate(@Param("id") Long id);
  5.  
  6.    @Lock(value = LockModeType.PESSIMISTIC_WRITE) //代表行級鎖
  7.    @Query("select a from Catalog a where a.id = :id")
  8.    Optional<Catalog> findCatalogWithPessimisticLock(@Param("id") Long id);
  9.  
  10.    @Modifying(clearAutomatically = true) //修改時需要帶上
  11.    @Query(value = "update Catalog set browse_count = :browseCount, version = version + 1 where id = :id " +
  12.            "and version = :version", nativeQuery = true)
  13.    int updateCatalogWithVersion(@Param("id") Long id, @Param("browseCount") Long browseCount, @Param("version") Long version);
  14.  
  15. }

實現悲觀鎖的時候也有兩種方式:

1、自行寫原生SQL,然後寫上for update陳述句。(方法:findCatalogsForUpdate)

2、使用@Lock註解,並且設定值為LockModeType.PESSIMISTIC_WRITE即可代表行級鎖。

還有我寫的測試類,方便大家進行測試:

  1. package com.hqs.dblock;
  2.  
  3. import org.junit.Test;
  4. import org.junit.runner.RunWith;
  5. import org.springframework.beans.factory.annotation.Autowired;
  6. import org.springframework.boot.test.context.SpringBootTest;
  7. import org.springframework.boot.test.web.client.TestRestTemplate;
  8. import org.springframework.test.context.junit4.SpringRunner;
  9. import org.springframework.util.LinkedMultiValueMap;
  10. import org.springframework.util.MultiValueMap;
  11.  
  12. @RunWith(SpringRunner.class)
  13. @SpringBootTest(classes = DblockApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
  14. public class DblockApplicationTests {
  15.  
  16.    @Autowired
  17.    private TestRestTemplate testRestTemplate;
  18.  
  19.    @Test
  20.    public void browseCatalogTest() {
  21.        String url = "http://localhost:8888/catalog";
  22.        for(int i = 0; i < 100; i++) {
  23.            final int num = i;
  24.            new Thread(() -> {
  25.                MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
  26.                params.add("catalogId", "1");
  27.                params.add("user", "user" + num);
  28.                String result = testRestTemplate.postForObject(url, params, String.class);
  29.                System.out.println("-------------" + result);
  30.            }
  31.            ).start();
  32.        }
  33.    }
  34.  
  35.    @Test
  36.    public void browseCatalogTestRetry() {
  37.        String url = "http://localhost:8888/catalogRetry";
  38.        for(int i = 0; i < 100; i++) {
  39.            final int num = i;
  40.            new Thread(() -> {
  41.                MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
  42.                params.add("catalogId", "1");
  43.                params.add("user", "user" + num);
  44.                String result = testRestTemplate.postForObject(url, params, String.class);
  45.                System.out.println("-------------" + result);
  46.            }
  47.            ).start();
  48.        }
  49.    }
  50. }

呼叫100次,即一個商品可以瀏覽一百次,採用悲觀鎖,catalog表的資料都是100,並且browse表也是100條記錄。採用樂觀鎖的時候,因為版本號的匹配關係,那麼會有一些記錄丟失,但是這兩個表的資料是可以對應上的。

樂觀鎖失敗後會丟擲ObjectOptimisticLockingFailureException,那麼我們就針對這塊考慮一下重試,下麵我就自定義了一個註解,用於做切麵。

  1. package com.hqs.dblock.annotation;
  2.  
  3. import java.lang.annotation.ElementType;
  4. import java.lang.annotation.Retention;
  5. import java.lang.annotation.RetentionPolicy;
  6. import java.lang.annotation.Target;
  7.  
  8. @Target(ElementType.METHOD)
  9. @Retention(RetentionPolicy.RUNTIME)
  10. public @interface RetryOnFailure {
  11. }

針對註解進行切麵,見如下程式碼。我設定了最大重試次數5,然後超過5次後就不再重試。

  1. package com.hqs.dblock.aspect;
  2.  
  3. import lombok.extern.slf4j.Slf4j;
  4. import org.aspectj.lang.ProceedingJoinPoint;
  5. import org.aspectj.lang.annotation.Around;
  6. import org.aspectj.lang.annotation.Aspect;
  7. import org.aspectj.lang.annotation.Pointcut;
  8. import org.hibernate.StaleObjectStateException;
  9. import org.springframework.orm.ObjectOptimisticLockingFailureException;
  10. import org.springframework.stereotype.Component;
  11.  
  12. @Slf4j
  13. @Aspect
  14. @Component
  15. public class RetryAspect {
  16.    public static final int MAX_RETRY_TIMES = 5;//max retry times
  17.  
  18.    @Pointcut("@annotation(com.hqs.dblock.annotation.RetryOnFailure)") //self-defined pointcount for RetryOnFailure
  19.    public void retryOnFailure(){}
  20.  
  21.    @Around("retryOnFailure()") //around can be execute before and after the point
  22.    public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
  23.        int attempts = 0;
  24.  
  25.        do {
  26.            attempts++;
  27.            try {
  28.                pjp.proceed();
  29.            } catch (Exception e) {
  30.                if(e instanceof ObjectOptimisticLockingFailureException ||
  31.                        e instanceof StaleObjectStateException) {
  32.                    log.info("retrying....times:{}", attempts);
  33.                    if(attempts > MAX_RETRY_TIMES) {
  34.                        log.info("retry excceed the max times..");
  35.                        throw e;
  36.                    }
  37.                }
  38.  
  39.            }
  40.        } while (attempts < MAX_RETRY_TIMES);
  41.        return  null;
  42.    }
  43. }

大致思路是這樣了,示例中的程式碼:

https://github.com/stonehqs/dblock


●編號513,輸入編號直達本文

●輸入m獲取文章

推薦↓↓↓

 

Web開發

更多推薦25個技術類公眾微信

涵蓋:程式人生、演演算法與資料結構、駭客技術與網路安全、大資料技術、前端開發、Java、Python、Web開發、安卓開發、iOS開發、C/C++、.NET、Linux、資料庫、運維等。

    閱讀原文

    贊(0)

    分享創造快樂