您好,登錄后才能下訂單哦!
本文以轉(zhuǎn)賬操作為例,實(shí)現(xiàn)并測試樂觀鎖和悲觀鎖。
全部代碼:https://github.com/imcloudfloating/Lock_Demo
GitHub Page:https://cloudli.top
死鎖問題
當(dāng) A, B 兩個(gè)賬戶同時(shí)向?qū)Ψ睫D(zhuǎn)賬時(shí),會(huì)出現(xiàn)如下情況:
時(shí)刻 | 事務(wù) 1 (A 向 B 轉(zhuǎn)賬) | 事務(wù) 2 (B 向 A 轉(zhuǎn)賬) |
---|---|---|
T1 | Lock A | Lock B |
T2 | Lock B (由于事務(wù) 2 已經(jīng) Lock A,等待) | Lock A (由于事務(wù) 1 已經(jīng) Lock B,等待) |
由于兩個(gè)事務(wù)都在等待對(duì)方釋放鎖,于是死鎖產(chǎn)生了,解決方案:按照主鍵的大小來加鎖,總是先鎖主鍵較小或較大的那行數(shù)據(jù)。
建立數(shù)據(jù)表并插入數(shù)據(jù)(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 方法上有個(gè)自定義注解 @Retry,這個(gè)用來實(shí)現(xiàn)樂觀鎖失敗后重試。
@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; /** * 轉(zhuǎn)賬操作,悲觀鎖 * * @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; } /** * 轉(zhuǎn)賬操作,樂觀鎖 * @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) { // 失敗,拋出重試異常,執(zhí)行重試 throw new RetryException("Transfer failed, retry."); } else { return Result.SUCCESS; } } }
使用 Spring AOP 實(shí)現(xiàn)樂觀鎖失敗后重試
自定義注解 Retry
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface Retry { int value() default 3; // 重試次數(shù) }
重試異常 RetryException
public class RetryException extends RuntimeException { public RetryException(String message) { super(message); } }
重試的切面類
tryAgain 方法使用了 @Around 注解(表示環(huán)繞通知),可以決定目標(biāo)方法在何時(shí)執(zhí)行,或者不執(zhí)行,以及自定義返回結(jié)果。這里首先通過 ProceedingJoinPoint.proceed() 方法執(zhí)行目標(biāo)方法,如果拋出了重試異常,那么重新執(zhí)行直到滿三次,三次都不成功則回滾并返回 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); } }
單元測試
用多個(gè)線程模擬并發(fā)轉(zhuǎn)賬,經(jīng)過測試,悲觀鎖除了賬戶余額不足,或者數(shù)據(jù)庫連接不夠以及等待超時(shí),全部成功;樂觀鎖即使加了重試,成功的線程也很少,500 個(gè)平均也就十幾個(gè)成功。
所以對(duì)于寫多讀少的操作,使用悲觀鎖,對(duì)于讀多寫少的操作,可以使用樂觀鎖。
完整代碼請(qǐng)見 Github:https://github.com/imcloudfloating/Lock_Demo。
@Slf4j @SpringBootTest @RunWith(SpringRunner.class) class AccountServiceTest { // 并發(fā)數(shù) 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)); } /** * 轉(zhuǎn)賬線程 */ 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
以上就是本文的全部內(nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持億速云。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請(qǐng)聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。