SpringBoot 는 MyBatis 를 통합 하여 낙관적 인 자물쇠 와 비관 적 인 자 물 쇠 를 실현 하 는 예 입 니 다.

본 고 는 이체 작업 을 예 로 들 어 낙관적 인 자물쇠 와 비관 적 인 자 물 쇠 를 실현 하고 테스트 한다.
모든 코드:https://github.com/imcloudfloating/Lock_Demo
GitHub Page: https://cloudli.top
질문
A,B 두 계좌 가 동시에 상대방 에 게 이 체 를 할 때 다음 과 같은 상황 이 발생 한다.
시시각각
사무 1(A 가 B 에 게 이체)
사무 2(B 가 A 에 게 이체)
T1
Lock A
Lock B
T2
Lock B(사무 2 로 인해 A 잠 금,대기)
Lock A(사무 1 이 잠 겨 있 기 때문에 B,대기)
두 가지 업무 가 모두 상대방 이 자 물 쇠 를 풀 기 를 기다 리 고 있 기 때문에 자물쇠 가 생 겼 습 니 다.해결 방안 은 홈 키 의 크기 에 따라 자 물 쇠 를 추가 하고 항상 홈 키 가 작 거나 큰 줄 의 데 이 터 를 잠 그 는 것 입 니 다.
데이터 시트 만 들 고 데이터 삽입(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 가 있 습 니 다.이것 은 낙관적 인 잠 금 이 실 패 했 을 때 다시 시도 합 니 다.

@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;

  /**
   *     ,   
   *
   * @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;
  }

  /**
   *     ,   
   * @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) {
      //   ,      ,    
      throw new RetryException("Transfer failed, retry.");
    } else {
      return Result.SUCCESS;
    }
  }
}

Spring AOP 를 사용 하여 낙관적 인 잠 금 을 실현 하 는 데 실패 한 후 다시 시도 합 니 다.
사용자 정의 주석 Retry

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Retry {
  int value() default 3; //     
}
다시 시도 이상 RetryException

public class RetryException extends RuntimeException {
  public RetryException(String message) {
    super(message);
  }
}
다시 시도 하 는 절단면 류
try Again 방법 은@Around 주석(서 라운드 알림 표시)을 사용 하여 대상 방법 이 언제 실 행 될 지,실행 하지 않 을 지,결 과 를 사용자 정의 할 수 있 습 니 다.여기 서 먼저 Proceeding JoinPoint.proceed()방법 을 통 해 목표 방법 을 수행 합 니 다.재 시도 이상 을 던 지면 만 3 회 까지 다시 실행 하고 세 번 성공 하지 못 하면 다시 굴 러 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);
  }
}

유닛 테스트
여러 스 레 드 로 동시 이체 시 뮬 레이 션 을 하고 테스트 를 통 해 비관 적 인 자 물 쇠 는 계 정 잔액 이 부족 하거나 데이터 베이스 연결 이 부족 하고 시간 이 초과 되 는 것 을 제외 하고 모두 성공 했다.낙관적 인 자 물 쇠 는 재 시도 에 도 성공 하 는 스 레 드 가 적 고 500 개 평균 10 여 개 에 성공 한다.
따라서 많이 읽 고 적 게 쓰 는 조작 에 대해 비관 적 인 자 물 쇠 를 사용 하고 많이 읽 고 적 게 쓰 는 조작 에 대해 낙관적 인 자 물 쇠 를 사용 할 수 있다.
전체 코드 는 Github:https://github.com/imcloudfloating/Lock_Demo을 보십시오.

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

  //    
  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));
  }

  /**
   *     
   */
  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
이상 이 바로 본 고의 모든 내용 입 니 다.여러분 의 학습 에 도움 이 되 고 저 희 를 많이 응원 해 주 셨 으 면 좋 겠 습 니 다.

좋은 웹페이지 즐겨찾기