Spring Boot整合MyBatis如何实现乐观锁和悲观锁
发表于:2025-02-24 作者:千家信息网编辑
千家信息网最后更新 2025年02月24日,这篇文章给大家分享的是有关Spring Boot整合MyBatis如何实现乐观锁和悲观锁的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。本文以转账操作为例,实现并测试乐观锁和
千家信息网最后更新 2025年02月24日Spring Boot整合MyBatis如何实现乐观锁和悲观锁
这篇文章给大家分享的是有关Spring Boot整合MyBatis如何实现乐观锁和悲观锁的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。
本文以转账操作为例,实现并测试乐观锁和悲观锁。
死锁问题
当 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 字段。
update account set deposit=#{deposit}, version = version + 1 where id = #{id} and version = #{version} update account set deposit=#{deposit} where id = #{id}
Mapper 接口
@Componentpublic interface AccountMapper { Account selectById(int id); Account selectByIdForUpdate(int id); int updateDepositWithVersion(Account account); void updateDeposit(Account account); BigDecimal getTotalDeposit();}
Account POJO
@Datapublic class Account { private int id; private BigDecimal deposit; private int version;}
AccountService
在 transferOptimistic 方法上有个自定义注解 @Retry,这个用来实现乐观锁失败后重试。
@Slf4j@Servicepublic 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; } }}
使用 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@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 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(); } } }}
MySQL 配置
innodb_rollback_on_timeout='ON'max_connections=1000innodb_lock_wait_timeout=500
感谢各位的阅读!关于"Spring Boot整合MyBatis如何实现乐观锁和悲观锁"这篇文章就分享到这里了,希望以上内容可以对大家有一定的帮助,让大家可以学到更多知识,如果觉得文章不错,可以把它分享出去让更多的人看到吧!
乐观
悲观
转账
账户
事务
方法
测试
成功
数据
死锁
较大
注解
线程
整合
两个
内容
对方
扣款
更多
目标
数据库的安全要保护哪些东西
数据库安全各自的含义是什么
生产安全数据库录入
数据库的安全性及管理
数据库安全策略包含哪些
海淀数据库安全审计系统
建立农村房屋安全信息数据库
易用的数据库客户端支持安全管理
连接数据库失败ssl安全错误
数据库的锁怎样保障安全
告警数据库
钢钢好电脑版出现连接服务器失败
正规计算机软件开发
智能网络安全技术未来
国网服务器安全要求
铜城分局网络安全保卫大队
服务器年费是指
宣传网络安全四格画
数据库有哪些关键技术
技术创新网络安全工程
安卓原神怎么搭建私人服务器
删除10g数据库方法
杭州市网信办网络安全支撑单位
数据库开发设计和疫情的联系
服务器架位签
中国人保寿险技术软件开发
网络安全法律法规和方针政策
软件开发公司研究生工资
软件开发人员能力矩阵
高鸿网络技术有限公司
服务器与主机的区别
全宅无线网络技术
上网无法解析服务器
万方数据库导出键在哪里
数据库的操作包括
战地4中国服务器
深圳触摸屏服务器厂家
互联网科技那些事儿
武汉明源软件开发面试题
哪些企业服务器放在贵州云服务器