Spring Framework 学习笔记5:事务

Spring Framework 学习笔记5:事务

1.快速入门

1.1.准备工作

这里提供一个示例项目 transaction-demo,这个项目包含 Spring 框架、MyBatis 以及 JUnit。

对应的表结构见 bank.sql。

服务层有一个方法可以用于在不同的账户间进行转账:

@Service
public class AccountServiceImpl implements AccountService {@Autowiredprivate AccountMapper accountMapper;@Overridepublic Account getAcountByName(String name) {return accountMapper.selectByName(name);}@Overridepublic void transfer(String from, String to, double amount) {//从转出账户扣款accountMapper.delAmount(from, amount);//给转入账户加钱accountMapper.addAmount(to, amount);}
}

这里我编写了一个简单的测试用例用于测试:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfig.class)
public class AccountServiceTests {@Autowiredprivate AccountService accountService;@Testpublic void testTransfer(){this.printAccounts();accountService.transfer("jack", "icexmoon", 20);this.printAccounts();}private void printAccounts(){System.out.println(accountService.getAcountByName("icexmoon"));System.out.println(accountService.getAcountByName("jack"));}
}

但实际上这里是有 bug 的,如果转出账户的余额小于要转出的金额,转出账户的金额就会变成负数。

最朴素的想法是在转出金额前先检查账户余额是否足够:

@Override
public void transfer(String from, String to, double amount) {//查询并检查转出账户的余额是否足够Account account = accountMapper.selectByName(from);if (account == null) {throw new RuntimeException("账户 %s 不存在");}if (account.getAmount() - amount < 0) {throw new RuntimeException("账户 %s 的余额不足");}//从转出账户扣款accountMapper.delAmount(from, amount);//给转入账户加钱accountMapper.addAmount(to, amount);
}

将测试用例中的转账金额改成一个很大的数字(比如10000)后再次测试,就能发现会抛出异常,转账不会进行。

1.2.并发问题

似乎这样做已经没有问题了。但是,显然我们的数据库操作是可以并行的,同时不可能只存在一个对 account 表的操作。如果同时存在多个对同一个账户的操作,会发生什么?

看这个测试用例:

@Test
public void testTransfer2() throws InterruptedException {this.printAccounts();new Thread(()->{accountService.saveMoney("jack", 1000);try {Thread.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}accountService.getBackMoney("jack", 1000);}).start();new Thread(()->{try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}accountService.transfer("jack", "icexmoon", 3000);}).start();Thread.sleep(2000);this.printAccounts();
}

这里有两个线程,一个尝试进行转账,从 jack 账户转账 3000 到 icexmoon 账户。另一个线程会先存 1000 再取 1000。

数据库里此时 jack 账户 2000,icexmoon 账户 1000。

理想情况是应该有两种结果:

  • 转账成功,存钱成功但取钱失败(因为存钱和取钱并不能同时发生)。
  • 转账失败,存钱和取钱操作成功。

如果你多执行几次,应该就能看到某次结果如下:

Account(id=1, name=icexmoon, amount=1000.0)
Account(id=2, name=jack, amount=2000.0)
Account(id=1, name=icexmoon, amount=4000.0)
Account(id=2, name=jack, amount=-1000.0)

这相当诡异,着表明转账、存钱和取钱都成功了。且 jack 账户余额变成了负数,明明我们有提前检查余额是否足够了。

为了能够“恰好”出现这种情况,我在代码中添加了一些 Thread.sleep(),以确保这种错误出现的概率提高。

出现这种情况的原因本质上和多线程的问题是一致的,即资源共享。本质上 account 表上 name 为 jack 的数据行在这里充当了共享资源。如果我们在访问该资源时不对其“锁定”(独占),就有可能出现:

  • A 线程存入 1000,余额为3000
  • B 线程尝试转账,发现余额足够,执行转账操作
  • A 线程取钱,发现余额足够,执行取钱操作
  • 转账操作执行,扣除2000
  • 取钱操作执行,扣除1000
  • 此时账户余额 -1000

要解决这个问题也很简单,使用 Spring 事务。

1.3.使用事务

使用事务要定义一个PlatformTransactionManager

@Configuration
public class TransactionConfig {@Beanpublic PlatformTransactionManager transactionManager(DataSource dataSource){DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();dataSourceTransactionManager.setDataSource(dataSource);return dataSourceTransactionManager;}
}

DataSourceTransactionManagerPlatformTransactionManager的一个实现类,它底层使用 JDBC 的事务,所以需要设置一个数据源。

还需要在配置类上添加@EnableTransactionManagement注解以开启事务:

@EnableTransactionManagement
public class SpringConfig {
}

在 Service 接口的相关方法上添加@Transactional

public interface AccountService {/*** 查看账户信息** @return*/@TransactionalAccount getAcountByName(String name);/*** 转账** @param from   转出账户* @param to     转入账户* @param amount*/@Transactionalvoid transfer(String from, String to, double amount);/*** 存钱** @param name   账户名* @param amount 金额*/@Transactionalvoid saveMoney(String name, double amount);/*** 取钱** @param name   账户名* @param amount 金额*/@Transactionalvoid getBackMoney(String name, double amount);
}

如果接口的所有方法都需要开启事务,可以在接口上使用@Transactional注解:

@Transactional
public interface AccountService {
}

当然也可以在实现类或方法上使用@Transactional注解,但在接口上使用更灵活——如果替换了实现类依然会使用事务。实际上 Spring 的事务是用 AOP 实现的,所以这种规则实际上是 AOP 的通知匹配 Bean 的规则。

如果测试用例中使用 Spring 事务,还需要在测试套件上添加注解:

@Transactional(transactionManager = "transactionManager")
@Rollback(value = false)
public class AccountServiceTests {// ...
}

现在再执行测试用例,就不会出现金额为负数的情况。

2.事务角色

Spring 事务除了可以发挥 JDBC 事务的用途——锁定共享资源以外。另一个重要的用途就是保证数据一致性,也就是说 Spring 事务生效的过程中,任意的异常产生都会让事务涉及的数据层操作回滚。

之所以 Spring 事务可以做到这一点,是因为 Spring 事务通过两个角色,将多个数据层事务(JDBC 事务)纳入了Spring 事务(通常定义在 Service 层)的管理,并形成一个事务整体。

在 Spring 事务中,两个角色分别是:

  • 事务管理员:发起事务方,在Spring中通常指代业务层开启事务的方法
  • 事务协调员:加入事务方,在Spring中通常指代数据层方法,也可以是业务层方法

具体到我们这个示例中,在服务层代码中:

public interface AccountService {// ...@Transactionalvoid transfer(String from, String to, double amount);
}@Service
public class AccountServiceImpl implements AccountService {// ...@Override@SneakyThrowspublic void transfer(String from, String to, double amount) {//查询并检查转出账户的余额是否足够this.checkAccountAmountIsEnough(from, amount);Thread.sleep(1000);//从转出账户扣款accountMapper.delAmount(from, amount);//给转入账户加钱accountMapper.addAmount(to, amount);System.out.println("转账成功");}
}

AccountService.transfer方法上的 Spring 事务就是事务管理员,这个方法中调用的两个数据层方法accountMapper.delAmountaccountMapper.addAmount上的 JDBC 事务就是事务协调员。

其实还调用了数据层的查询方法,这里省略。

之所以 Spring 可以做到这一点(统一管理 JDBC 事务),是因为我们定义的事务管理器(DataSourceTransactionManager)中使用的数据源(DataSource)和数据层(MyBatis)使用的数据源是同一个数据源。

3.事务属性

3.1.rollbackFor

Spring 事务并非对所有异常的产生都会回滚,比如:

@Override
public void transfer(String from, String to, double amount) throws InterruptedException, IOException {//查询并检查转出账户的余额是否足够this.checkAccountAmountIsEnough(from, amount);Thread.sleep(1000);//从转出账户扣款accountMapper.delAmount(from, amount);if (true) throw new IOException();//给转入账户加钱accountMapper.addAmount(to, amount);System.out.println("转账成功");
}

这里强制抛出一个IOException类型的异常。

  • 注意,这里没有使用@SneakyThrow处理异常,原因之后会说明。
  • if(true)是为了骗过编译器的语法检查。

执行测试用例:

@Test
@SneakyThrows
public void testTransfer() {this.printAccounts();accountService.transfer("jack", "icexmoon", 1000);this.printAccounts();
}

会发现 jack 账户的钱减少了,但 icexmoon 账户的钱没有增加,这说明事务回滚并没有生效。

原因是,默认情况下,Spring 事务只会对 ErrorRuntimeException类型的异常进行回滚

换言之,Spring 事务不会对“被检查的异常”进行回滚。而在上面的示例中,IOException就是一个被检查的异常。

很容易分辨异常是不是“被检查异常”,因为如果代码中有被检查的异常存在,编译器就会强制要求你进行处理(转换为运行时异常或在方法签名中声明异常抛出)。

解决的方式也很简单,将被检查的异常加入@Transactionalrollback属性:

public interface AccountService {// ...@Transactional(rollbackFor = {InterruptedException.class, IOException.class})void transfer(String from, String to, double amount) throws InterruptedException, IOException;
}

现在再执行测试用例,事务回滚就会正常生效。

此外,还可以用noRollbackFor属性指定哪些异常发生后不进行回滚。

当然,也可以将被检查的异常转换为运行时异常:

@Override
public void transfer(String from, String to, double amount) throws InterruptedException {//查询并检查转出账户的余额是否足够this.checkAccountAmountIsEnough(from, amount);Thread.sleep(1000);//从转出账户扣款accountMapper.delAmount(from, amount);try {if (true) throw new IOException();}catch (Exception e){throw new RuntimeException(e);}//给转入账户加钱accountMapper.addAmount(to, amount);System.out.println("转账成功");
}

这样就不存在我们之前说的问题,同样可以触发事务回滚。

在这里我们并不能使用@SneakyThrows,因为@SneakyThrows仅仅是骗过编译器,在不用在方法签名中声明异常的情况下抛出异常,并不会将被检查的异常转换为运行时异常:

@Override
@SneakyThrows
public void transfer(String from, String to, double amount) {//查询并检查转出账户的余额是否足够this.checkAccountAmountIsEnough(from, amount);Thread.sleep(1000);//从转出账户扣款accountMapper.delAmount(from, amount);if (true) throw new IOException();//给转入账户加钱accountMapper.addAmount(to, amount);System.out.println("转账成功");
}

如果你像上面示例中那样做了,实际上代码将抛出一个方法签名中不存在的被检查异常IOException,显然不会触发事务回滚。此外因为方法签名中没有声明的被检查异常被抛出,JVM 会抛出一个UndeclaredThrowableException

这件事告诉我们,要谨慎使用@SneakyThrows

3.2.案例:为转账添加日志

添加一张日志表:

CREATE TABLE `log` (`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '标识符',`content` varchar(255) COLLATE utf8mb4_general_ci NOT NULL COMMENT '内容',`create_time` datetime NOT NULL COMMENT '创建时间',PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='日志表'

添加 Mapper:

public interface LogMapper {@Insert("insert into log(content,create_time) values (#{content},NOW())")void addLog(String content);
}

添加 Service:

public interface LogService {@Transactionalvoid addTransferLog(String from, String to, double amount);
}@Service
public class LogServiceImpl implements LogService {@Autowiredprivate LogMapper logMapper;@Overridepublic void addTransferLog(String from, String to, double amount) {logMapper.addLog("%s 转账 %.2f 到 %s".formatted(from, amount, to));}
}

在转账操作中添加日志记录:

@Override
public void transfer(String from, String to, double amount) {try {//查询并检查转出账户的余额是否足够this.checkAccountAmountIsEnough(from, amount);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();throw new RuntimeException(e);}//从转出账户扣款accountMapper.delAmount(from, amount);//给转入账户加钱accountMapper.addAmount(to, amount);System.out.println("转账成功");} finally {logService.addTransferLog(from, to, amount);}
}

现在可以成功转账,并写入日志信息。

但这里存在一个问题,如果我们希望无论转账是否成功,都写一条日志信息。就会发现一些问题。

在转账逻辑中添加一条代码,触发“除零异常”:

@Override
public void transfer(String from, String to, double amount) {try {//查询并检查转出账户的余额是否足够this.checkAccountAmountIsEnough(from, amount);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();throw new RuntimeException(e);}//从转出账户扣款accountMapper.delAmount(from, amount);int i = 1/0;//给转入账户加钱accountMapper.addAmount(to, amount);System.out.println("转账成功");} finally {logService.addTransferLog(from, to, amount);}
}

这是一个运行时异常,所以事务回滚被触发,账户金额不会改变。但问题在于,日志同样没有写入。

因为在上面这个示例中,LogService.addTransferLog()方法的事务是一个事务协调员,它同样加入了AccountService.transfer()方法管理的事务,所以在异常发生后被一同回滚了。

要让日志添加操作不被回滚,我们就需要将其设置为单独的事务。

方法也很简单:

public interface LogService {@Transactional(propagation = Propagation.REQUIRES_NEW)void addTransferLog(String from, String to, double amount);
}

现在LogService.addTransferLog()将会在单独事务中执行,所以无论转账成功与否,都会有日志信息添加。

3.3.事务传播行为

在上面案例中,我们修改了@Transactionalpropagation属性,实际上是修改了“事务的传播行为”。

事务协调员的传播行为会影响到最终的执行效果,传播行为分为以下几种:

1630254257628

  • REQUIRED,默认行为。如果事务管理员开启了事务,就加入该事务。如果没有,新建事务。
  • REQUIRES_NEW,无论事务管理员是否开启事务,都新建一个事务。
  • SUPPORTS,如果事务管理员开启了事务,加入。如果没有,不使用事务。
  • NOT_SUPPORTED,无论事务管理员是否开启事务,都不使用事务。
  • MANDATORY,如果事务管理员开启了事务,加入。如果没有,报错。
  • NEVER,与MANDATORY规则相反。如果事务管理员开启了事务,报错。如果没有,不使用事务。
  • NESTED,设置回滚点,让事务回滚到指定的回滚点。

本文的完整示例可以从这里获取。

4.参考资料

  • 黑马程序员SSM框架教程

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/146003.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

机器学习之单层神经网络的训练:增量规则(Delta Rule)

文章目录 权重的调整单层神经网络使用delta规则的训练过程 神经网络以权值的形式存储信息,根据给定的信息来修改权值的系统方法称为学习规则。由于训练是神经网络系统地存储信息的唯一途径&#xff0c;因此学习规则是神经网络研究中的一个重要组成部分 权重的调整 &#xff08…

【中秋国庆不断更】OpenHarmony多态样式stateStyles使用场景

Styles和Extend仅仅应用于静态页面的样式复用&#xff0c;stateStyles可以依据组件的内部状态的不同&#xff0c;快速设置不同样式。这就是我们本章要介绍的内容stateStyles&#xff08;又称为&#xff1a;多态样式&#xff09;。 概述 stateStyles是属性方法&#xff0c;可以根…

蓝桥等考Python组别十级003

第一部分&#xff1a;选择题 1、Python L10 &#xff08;15分&#xff09; 已知s Pencil&#xff0c;下列说法正确的是&#xff08; &#xff09;。 s[0]对应的字符是Ps[1]对应的字符是ns[-1]对应的字符是is[3]对应的字符是e 正确答案&#xff1a;A 2、Python L10 &am…

NLP 03(LSTM)

一、LSTM LSTM (Long Short-Term Memory) 也称长短时记忆结构,它是传统RNN的变体,与经典RNN相比&#xff1a; 能够有效捕捉长序列之间的语义关联缓解梯度消失或爆炸现象 LSTM的结构更复杂,它的核心结构可以分为四个部分去解析: 遗忘门、输入门、细胞状态、输出门 LSTM内部结构…

解决 react 项目启动端口冲突

报错信息&#xff1a; Emitted error event on Server instance at:at emitErrorNT (net.js:1358:8)at processTicksAndRejections (internal/process/task_queues.js:82:21) {code: EADDRINUSE,errno: -4091,syscall: listen,address: 0.0.0.0,port: 8070 }解决方法&#xff…

叶工好容6-自定义与扩展

本篇主要介绍扩展的本质以及CRD与Operator之间的区别&#xff0c;帮助大家理解相关的概念以及知道要进行扩展需要做哪些工作。 CRD&#xff08;CustomerResourceDefinition&#xff09; 自定义资源定义,代表某种自定义的配置或者独立运行的服务。 用户只定义了CRD没有任何意…

最新AI智能创作系统ChatGPT商业源码+详细图文搭建部署教程+AI绘画系统

一、AI系统介绍 SparkAi创作系统是基于国外很火的ChatGPT进行开发的Ai智能问答系统。本期针对源码系统整体测试下来非常完美&#xff0c;可以说SparkAi是目前国内一款的ChatGPT对接OpenAI软件系统。那么如何搭建部署AI创作ChatGPT&#xff1f;小编这里写一个详细图文教程吧&am…

liunx的攻击

1.场景和分析 2.病毒分析 3.解决步骤

MySQL在线修改表结构-PerconaTookit工具

在线修改表结构必须慎重 在业务系统 运行 过程中随意删改字段&#xff0c;会 造成重大事故。 常规的做法是&#xff1a;业务停机&#xff0c;再 维护表结构 比如&#xff1a;12306 凌晨 0 点到早上 7 点是停机维护 如果是不影响正常业务的表结构是允许在线修改的。 比如&…

Ubuntu部署运行ORB-SLAM2

ORB-SLAM2是特征点法的视觉SLAM集大成者&#xff0c;不夸张地说是必学代码。博主已经多次部署运行与ORB-SLAM2相关的代码&#xff0c;所以对环境和依赖很熟悉&#xff0c;对整个系统也是学习了几个月&#xff0c;一行行代码理解。本次在工控机上部署记录下完整的流程。 ORB-SLA…

Vuex状态管理

一、Vuex简介&安装 简介 vuex是使用vue中必不可少的一部分&#xff0c;基于父子、兄弟组件&#xff0c;我们传值可能会很方便&#xff0c;但是如果是没有关联的组件之间要使用同一组数据&#xff0c;就显得很无能为力&#xff0c;那么vuex就很好的解决了我们这种问题&…

【数据结构】外部排序、多路平衡归并与败者树、置换-选择排序(生成初始归并段)、最佳归并树算法

目录 1、外部排序 1.1 基本概念 1.2 方法 2、多路平衡归并与败者树 2.1 K路平衡归并 2.2 败者树 3、置换-选择排序&#xff08;生成初始归并段&#xff09;​编辑 4、最佳归并树 4.1 理论基础​编辑 4.2 构造方法 ​编辑 5、各种排序算法的性质 1、外部排序 1.1 基本概…

简易磁盘自动监控服务

本文旨在利用crontab定时任务(脚本请参考附件)来监控单个服务节点上所有磁盘使用情况&#xff0c;一旦超过既定阈值则会通过邮件形式告警相关利益人及时介入处理。 1. 开启SMTP服务 为了能够成功接收告警信息&#xff0c;需要邮件接收客户都安开启SMTP服务。简要流程请参考下…

数字孪生智慧能源:风光储一体化能源中心

自“双碳”目标提出以来&#xff0c;我国能源产业不断朝着清洁低碳化、绿色化的方向发展。其中&#xff0c;风能、太阳能等可再生能源在促进全球能源可持续发展、共建清洁美丽世界中被寄予厚望。风能、太阳能具有波动性、间歇性、随机性等特点&#xff0c;主要通过转化为电能再…

中国逐年干燥度指数数据集

简介&#xff1a; 中国逐年干燥度指数&#xff0c;空间分辨率为1km&#xff0c;时间为1901-2022&#xff0c;为比值&#xff0c;没有单位。该数据集是基于中国1km逐月潜在蒸散发&#xff08;PET&#xff09;和降水量&#xff08;PRE&#xff09;采用比值法计算式得到&#xff…

Go_原子操作和锁

原子操作和锁 本文先探究并发问题&#xff0c;再探究锁和原子操作解决问题的方式&#xff0c;最后进行对比。 并发问题 首先&#xff0c;我们看一下程序 num该程序表面看上去一步就可以运行完成&#xff0c;但是实际上&#xff0c;在计算机中是分三步运行的&#xff0c;如下…

PHP 二手物品交易网站系统mysql数据库web结构apache计算机软件工程网页wamp

一、源码特点 PHP 二手物品交易网站系统是一套完善的web设计系统&#xff0c;对理解php编程开发语言有帮助&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S模式开发。 代码下载 https://download.csdn.net/download/qq_41221322/88385559 二、功能介…

分析各种表达式求值过程

目录 算术运算与赋值 编译器常用的两种优化方案 常量传播 常量折叠 加法 Debug编译选项组下编译后的汇编代码分析 Release开启02执行效率优先 减法 Release版下优化和加法一致&#xff0c;不再赘述 乘法 除法 算术结果溢出 自增和自减 关系运算与逻辑运算 JCC指…

What is an HTTP Flood DDoS attack?

HTTP 洪水攻击是一种针对 Web 和应用程序服务器的第 7 层分布式拒绝服务 &#xff08;DDoS&#xff09; 攻击。HTTP 洪水攻击通过使用 HTTP GET 或 HTTP POST 请求执行 DDoS 攻击。这些请求是有效的&#xff0c;并且针对可用资源&#xff0c;因此很难防范 HTTP 洪水攻击。 匿名…

你熟悉Docker吗?

你熟悉Docker吗&#xff1f; 文章目录 你熟悉Docker吗&#xff1f;快速入门Docker安装1.卸载旧版2.配置Docker的yum库3.安装Docker4.启动和校验5.配置镜像加速5.1.注册阿里云账号5.2.开通镜像服务5.3.配置镜像加速 部署MySQL镜像和容器命令解读 Docker基础常用命令数据卷数据卷…