欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

SpringBoot整合MyBatis實現(xiàn)樂觀鎖和悲觀鎖的示例

 更新時間:2019年09月20日 10:05:40   作者:浮云Cloud  
這篇文章主要介紹了SpringBoot整合MyBatis實現(xiàn)樂觀鎖和悲觀鎖的示例,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧

本文以轉(zhuǎn)賬操作為例,實現(xiàn)并測試樂觀鎖和悲觀鎖。

全部代碼:https://github.com/imcloudfloating/Lock_Demo

GitHub Page:https://cloudli.top

死鎖問題

當(dāng) A, B 兩個賬戶同時向?qū)Ψ睫D(zhuǎn)賬時,會出現(xiàn)如下情況:

時刻 事務(wù) 1 (A 向 B 轉(zhuǎn)賬) 事務(wù) 2 (B 向 A 轉(zhuǎn)賬)
T1 Lock A Lock B
T2 Lock B (由于事務(wù) 2 已經(jīng) Lock A,等待) Lock A (由于事務(wù) 1 已經(jīng) Lock B,等待)

由于兩個事務(wù)都在等待對方釋放鎖,于是死鎖產(chǎn)生了,解決方案:按照主鍵的大小來加鎖,總是先鎖主鍵較小或較大的那行數(shù)據(jù)。

建立數(shù)據(jù)表并插入數(shù)據(jù)(MySQL)

create table account
(
  id   int auto_increment
    primary key,
  deposit decimal(10, 2) default 0.00 not null,
  version int      default 0  not null
);

INSERT INTO vault.account (id, deposit, version) VALUES (1, 1000, 0);
INSERT INTO vault.account (id, deposit, version) VALUES (2, 1000, 0);
INSERT INTO vault.account (id, deposit, version) VALUES (3, 1000, 0);
INSERT INTO vault.account (id, deposit, version) VALUES (4, 1000, 0);
INSERT INTO vault.account (id, deposit, version) VALUES (5, 1000, 0);
INSERT INTO vault.account (id, deposit, version) VALUES (6, 1000, 0);
INSERT INTO vault.account (id, deposit, version) VALUES (7, 1000, 0);
INSERT INTO vault.account (id, deposit, version) VALUES (8, 1000, 0);
INSERT INTO vault.account (id, deposit, version) VALUES (9, 1000, 0);
INSERT INTO vault.account (id, deposit, version) VALUES (10, 1000, 0);

Mapper 文件

悲觀鎖使用 select ... for update,樂觀鎖使用 version 字段。

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.cloud.demo.mapper.AccountMapper">
  <select id="selectById" resultType="com.cloud.demo.model.Account">
    select *
    from account
    where id = #{id}
  </select>
  <update id="updateDeposit" keyProperty="id" parameterType="com.cloud.demo.model.Account">
    update account
    set deposit=#{deposit},
      version = version + 1
    where id = #{id}
     and version = #{version}
  </update>
  <select id="selectByIdForUpdate" resultType="com.cloud.demo.model.Account">
    select *
    from account
    where id = #{id} for
    update
  </select>
  <update id="updateDepositPessimistic" keyProperty="id" parameterType="com.cloud.demo.model.Account">
    update account
    set deposit=#{deposit}
    where id = #{id}
  </update>
  <select id="getTotalDeposit" resultType="java.math.BigDecimal">
    select sum(deposit) from account;
  </select>
</mapper>

Mapper 接口

@Component
public interface AccountMapper {
  Account selectById(int id);
  Account selectByIdForUpdate(int id);
  int updateDepositWithVersion(Account account);
  void updateDeposit(Account account);
  BigDecimal getTotalDeposit();
}

Account POJO

@Data
public class Account {
  private int id;
  private BigDecimal deposit;
  private int version;
}

AccountService

在 transferOptimistic 方法上有個自定義注解 @Retry,這個用來實現(xiàn)樂觀鎖失敗后重試。

@Slf4j
@Service
public class AccountService {

  public enum Result{
    SUCCESS,
    DEPOSIT_NOT_ENOUGH,
    FAILED,
  }

  @Resource
  private AccountMapper accountMapper;

  private BiPredicate<BigDecimal, BigDecimal> isDepositEnough = (deposit, value) -> deposit.compareTo(value) > 0;

  /**
   * 轉(zhuǎn)賬操作,悲觀鎖
   *
   * @param fromId 扣款賬戶
   * @param toId  收款賬戶
   * @param value 金額
   */
  @Transactional(isolation = Isolation.READ_COMMITTED)
  public Result transferPessimistic(int fromId, int toId, BigDecimal value) {
    Account from, to;

    try {
      // 先鎖 id 較大的那行,避免死鎖
      if (fromId > toId) {
        from = accountMapper.selectByIdForUpdate(fromId);
        to = accountMapper.selectByIdForUpdate(toId);
      } else {
        to = accountMapper.selectByIdForUpdate(toId);
        from = accountMapper.selectByIdForUpdate(fromId);
      }
    } catch (Exception e) {
      log.error(e.getMessage());
      TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
      return Result.FAILED;
    }

    if (!isDepositEnough.test(from.getDeposit(), value)) {
      TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
      log.info(String.format("Account %d is not enough.", fromId));
      return Result.DEPOSIT_NOT_ENOUGH;
    }

    from.setDeposit(from.getDeposit().subtract(value));
    to.setDeposit(to.getDeposit().add(value));

    accountMapper.updateDeposit(from);
    accountMapper.updateDeposit(to);

    return Result.SUCCESS;
  }

  /**
   * 轉(zhuǎn)賬操作,樂觀鎖
   * @param fromId 扣款賬戶
   * @param toId  收款賬戶
   * @param value 金額
   */
  @Retry
  @Transactional(isolation = Isolation.REPEATABLE_READ)
  public Result transferOptimistic(int fromId, int toId, BigDecimal value) {
    Account from = accountMapper.selectById(fromId),
        to = accountMapper.selectById(toId);

    if (!isDepositEnough.test(from.getDeposit(), value)) {
      TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
      return Result.DEPOSIT_NOT_ENOUGH;
    }

    from.setDeposit(from.getDeposit().subtract(value));
    to.setDeposit(to.getDeposit().add(value));

    int r1, r2;

    // 先鎖 id 較大的那行,避免死鎖
    if (from.getId() > to.getId()) {
      r1 = accountMapper.updateDepositWithVersion(from);
      r2 = accountMapper.updateDepositWithVersion(to);
    } else {
      r2 = accountMapper.updateDepositWithVersion(to);
      r1 = accountMapper.updateDepositWithVersion(from);
    }

    if (r1 < 1 || r2 < 1) {
      // 失敗,拋出重試異常,執(zhí)行重試
      throw new RetryException("Transfer failed, retry.");
    } else {
      return Result.SUCCESS;
    }
  }
}

使用 Spring AOP 實現(xiàn)樂觀鎖失敗后重試

自定義注解 Retry

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Retry {
  int value() default 3; // 重試次數(shù)
}

重試異常 RetryException

public class RetryException extends RuntimeException {
  public RetryException(String message) {
    super(message);
  }
}

重試的切面類

tryAgain 方法使用了 @Around 注解(表示環(huán)繞通知),可以決定目標方法在何時執(zhí)行,或者不執(zhí)行,以及自定義返回結(jié)果。這里首先通過 ProceedingJoinPoint.proceed() 方法執(zhí)行目標方法,如果拋出了重試異常,那么重新執(zhí)行直到滿三次,三次都不成功則回滾并返回 FAILED。

@Slf4j
@Aspect
@Component
public class RetryAspect {

  @Pointcut("@annotation(com.cloud.demo.annotation.Retry)")
  public void retryPointcut() {

  }

  @Around("retryPointcut() && @annotation(retry)")
  @Transactional(isolation = Isolation.READ_COMMITTED)
  public Object tryAgain(ProceedingJoinPoint joinPoint, Retry retry) throws Throwable {
    int count = 0;
    do {
      count++;
      try {
        return joinPoint.proceed();
      } catch (RetryException e) {
        if (count > retry.value()) {
          log.error("Retry failed!");
          TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
          return AccountService.Result.FAILED;
        }
      }
    } while (true);
  }
}

單元測試

用多個線程模擬并發(fā)轉(zhuǎn)賬,經(jīng)過測試,悲觀鎖除了賬戶余額不足,或者數(shù)據(jù)庫連接不夠以及等待超時,全部成功;樂觀鎖即使加了重試,成功的線程也很少,500 個平均也就十幾個成功。

所以對于寫多讀少的操作,使用悲觀鎖,對于讀多寫少的操作,可以使用樂觀鎖。

完整代碼請見 Github:https://github.com/imcloudfloating/Lock_Demo。

@Slf4j
@SpringBootTest
@RunWith(SpringRunner.class)
class AccountServiceTest {

  // 并發(fā)數(shù)
  private static final int COUNT = 500;

  @Resource
  AccountMapper accountMapper;

  @Resource
  AccountService accountService;

  private CountDownLatch latch = new CountDownLatch(COUNT);
  private List<Thread> transferThreads = new ArrayList<>();
  private List<Pair<Integer, Integer>> transferAccounts = new ArrayList<>();

  @BeforeEach
  void setUp() {
    Random random = new Random(currentTimeMillis());
    transferThreads.clear();
    transferAccounts.clear();

    for (int i = 0; i < COUNT; i++) {
      int from = random.nextInt(10) + 1;
      int to;
      do{
        to = random.nextInt(10) + 1;
      } while (from == to);
      transferAccounts.add(new Pair<>(from, to));
    }
  }

  /**
   * 測試悲觀鎖
   */
  @Test
  void transferByPessimisticLock() throws Throwable {
    for (int i = 0; i < COUNT; i++) {
      transferThreads.add(new Transfer(i, true));
    }
    for (Thread t : transferThreads) {
      t.start();
    }
    latch.await();

    Assertions.assertEquals(accountMapper.getTotalDeposit(),
        BigDecimal.valueOf(10000).setScale(2, RoundingMode.HALF_UP));
  }

  /**
   * 測試樂觀鎖
   */
  @Test
  void transferByOptimisticLock() throws Throwable {
    for (int i = 0; i < COUNT; i++) {
      transferThreads.add(new Transfer(i, false));
    }
    for (Thread t : transferThreads) {
      t.start();
    }
    latch.await();

    Assertions.assertEquals(accountMapper.getTotalDeposit(),
        BigDecimal.valueOf(10000).setScale(2, RoundingMode.HALF_UP));
  }

  /**
   * 轉(zhuǎn)賬線程
   */
  class Transfer extends Thread {
    int index;
    boolean isPessimistic;

    Transfer(int i, boolean b) {
      index = i;
      isPessimistic = b;
    }

    @Override
    public void run() {
      BigDecimal value = BigDecimal.valueOf(
          new Random(currentTimeMillis()).nextFloat() * 100
      ).setScale(2, RoundingMode.HALF_UP);

      AccountService.Result result = AccountService.Result.FAILED;
      int fromId = transferAccounts.get(index).getKey(),
          toId = transferAccounts.get(index).getValue();
      try {
        if (isPessimistic) {
          result = accountService.transferPessimistic(fromId, toId, value);
        } else {
          result = accountService.transferOptimistic(fromId, toId, value);
        }
      } catch (Exception e) {
        log.error(e.getMessage());
      } finally {
        if (result == AccountService.Result.SUCCESS) {
          log.info(String.format("Transfer %f from %d to %d success", value, fromId, toId));
        }
        latch.countDown();
      }
    }
  }
}

MySQL 配置

innodb_rollback_on_timeout='ON'
max_connections=1000
innodb_lock_wait_timeout=500

以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。

相關(guān)文章

  • JAVA生成pdf文件的實操教程

    JAVA生成pdf文件的實操教程

    PDF是可移植文檔格式,是一種電子文件格式,具有許多其他電子文檔格式無法相比的優(yōu)點,下面這篇文章主要給大家介紹了關(guān)于JAVA生成pdf文件的相關(guān)資料,文中通過圖文介紹的非常詳細,需要的朋友可以參考下
    2022-11-11
  • 基于從request獲取各種路徑的方法介紹

    基于從request獲取各種路徑的方法介紹

    下面小編就為大家分享一篇基于從request獲取各種路徑的方法介紹,具有很好的參考價值,希望對大家有所幫助
    2017-11-11
  • Java Base64算法實際應(yīng)用之郵件發(fā)送實例分析

    Java Base64算法實際應(yīng)用之郵件發(fā)送實例分析

    這篇文章主要介紹了Java Base64算法實際應(yīng)用之郵件發(fā)送,結(jié)合實例形式分析了java字符編碼與郵件發(fā)送相關(guān)操作技巧,需要的朋友可以參考下
    2019-09-09
  • 使用spring-cache一行代碼解決緩存擊穿問題

    使用spring-cache一行代碼解決緩存擊穿問題

    本文主要介紹了使用spring-cache一行代碼解決緩存擊穿問題,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2022-04-04
  • idea maven 構(gòu)建本地jar包及pom文件的過程

    idea maven 構(gòu)建本地jar包及pom文件的過程

    這篇文章主要介紹了idea maven 構(gòu)建本地jar包及pom文件的過程,本文通過實例代碼給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友參考下吧
    2023-11-11
  • Java Web應(yīng)用程序?qū)崿F(xiàn)基礎(chǔ)的文件下載功能的實例講解

    Java Web應(yīng)用程序?qū)崿F(xiàn)基礎(chǔ)的文件下載功能的實例講解

    這里我們演示了Servelet驅(qū)動Tomcat來進行HTTP下載的方法,接下來就詳細來看Java Web應(yīng)用程序?qū)崿F(xiàn)基礎(chǔ)的文件下載功能的實例講解
    2016-05-05
  • 在Docker中部署Spring Boot項目過程詳解

    在Docker中部署Spring Boot項目過程詳解

    這篇文章主要介紹了在Docker中部署Spring Boot項目,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下
    2019-08-08
  • Maven發(fā)布項目到Nexus私有服務(wù)器

    Maven發(fā)布項目到Nexus私有服務(wù)器

    本文主要介紹了Maven發(fā)布項目到Nexus私有服務(wù)器,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2023-07-07
  • java返回前端實體類json數(shù)據(jù)時忽略某個屬性方法

    java返回前端實體類json數(shù)據(jù)時忽略某個屬性方法

    這篇文章主要給大家介紹了關(guān)于java返回前端實體類json數(shù)據(jù)時忽略某個屬性的相關(guān)資料,文中通過示例代碼介紹的非常詳細,需要的朋友可以參考下
    2023-08-08
  • Java異常處理與throws關(guān)鍵字用法分析

    Java異常處理與throws關(guān)鍵字用法分析

    這篇文章主要介紹了Java異常處理與throws關(guān)鍵字用法,結(jié)合實例形式分析了java常見的異常、錯誤處理及throws關(guān)鍵字相關(guān)使用技巧、注意事項,需要的朋友可以參考下
    2019-01-01

最新評論