봄 부팅 달성 통합은 낙관적 및 비관적 잠금을 MyBatis로

본 논문에서는 전송 작업은, 예를 들어, 구현하고 낙관적 비관적 잠금을 테스트합니다.

모든 코드 : https://github.com/imcloudfloating/Lock_Demo

교착 상태

A는 서로 B는 동시에 두 개의 계정을 전송하면, 다음이 될 것입니다 :

시간 트랜잭션 1 (B에 대한 전송) 트랜잭션 2 (A B에 전송)
T1 잠금 장치 잠금 B
T2 B 잠금 (트랜잭션이 잠금 장치가 있기 때문에, 대기) 잠금 장치 (트랜잭션이 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);

매퍼 파일

버전 낙관적 잠금 필드를 사용하여, 업데이트 ... 선택을 사용하여 비관적 잠금.

<?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>

매퍼 인터페이스

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

계정 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;
        }
    }
}

스프링 AOP가 낙관적 잠금 실패를 달성하고 다시 시도합니다

사용자 정의 주석 재시도

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

측면 클래스를 다시 시도

TRYAGAIN 방법은 어떤 방법을 수행하기 위해, 또는 사용자 정의 반환 결과뿐만 아니라 수행하지 않는 경우를 결정할 수 있습니다 (거들 통지) 주석을 @Around 사용합니다. 여기서 제 ProceedingJoinPoint.proceed () 대상에있어서의 방법에 의해 수행되고, 슬로우 재시 제외하면 다시 성공없이 세 번 롤 후, 전체 세 번까지 다시 실행 실패 돌아갔다.

@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 평균 열 재 시도를 추가 록킹 성공적인 여러.

그래서 쓰기 위해 일단, 많은 작은 읽기 작업 읽기, 비관적 잠금을 사용하여 작은 작업을 작성, 당신은 낙관적 잠금을 사용할 수 있습니다.

: 전체 코드 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

추천

출처www.cnblogs.com/cloudfloating/p/11461530.html