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

    640?wx_fmt=jpeg

    作者:黄青石 

    来源: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. CREATE table browse (

    9. id int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',

    10. cata_id int(11) NOT NULL COMMENT '商品ID',

    11. user varchar(50) NOT NULL DEFAULT '' COMMENT '',

    12. create_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间',

    13. PRIMARY KEY(id)

    14. ) ENGINE=INNODB DEFAULT CHARSET=utf8;

    POM.XML的依赖如下:

    1. <?xml version="1.0" encoding="UTF-8"?>

    2. <project 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.    <modelVersion>4.0.0</modelVersion>

    5.    <parent>

    6.        <groupId>org.springframework.boot</groupId>

    7.        <artifactId>spring-boot-starter-parent</artifactId>

    8.        <version>2.1.1.RELEASE</version>

    9.        <relativePath/> <!-- lookup parent from repository -->

    10.    </parent>

    11.    <groupId>com.hqs</groupId>

    12.    <artifactId>dblock</artifactId>

    13.    <version>1.0-SNAPSHOT</version>

    14.    <name>dblock</name>

    15.    <description>Demo project for Spring Boot</description>


    16.    <properties>

    17.        <java.version>1.8</java.version>

    18.    </properties>


    19.    <dependencies>

    20.        <dependency>

    21.            <groupId>org.springframework.boot</groupId>

    22.            <artifactId>spring-boot-starter-web</artifactId>

    23.        </dependency>


    24.        <dependency>

    25.            <groupId>org.springframework.boot</groupId>

    26.            <artifactId>spring-boot-devtools</artifactId>

    27.            <scope>runtime</scope>

    28.        </dependency>

    29.        <dependency>

    30.            <groupId>mysql</groupId>

    31.            <artifactId>mysql-connector-java</artifactId>

    32.            <scope>runtime</scope>

    33.        </dependency>

    34.        <dependency>

    35.            <groupId>org.springframework.boot</groupId>

    36.            <artifactId>spring-boot-starter-test</artifactId>

    37.            <scope>test</scope>

    38.        </dependency>

    39.        <dependency>

    40.            <groupId>org.springframework.boot</groupId>

    41.            <artifactId>spring-boot-starter-data-jpa</artifactId>

    42.        </dependency>

    43.        <dependency>

    44.            <groupId>mysql</groupId>

    45.            <artifactId>mysql-connector-java</artifactId>

    46.        </dependency>

    47.        <dependency>

    48.            <groupId>org.projectlombok</groupId>

    49.            <artifactId>lombok</artifactId>

    50.            <optional>true</optional>

    51.        </dependency>


    52.        <!-- aop -->

    53.        <dependency>

    54.            <groupId>org.aspectj</groupId>

    55.            <artifactId>aspectjweaver</artifactId>

    56.            <version>1.8.4</version>

    57.        </dependency>


    58.    </dependencies>


    59.    <build>

    60.        <plugins>

    61.            <plugin>

    62.                <groupId>org.springframework.boot</groupId>

    63.                <artifactId>spring-boot-maven-plugin</artifactId>

    64.            </plugin>

    65.        </plugins>

    66.    </build>


    67. </project>

    项目的结构如下:

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

    • 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.    @Query(value = "select * from Catalog a where a.id = :id for update", nativeQuery = true)

    3.    Optional<Catalog> findCatalogsForUpdate(@Param("id") Long id);


    4.    @Lock(value = LockModeType.PESSIMISTIC_WRITE) //代表行级锁

    5.    @Query("select a from Catalog a where a.id = :id")

    6.    Optional<Catalog> findCatalogWithPessimisticLock(@Param("id") Long id);


    7.    @Modifying(clearAutomatically = true) //修改时需要带上

    8.    @Query(value = "update Catalog set browse_count = :browseCount, version = version + 1 where id = :id " +

    9.            "and version = :version", nativeQuery = true)

    10.    int updateCatalogWithVersion(@Param("id") Long id, @Param("browseCount") Long browseCount, @Param("version") Long version);


    11. }

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

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

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

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

    1. package com.hqs.dblock;


    2. import org.junit.Test;

    3. import org.junit.runner.RunWith;

    4. import org.springframework.beans.factory.annotation.Autowired;

    5. import org.springframework.boot.test.context.SpringBootTest;

    6. import org.springframework.boot.test.web.client.TestRestTemplate;

    7. import org.springframework.test.context.junit4.SpringRunner;

    8. import org.springframework.util.LinkedMultiValueMap;

    9. import org.springframework.util.MultiValueMap;


    10. @RunWith(SpringRunner.class)

    11. @SpringBootTest(classes = DblockApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)

    12. public class DblockApplicationTests {


    13.    @Autowired

    14.    private TestRestTemplate testRestTemplate;


    15.    @Test

    16.    public void browseCatalogTest() {

    17.        String url = "http://localhost:8888/catalog";

    18.        for(int i = 0; i < 100; i++) {

    19.            final int num = i;

    20.            new Thread(() -> {

    21.                MultiValueMap<String, String> params = new LinkedMultiValueMap<>();

    22.                params.add("catalogId", "1");

    23.                params.add("user", "user" + num);

    24.                String result = testRestTemplate.postForObject(url, params, String.class);

    25.                System.out.println("-------------" + result);

    26.            }

    27.            ).start();

    28.        }

    29.    }


    30.    @Test

    31.    public void browseCatalogTestRetry() {

    32.        String url = "http://localhost:8888/catalogRetry";

    33.        for(int i = 0; i < 100; i++) {

    34.            final int num = i;

    35.            new Thread(() -> {

    36.                MultiValueMap<String, String> params = new LinkedMultiValueMap<>();

    37.                params.add("catalogId", "1");

    38.                params.add("user", "user" + num);

    39.                String result = testRestTemplate.postForObject(url, params, String.class);

    40.                System.out.println("-------------" + result);

    41.            }

    42.            ).start();

    43.        }

    44.    }

    45. }

    调用100次,即一个商品可以浏览一百次,采用悲观锁,catalog表的数据都是100,并且browse表也是100条记录。采用乐观锁的时候,因为版本号的匹配关系,那么会有一些记录丢失,但是这两个表的数据是可以对应上的。

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

    1. package com.hqs.dblock.annotation;


    2. import java.lang.annotation.ElementType;

    3. import java.lang.annotation.Retention;

    4. import java.lang.annotation.RetentionPolicy;

    5. import java.lang.annotation.Target;


    6. @Target(ElementType.METHOD)

    7. @Retention(RetentionPolicy.RUNTIME)

    8. public @interface RetryOnFailure {

    9. }

    针对注解进行切面,见如下代码。我设置了最大重试次数5,然后超过5次后就不再重试。  

    1. package com.hqs.dblock.aspect;


    2. import lombok.extern.slf4j.Slf4j;

    3. import org.aspectj.lang.ProceedingJoinPoint;

    4. import org.aspectj.lang.annotation.Around;

    5. import org.aspectj.lang.annotation.Aspect;

    6. import org.aspectj.lang.annotation.Pointcut;

    7. import org.hibernate.StaleObjectStateException;

    8. import org.springframework.orm.ObjectOptimisticLockingFailureException;

    9. import org.springframework.stereotype.Component;


    10. @Slf4j

    11. @Aspect

    12. @Component

    13. public class RetryAspect {

    14.    public static final int MAX_RETRY_TIMES = 5;//max retry times


    15.    @Pointcut("@annotation(com.hqs.dblock.annotation.RetryOnFailure)") //self-defined pointcount for RetryOnFailure

    16.    public void retryOnFailure(){}


    17.    @Around("retryOnFailure()") //around can be execute before and after the point

    18.    public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {

    19.        int attempts = 0;


    20.        do {

    21.            attempts++;

    22.            try {

    23.                pjp.proceed();

    24.            } catch (Exception e) {

    25.                if(e instanceof ObjectOptimisticLockingFailureException ||

    26.                        e instanceof StaleObjectStateException) {

    27.                    log.info("retrying....times:{}", attempts);

    28.                    if(attempts > MAX_RETRY_TIMES) {

    29.                        log.info("retry excceed the max times..");

    30.                        throw e;

    31.                    }

    32.                }


    33.            }

    34.        } while (attempts < MAX_RETRY_TIMES);

    35.        return  null;

    36.    }

    37. }

    大致思路是这样了。

    关注Java技术栈微信公众号,在后台回复关键字:分布式,可以获取更多栈长整理的分布式技术干货。

    最近干货分享

    一个比Spring Boot快44倍的Java框架!

    为什么面试你要25K,HR只给你20K?

    Spring 系列干货,2019最新整理版!

    推荐一个技术圈子,30k的offer靠它了!

    分享一份Java架构师学习资料!

    640


    640点击「阅读原文和栈长学更多…
  • 相关阅读:
    【leetcode】1365. How Many Numbers Are Smaller Than the Current Number
    【leetcode】1363. Largest Multiple of Three
    【leetcode】1362. Closest Divisors
    【leetcode】1361. Validate Binary Tree Nodes
    【leetcode】1360. Number of Days Between Two Dates
    【leetcode】1359. Count All Valid Pickup and Delivery Options
    【leetcode】1357. Apply Discount Every n Orders
    【leetcode】1356. Sort Integers by The Number of 1 Bits
    ISE应用入门的一些问题
    DDR的型号问题
  • 原文地址:https://www.cnblogs.com/java-stack/p/11952343.html
Copyright © 2011-2022 走看看