您好,登錄后才能下訂單哦!
本文以轉賬操作為例,實現并測試樂觀鎖和悲觀鎖。
全部代碼: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 已經 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
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持億速云。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。