符合中小企业对网站设计、功能常规化式的企业展示型网站建设
本套餐主要针对企业品牌型网站、中高端设计、前端互动体验...
商城网站建设因基本功能的需求不同费用上面也有很大的差别...
手机微信网站开发、微信官网、微信商城网站...
这篇文章给大家分享的是有关Spring Boot整合MyBatis如何实现乐观锁和悲观锁的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。
青县网站制作公司哪家好,找创新互联!从网页设计、网站建设、微信开发、APP开发、响应式网站等网站项目制作,到程序开发,运营维护。创新互联从2013年创立到现在10年的时间,我们拥有了丰富的建站经验和运维经验,来保证我们的工作的顺利进行。专注于网站建设就选创新互联。
本文以转账操作为例,实现并测试乐观锁和悲观锁。
当 A, B 两个账户同时向对方转账时,会出现如下情况:
时刻 | 事务 1 (A 向 B 转账) | 事务 2 (B 向 A 转账) |
---|---|---|
T1 | Lock A | Lock B |
T2 | Lock B (由于事务 2 已经 Lock A,等待) | Lock A (由于事务 1 已经 Lock B,等待) |
由于两个事务都在等待对方释放锁,于是死锁产生了,解决方案:按照主键的大小来加锁,总是先锁主键较小或较大的那行数据。
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,乐观锁使用 version 字段。
update account set deposit=#{deposit}, version = version + 1 where id = #{id} and version = #{version} update account set deposit=#{deposit} where id = #{id}
@Component public interface AccountMapper { Account selectById(int id); Account selectByIdForUpdate(int id); int updateDepositWithVersion(Account account); void updateDeposit(Account account); BigDecimal getTotalDeposit(); }
@Data public class Account { private int id; private BigDecimal deposit; private int version; }
在 transferOptimistic 方法上有个自定义注解 @Retry,这个用来实现乐观锁失败后重试。
@Slf4j @Service public class AccountService { public enum Result{ SUCCESS, DEPOSIT_NOT_ENOUGH, FAILED, } @Resource private AccountMapper accountMapper; private BiPredicateisDepositEnough = (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; } } }
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface Retry { int value() default 3; // 重试次数 }
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 ListtransferThreads = new ArrayList<>(); private List > 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(); } } } }
innodb_rollback_on_timeout='ON' max_connections=1000 innodb_lock_wait_timeout=500
感谢各位的阅读!关于“Spring Boot整合MyBatis如何实现乐观锁和悲观锁”这篇文章就分享到这里了,希望以上内容可以对大家有一定的帮助,让大家可以学到更多知识,如果觉得文章不错,可以把它分享出去让更多的人看到吧!