zoukankan      html  css  js  c++  java
  • 解决并发问题,数据库常用的两把锁

    数据锁分为乐观锁和悲观锁

    它们使用的场景如下:

    • 乐观锁适用于写少读多的情景,因为这种乐观锁相当于JAVA的CAS,所以多条数据同时过来的时候,不用等待,可以立即进行返回。

    • 悲观锁适用于写多读少的情景,这种情况也相当于JAVA的synchronized,reentrantLock等,大量数据过来的时候,只有一条数据可以被写入,其他的数据需要等待。执行完成后下一条数据可以继续。

    A.乐观锁采用版本号的方式,即当前版本号如果对应上了就可以写入数据,如果判断当前版本号不一致,那么就不会更新成功,比如

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

    B.悲观锁实现的机制一般是在执行更新语句的时候采用for update方式,这种情况where条件呢一定要涉及到数据库对应的索引字段,这样才会是行级锁,否则会是表锁,这样执行速度会变慢,比如

    update table set  column='value'  for  update
    

    下面我就弄一个spring boot(springboot 2.1.1 + mysql + lombok + aop + jpa)工程,然后逐渐的实现乐观锁和悲观锁。

    假设有一个场景,有一个catalog商品目录表,然后还有一个browse浏览表,假如一个商品被浏览了,那么就需要记录下浏览的user是谁,并且记录访问的总数。

    1.创建数据库表的结构非常简单:  

    create table catalog  (
    
    
    
    
    
    id 
    int
    (
    11
    ) 
    unsigned
     NOT NULL AUTO_INCREMENT COMMENT 
    '主键'
    ,
    
    
    
    
    
    name varchar(
    50
    ) NOT NULL DEFAULT 
    ''
     COMMENT 
    '商品名称'
    ,
    
    
    
    
    
    browse_count 
    int
    (
    11
    ) NOT NULL DEFAULT 
    0
     COMMENT 
    '浏览数'
    ,
    
    
    
    
    
    version 
    int
    (
    11
    ) NOT NULL DEFAULT 
    0
     COMMENT 
    '乐观锁,版本号'
    ,
    
    
    
    
    
    PRIMARY KEY(id)
    
    
    
    
    
    ) ENGINE=INNODB DEFAULT CHARSET=utf8;
    
    
    
    
    
    
    
    CREATE table browse (
    
    
    
    
    
    id 
    int
    (
    11
    ) 
    unsigned
     NOT NULL AUTO_INCREMENT COMMENT 
    '主键'
    ,
    
    
    
    
    
    cata_id 
    int
    (
    11
    ) NOT NULL COMMENT 
    '商品ID'
    ,
    
    
    
    
    
    user varchar(
    50
    ) NOT NULL DEFAULT 
    ''
     COMMENT 
    ''
    ,
    
    
    
    
    
    create_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 
    '创建时间'
    ,
    
    
    
    
    
    PRIMARY KEY(id)
    
    
    
    
    
    ) ENGINE=INNODB DEFAULT CHARSET=utf8;
    View Code

    2.配置Pom.xml

    <?xml version=
    "1.0"
     encoding=
    "UTF-8"
    ?>
    
    
    
    
    
    <project
     
    xmlns
    =
    "http://maven.apache.org/POM/4.0.0"
     
    xmlns:xsi
    =
    "http://www.w3.org/2001/XMLSchema-instance"
    
    
    
    
    
        
    xsi:schemaLocation
    =
    "http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
    >
    
    
    
    
    
        
    <modelVersion>
    4.0.0
    </modelVersion>
    
    
    
    
    
        
    <parent>
    
    
    
    
    
            
    <groupId>
    org.springframework.boot
    </groupId>
    
    
    
    
    
            
    <artifactId>
    spring-boot-starter-parent
    </artifactId>
    
    
    
    
    
            
    <version>
    2.1.1.RELEASE
    </version>
    
    
    
    
    
            
    <relativePath/>
     
    <!-- lookup parent from repository -->
    
    
    
    
    
        
    </parent>
    
    
    
    
    
        
    <groupId>
    com.hqs
    </groupId>
    
    
    
    
    
        
    <artifactId>
    dblock
    </artifactId>
    
    
    
    
    
        
    <version>
    1.0-SNAPSHOT
    </version>
    
    
    
    
    
        
    <name>
    dblock
    </name>
    
    
    
    
    
        
    <description>
    Demo project for Spring Boot
    </description>
    
    
    
    
    
    
    
        
    <properties>
    
    
    
    
    
            
    <java.version>
    1.8
    </java.version>
    
    
    
    
    
        
    </properties>
    
    
    
    
    
    
    
        
    <dependencies>
    
    
    
    
    
            
    <dependency>
    
    
    
    
    
                
    <groupId>
    org.springframework.boot
    </groupId>
    
    
    
    
    
                
    <artifactId>
    spring-boot-starter-web
    </artifactId>
    
    
    
    
    
            
    </dependency>
    
    
    
    
    
    
    
            
    <dependency>
    
    
    
    
    
                
    <groupId>
    org.springframework.boot
    </groupId>
    
    
    
    
    
                
    <artifactId>
    spring-boot-devtools
    </artifactId>
    
    
    
    
    
                
    <scope>
    runtime
    </scope>
    
    
    
    
    
            
    </dependency>
    
    
    
    
    
            
    <dependency>
    
    
    
    
    
                
    <groupId>
    mysql
    </groupId>
    
    
    
    
    
                
    <artifactId>
    mysql-connector-java
    </artifactId>
    
    
    
    
    
                
    <scope>
    runtime
    </scope>
    
    
    
    
    
            
    </dependency>
    
    
    
    
    
            
    <dependency>
    
    
    
    
    
                
    <groupId>
    org.springframework.boot
    </groupId>
    
    
    
    
    
                
    <artifactId>
    spring-boot-starter-test
    </artifactId>
    
    
    
    
    
                
    <scope>
    test
    </scope>
    
    
    
    
    
            
    </dependency>
    
    
    
    
    
            
    <dependency>
    
    
    
    
    
                
    <groupId>
    org.springframework.boot
    </groupId>
    
    
    
    
    
                
    <artifactId>
    spring-boot-starter-data-jpa
    </artifactId>
    
    
    
    
    
            
    </dependency>
    
    
    
    
    
            
    <dependency>
    
    
    
    
    
                
    <groupId>
    mysql
    </groupId>
    
    
    
    
    
                
    <artifactId>
    mysql-connector-java
    </artifactId>
    
    
    
    
    
            
    </dependency>
    
    
    
    
    
            
    <dependency>
    
    
    
    
    
                
    <groupId>
    org.projectlombok
    </groupId>
    
    
    
    
    
                
    <artifactId>
    lombok
    </artifactId>
    
    
    
    
    
                
    <optional>
    true
    </optional>
    
    
    
    
    
            
    </dependency>
    
    
    
    
    
    
    
            
    <!-- aop -->
    
    
    
    
    
            
    <dependency>
    
    
    
    
    
                
    <groupId>
    org.aspectj
    </groupId>
    
    
    
    
    
                
    <artifactId>
    aspectjweaver
    </artifactId>
    
    
    
    
    
                
    <version>
    1.8.4
    </version>
    
    
    
    
    
            
    </dependency>
    
    
    
    
    
    
    
        
    </dependencies>
    
    
    
    
    
    
    
        
    <build>
    
    
    
    
    
            
    <plugins>
    
    
    
    
    
                
    <plugin>
    
    
    
    
    
                    
    <groupId>
    org.springframework.boot
    </groupId>
    
    
    
    
    
                    
    <artifactId>
    spring-boot-maven-plugin
    </artifactId>
    
    
    
    
    
                
    </plugin>
    
    
    
    
    
            
    </plugins>
    
    
    
    
    
        
    </build>
    
    
    
    
    
    
    
    </project>
    View Code

    项目的结构如下:

    介绍一下项目的结构的内容:

    • 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匹配和更新,是不是很简单。 

    public
     
    interface
     
    CatalogRepository
     
    extends
     
    CrudRepository
    <
    Catalog
    , 
    Long
    > {
    
    
    
    
    
    
    
        
    @Query
    (value = 
    "select * from Catalog a where a.id = :id for update"
    , nativeQuery = 
    true
    )
    
    
    
    
    
        
    Optional
    <
    Catalog
    > findCatalogsForUpdate(
    @Param
    (
    "id"
    ) 
    Long
     id);
    
    
    
    
    
    
    
        
    @Lock
    (value = 
    LockModeType
    .PESSIMISTIC_WRITE) 
    //代表行级锁
    
    
    
    
    
        
    @Query
    (
    "select a from Catalog a where a.id = :id"
    )
    
    
    
    
    
        
    Optional
    <
    Catalog
    > findCatalogWithPessimisticLock(
    @Param
    (
    "id"
    ) 
    Long
     id);
    
    
    
    
    
    
    
        
    @Modifying
    (clearAutomatically = 
    true
    ) 
    //修改时需要带上
    
    
    
    
    
        
    @Query
    (value = 
    "update Catalog set browse_count = :browseCount, version = version + 1 where id = :id "
     +
    
    
    
    
    
                
    "and version = :version"
    , nativeQuery = 
    true
    )
    
    
    
    
    
        
    int
     updateCatalogWithVersion(
    @Param
    (
    "id"
    ) 
    Long
     id, 
    @Param
    (
    "browseCount"
    ) 
    Long
     browseCount, 
    @Param
    (
    "version"
    ) 
    Long
     version);
    
    
    
    
    
    
    
    }
    View Code

    实现悲观锁的时候也有两种方式:

    1. 自行写原生SQL,然后写上for update语句。(方法:findCatalogsForUpdate)

    2. 使用@Lock注解,并且设置值为LockModeType.PESSIMISTIC_WRITE即可代表行级锁。

    还有我写的测试类,方便大家进行测试:

    package
     com.hqs.dblock;
    
    
    
    
    
    
    
    import
     org.junit.
    Test
    ;
    
    
    
    
    
    import
     org.junit.runner.
    RunWith
    ;
    
    
    
    
    
    import
     org.springframework.beans.factory.annotation.
    Autowired
    ;
    
    
    
    
    
    import
     org.springframework.boot.test.context.
    SpringBootTest
    ;
    
    
    
    
    
    import
     org.springframework.boot.test.web.client.
    TestRestTemplate
    ;
    
    
    
    
    
    import
     org.springframework.test.context.junit4.
    SpringRunner
    ;
    
    
    
    
    
    import
     org.springframework.util.
    LinkedMultiValueMap
    ;
    
    
    
    
    
    import
     org.springframework.util.
    MultiValueMap
    ;
    
    
    
    
    
    
    
    @RunWith
    (
    SpringRunner
    .
    class
    )
    
    
    
    
    
    @SpringBootTest
    (classes = 
    DblockApplication
    .
    class
    , webEnvironment = 
    SpringBootTest
    .
    WebEnvironment
    .RANDOM_PORT)
    
    
    
    
    
    public
     
    class
     
    DblockApplicationTests
     {
    
    
    
    
    
    
    
        
    @Autowired
    
    
    
    
    
        
    private
     
    TestRestTemplate
     testRestTemplate;
    
    
    
    
    
    
    
        
    @Test
    
    
    
    
    
        
    public
     
    void
     browseCatalogTest() {
    
    
    
    
    
            
    String
     url = 
    "http://localhost:8888/catalog"
    ;
    
    
    
    
    
            
    for
    (
    int
     i = 
    0
    ; i < 
    100
    ; i++) {
    
    
    
    
    
                
    final
     
    int
     num = i;
    
    
    
    
    
                
    new
     
    Thread
    (() -> {
    
    
    
    
    
                    
    MultiValueMap
    <
    String
    , 
    String
    > 
    params
     = 
    new
     
    LinkedMultiValueMap
    <>();
    
    
    
    
    
                    
    params
    .add(
    "catalogId"
    , 
    "1"
    );
    
    
    
    
    
                    
    params
    .add(
    "user"
    , 
    "user"
     + num);
    
    
    
    
    
                    
    String
     result = testRestTemplate.postForObject(url, 
    params
    , 
    String
    .
    class
    );
    
    
    
    
    
                    
    System
    .
    out
    .println(
    "-------------"
     + result);
    
    
    
    
    
                }
    
    
    
    
    
                ).start();
    
    
    
    
    
            }
    
    
    
    
    
        }
    
    
    
    
    
    
    
        
    @Test
    
    
    
    
    
        
    public
     
    void
     browseCatalogTestRetry() {
    
    
    
    
    
            
    String
     url = 
    "http://localhost:8888/catalogRetry"
    ;
    
    
    
    
    
            
    for
    (
    int
     i = 
    0
    ; i < 
    100
    ; i++) {
    
    
    
    
    
                
    final
     
    int
     num = i;
    
    
    
    
    
                
    new
     
    Thread
    (() -> {
    
    
    
    
    
                    
    MultiValueMap
    <
    String
    , 
    String
    > 
    params
     = 
    new
     
    LinkedMultiValueMap
    <>();
    
    
    
    
    
                    
    params
    .add(
    "catalogId"
    , 
    "1"
    );
    
    
    
    
    
                    
    params
    .add(
    "user"
    , 
    "user"
     + num);
    
    
    
    
    
                    
    String
     result = testRestTemplate.postForObject(url, 
    params
    , 
    String
    .
    class
    );
    
    
    
    
    
                    
    System
    .
    out
    .println(
    "-------------"
     + result);
    
    
    
    
    
                }
    
    
    
    
    
                ).start();
    
    
    
    
    
            }
    
    
    
    
    
        }
    
    
    
    
    
    }
    View Code

    调用100次,即一个商品可以浏览一百次,采用悲观锁,catalog表的数据都是100,并且browse表也是100条记录。

    采用乐观锁的时候,因为版本号的匹配关系,那么会有一些记录丢失,但是这两个表的数据是可以对应上的。

    乐观锁失败后会抛出ObjectOptimisticLockingFailureException,那么我们就针对这块考虑一下重试,下面我就自定义了一个注解,用于做切面。

    package
     com.hqs.dblock.annotation;
    
    
    
    
    
    
    
    import
     java.lang.annotation.
    ElementType
    ;
    
    
    
    
    
    import
     java.lang.annotation.
    Retention
    ;
    
    
    
    
    
    import
     java.lang.annotation.
    RetentionPolicy
    ;
    
    
    
    
    
    import
     java.lang.annotation.
    Target
    ;
    
    
    
    
    
    
    
    @Target
    (
    ElementType
    .METHOD)
    
    
    
    
    
    @Retention
    (
    RetentionPolicy
    .RUNTIME)
    
    
    
    
    
    public
     
    @interface
     
    RetryOnFailure
     {
    
    
    
    
    
    }
    View Code

    针对注解进行切面,见如下代码。

    我设置了最大重试次数5,然后超过5次后就不再重试

    package
     com.hqs.dblock.aspect;
    
    
    
    
    
    
    
    import
     lombok.
    extern
    .slf4j.
    Slf4j
    ;
    
    
    
    
    
    import
     org.aspectj.lang.
    ProceedingJoinPoint
    ;
    
    
    
    
    
    import
     org.aspectj.lang.annotation.
    Around
    ;
    
    
    
    
    
    import
     org.aspectj.lang.annotation.
    Aspect
    ;
    
    
    
    
    
    import
     org.aspectj.lang.annotation.
    Pointcut
    ;
    
    
    
    
    
    import
     org.hibernate.
    StaleObjectStateException
    ;
    
    
    
    
    
    import
     org.springframework.orm.
    ObjectOptimisticLockingFailureException
    ;
    
    
    
    
    
    import
     org.springframework.stereotype.
    Component
    ;
    
    
    
    
    
    
    
    @Slf4j
    
    
    
    
    
    @Aspect
    
    
    
    
    
    @Component
    
    
    
    
    
    public
     
    class
     
    RetryAspect
     {
    
    
    
    
    
        
    public
     
    static
     
    final
     
    int
     MAX_RETRY_TIMES = 
    5
    ;
    //max retry times
    
    
    
    
    
    
    
        
    @Pointcut
    (
    "@annotation(com.hqs.dblock.annotation.RetryOnFailure)"
    ) 
    //self-defined pointcount for RetryOnFailure
    
    
    
    
    
        
    public
     
    void
     retryOnFailure(){}
    
    
    
    
    
    
    
        
    @Around
    (
    "retryOnFailure()"
    ) 
    //around can be execute before and after the point
    
    
    
    
    
        
    public
     
    Object
     doConcurrentOperation(
    ProceedingJoinPoint
     pjp) 
    throws
     
    Throwable
     {
    
    
    
    
    
            
    int
     attempts = 
    0
    ;
    
    
    
    
    
    
    
            
    do
     {
    
    
    
    
    
                attempts++;
    
    
    
    
    
                
    try
     {
    
    
    
    
    
                    pjp.proceed();
    
    
    
    
    
                } 
    catch
     (
    Exception
     e) {
    
    
    
    
    
                    
    if
    (e 
    instanceof
     
    ObjectOptimisticLockingFailureException
     ||
    
    
    
    
    
                            e 
    instanceof
     
    StaleObjectStateException
    ) {
    
    
    
    
    
                        log.info(
    "retrying....times:{}"
    , attempts);
    
    
    
    
    
                        
    if
    (attempts > MAX_RETRY_TIMES) {
    
    
    
    
    
                            log.info(
    "retry excceed the max times.."
    );
    
    
    
    
    
                            
    throw
     e;
    
    
    
    
    
                        }
    
    
    
    
    
                    }
    
    
    
    
    
    
    
                }
    
    
    
    
    
            } 
    while
     (attempts < MAX_RETRY_TIMES);
    
    
    
    
    
            
    return
      
    null
    ;
    
    
    
    
    
        }
    
    
    
    
    
    }
    View Code

     

  • 相关阅读:
    ASP.NET线程相关配置
    ECshop 数据库表结构
    PHPnow 升级后 PHP不支持GD、MySQL
    C# 创建iis站点以及IIS站点属性,iis不能启动站点
    CSPS_107
    CSPS_106
    CSPS_105
    CSPS_104
    CSPS_103
    CSPS_102
  • 原文地址:https://www.cnblogs.com/ningshare/p/10687694.html
Copyright © 2011-2022 走看看