时间:2021-05-20
本文以转账操作为例,实现并测试乐观锁和悲观锁。
全部代码: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)
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 接口
Account POJO
AccountService
在 transferOptimistic 方法上有个自定义注解 @Retry,这个用来实现乐观锁失败后重试。
@Slf4j@Servicepublic 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
重试异常 RetryException
重试的切面类
tryAgain 方法使用了 @Around 注解(表示环绕通知),可以决定目标方法在何时执行,或者不执行,以及自定义返回结果。这里首先通过 ProceedingJoinPoint.proceed() 方法执行目标方法,如果抛出了重试异常,那么重新执行直到满三次,三次都不成功则回滚并返回 FAILED。
@Slf4j@Aspect@Componentpublic 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 配置
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。
声明:本页内容来源网络,仅供用户参考;我单位不保证亦不表示资料全面及准确无误,也不保证亦不表示这些资料为最新信息,如因任何原因,本网内容或者用户因倚赖本网内容造成任何损失或损害,我单位将不会负任何法律责任。如涉及版权问题,请提交至online#300.cn邮箱联系删除。
本文实例讲述了java乐观锁原理与实现。分享给大家供大家参考,具体如下:简单说说乐观锁。乐观锁是相对于悲观锁而言。悲观锁认为,这个线程,发生并发的可能性极大,线
本文实例讲述了PHP+redis实现的悲观锁。分享给大家供大家参考,具体如下:锁机制通常使用的锁分为乐观锁,悲观锁这两种,简单介绍下这两种锁,作为本文的背景知识
本文研究的主要是Hibernate悲观锁和乐观锁的全部内容,具体介绍如下。悲观锁悲观锁通常是由数据库机制实现的,在整个过程中把数据锁住(查询时),只要事物不释放
悲观锁、乐观锁简介:悲观锁:同步操作。即用户A在操作某条数据时,为其上锁,限制其他用户操作,用户A操作完成提交事务后其他用户方可操作此数据。乐观锁:使用版本控制
目录前言一、乐观锁添加version字段二、悲观锁读锁全表锁(LOCKTABLE表READ)行锁(SELECT...LOCKINSHAREMODE)写锁全表锁(