Spring Boot整合MyBatis如何实现乐观锁和悲观锁
发表于:2025-01-16 作者:千家信息网编辑
千家信息网最后更新 2025年01月16日,这篇文章给大家分享的是有关Spring Boot整合MyBatis如何实现乐观锁和悲观锁的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。本文以转账操作为例,实现并测试乐观锁和
千家信息网最后更新 2025年01月16日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安全错误
数据库的锁怎样保障安全
常用空间数据库技术
深圳市唯盛网络技术有限公司
知商汇选互联网科技有限公司
明日之后希望谷地服务器什么好卖
vba 数据库增加 记录
僵尸毁灭工程突然没服务器了
网络安全监督检查视频
u8数据库引擎打不开文件
江西常见软件开发制造价格
供应链系统软件开发
湖南服务器虚拟化迁移云服务器
华为网络顶盒刷打印机服务器
后端软件开发基础
超市数据库下载
关于网络安全的黑板报大学
燃烧的远征服务器开放时间
sql2008数据库备份
机房服务器怎么重启
中经网统计数据库如何下载数据
软件开发f层
音乐服务器如何连接hifi
导入sql数据库慢
兰固分局网络安全
数据库表输出为exc
有没有邮箱间数据库
中国大学生网络安全竞赛
网络安全常用工具和原理
千年服务器连接网站更改
燃烧的远征服务器开放时间
网络安全防疫设备