中文字幕av专区_日韩电影在线播放_精品国产精品久久一区免费式_av在线免费观看网站

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Spring Boot整合MyBatis如何實現樂觀鎖和悲觀鎖

發布時間:2021-10-21 10:54:54 來源:億速云 閱讀:157 作者:小新 欄目:編程語言

這篇文章給大家分享的是有關Spring Boot整合MyBatis如何實現樂觀鎖和悲觀鎖的內容。小編覺得挺實用的,因此分享給大家做個參考,一起跟隨小編過來看看吧。

本文以轉賬操作為例,實現并測試樂觀鎖和悲觀鎖。

死鎖問題

當 A, B 兩個賬戶同時向對方轉賬時,會出現如下情況:

時刻事務 1 (A 向 B 轉賬)事務 2 (B 向 A 轉賬)
T1Lock ALock B
T2Lock B (由于事務 2 已經 Lock A,等待)Lock A (由于事務 1 已經 Lock 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);
    }
}

重試的切面類

tryAgain 方法使用了 @Around 注解(表示環繞通知),可以決定目標方法在何時執行,或者不執行,以及自定義返回結果。這里首先通過 ProceedingJoinPoint.proceed() 方法執行目標方法,如果拋出了重試異常,那么重新執行直到滿三次,三次都不成功則回滾并返回 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 個平均也就十幾個成功。

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

完整代碼請見 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

感謝各位的閱讀!關于“Spring Boot整合MyBatis如何實現樂觀鎖和悲觀鎖”這篇文章就分享到這里了,希望以上內容可以對大家有一定的幫助,讓大家可以學到更多知識,如果覺得文章不錯,可以把它分享出去讓更多的人看到吧!

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

珠海市| 颍上县| 依安县| 横峰县| 长乐市| 象山县| 维西| 井研县| 灵石县| 安塞县| 黑水县| 新化县| 贡山| 黎城县| 烟台市| 康定县| 黔西| 宣化县| 商丘市| 盐源县| 陇西县| 景德镇市| 建德市| 嘉鱼县| 永修县| 平度市| 隆化县| 丹东市| 铜陵市| 泽普县| 柘城县| 宿迁市| 湟源县| 招远市| 噶尔县| 浮梁县| 泽库县| 衡阳县| 华亭县| 锡林浩特市| 隆安县|