春ブーツは、統合MyBatisの楽観と悲観的ロックを達成します

この論文では、転送動作は、例えば、楽観的と悲観的ロックを実装し、テストします。

すべてのコード:https://github.com/imcloudfloating/Lock_Demo

デッドロック

A、B、互いに同時に2つのアカウントを転送する場合、以下が存在するであろう。

時間 トランザクション1(Bに転送) トランザクション2(AにB転送)
T1 ロックA ロックB
T2 ロックB(トランザクションが2ロックAを有しているので、待ちます) ロックA(トランザクションが1ロックBを持っているので、待ちます)

主キーロックの大きさに応じて、常にデータの小さいまたは大きい行に主キーをロック:お互いのロックを解除するための2つのトランザクションが待っているので、その後、デッドロック、溶液を生成しました。

データテーブルを設定し、データを挿入する(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);

マッパーファイル

ペシミスティック・ロックバージョン楽観的ロック・フィールドを使用して、SELECT ... FOR UPDATEを使用します。

<?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()メソッドによって実行され、再試行例外をスローした場合、その後、成功せずに戻って三回をロール、その後、完全な3回まで再実行し、失敗した返さ。

@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

おすすめ

転載: www.cnblogs.com/cloudfloating/p/11461530.html