银行账户转账异常
需求: 实现act-001账户向act-002账户转账10000,要求两个账户的余额一个减成功一个加成功,即执行的两条update语句必须同时成功或失败
实现步骤
第一步: 引入项目所需要的依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.powernode</groupId>
<artifactId>spring6-013-tx-bank</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<!--仓库-->
<repositories>
<!--spring里程碑版本的仓库-->
<repository>
<id>repository.spring.milestone</id>
<name>Spring Milestone Repository</name>
<url>https://repo.spring.io/milestone</url>
</repository>
</repositories>
<!--依赖-->
<dependencies>
<!--spring context,关联引入AOP-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>6.0.0-M2</version>
</dependency>
<!--spring jdbc(Spring框架的JdbcTemplate),关联引入了事务相关的依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>6.0.0-M2</version>
</dependency>
<!--mysql驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.30</version>
</dependency>
<!--德鲁伊连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.13</version>
</dependency>
<!--@Resource注解-->
<dependency>
<groupId>jakarta.annotation</groupId>
<artifactId>jakarta.annotation-api</artifactId>
<version>2.1.1</version>
</dependency>
<!--junit-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
</dependencies>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
</project>
第二步: 准备t_act
表并向表中插入两条账户记录act-001和act-002
第三步: 编写t_act
表对应的实体类
@Date
public class Account {
private String actno;
private Double balance;
}
第四步: 编写Dao(持久层)
中的AccountDao
接口及其实现类AccountDaoImpl
专门负责t_act
表的CRUD操作, 没有任何业务逻辑代码
public interface AccountDao {
// 根据账号查询余额
Account selectByActno(String actno);
// 更新账户信息
int update(Account act);
}
@Repository("accountDao")
public class AccountDaoImpl implements AccountDao {
@Resource(name = "jdbcTemplate")
private JdbcTemplate jdbcTemplate;
@Override
public Account selectByActno(String actno) {
// 根据账号查询账户的信息,并把查询结果封装到对应的实体类中
String sql = "select actno, balance from t_act where actno = ?";
Account account = jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<>(Account.class), actno);
return account;
}
@Override
public int update(Account act) {
// 根据账号更新账户的余额
String sql = "update t_act set balance = ? where actno = ?";
int count = jdbcTemplate.update(sql, act.getBalance(), act.getActno());
return count;
}
}
第五步: 编写Service(业务层)
中的AccountService
及其实现类AccountServiceImpl
专门负责关于账户的业务逻辑处理,如事务控制的相关代码
public interface AccountService {
// 转账方法
void transfer(String fromActno, String toActno, double money);
}
@Service("accountService")
public class AccountServiceImpl implements AccountService {
@Resource(name = "accountDao")
private AccountDao accountDao;
// 因为在这个方法中要完成所有的转账业务,所以需要控制事务
@Override
public void transfer(String fromActno, String toActno, double money) {
// 查询账户余额是否充足
Account fromAct = accountDao.selectByActno(fromActno);
if (fromAct.getBalance() < money) {
throw new RuntimeException("账户余额不足");
}
// 如果余额充足开始转账
Account toAct = accountDao.selectByActno(toActno);
// 将内存中两个对象的余额先修改
fromAct.setBalance(fromAct.getBalance() - money);
toAct.setBalance(toAct.getBalance() + money);
// 将数据库账户的余额更新
int count = accountDao.update(fromAct);
count += accountDao.update(toAct);
if (count != 2) {
throw new RuntimeException("转账失败,请联系银行");
}
}
}
第六步: 编写spring
的配置文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<!--组件扫描-->
<context:component-scan base-package="com.powernode.bank"/>
<!--配置数据源-->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/spring6"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</bean>
<!--配置jdbcTemplate-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"/>
</bean>
</beans>
第七步: 编写测试程序,模拟表示/控制层
处理用户的需求,后台调用对应业务层完成业务
public class BankTest {
@Test
public void testTransfer(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
AccountService accountService = applicationContext.getBean("accountService", AccountService.class);
try {
accountService.transfer("act-001", "act-002", 10000);
System.out.println("转账成功");
} catch (Exception e) {
e.printStackTrace();
}
}
}
第八步: 模拟转账异常,如果在更新两个账户余额的操作中出现了异常,此时就会出现前者的余额减了但后者的余额没有加上
@Service("accountService")
public class AccountServiceImpl implements AccountService {
@Resource(name = "accountDao")
private AccountDao accountDao;
@Override
public void transfer(String fromActno, String toActno, double money) {
// 查询账户余额是否充足
Account fromAct = accountDao.selectByActno(fromActno);
if (fromAct.getBalance() < money) {
throw new RuntimeException("账户余额不足");
}
// 余额充足,开始转账
Account toAct = accountDao.selectByActno(toActno);
fromAct.setBalance(fromAct.getBalance() - money);
toAct.setBalance(toAct.getBalance() + money);
int count = accountDao.update(fromAct);
// 模拟异常
String s = null;
s.toString();
count += accountDao.update(toAct);
if (count != 2) {
throw new RuntimeException("转账失败,请联系银行");
}
}
}
Spring事务的实现
编程式事务
编程式事务(了解)
,自己在业务方法中手写控制事务的代码
@Override
public void transfer(String fromActno, String toActno, double money) {
// 第一步开启事务
// 第二步执行核心业务逻辑
// 第三步如果执行核心业务流程中没有异常则提交事务
// 第四步如果执行核心业务流程中有异常则回滚事务
}
声明式事务
声明式事务(常用)
,基于注解方式
或XML配置方式
实现事务的控制
Spring事务管理的底层是基于AOP实现的,所以Spring专门针对事务开发了一套APIPlatformTransactionManager(事务管理器的核心接口)
并且有两个实现类
实现类 | 描述 |
---|---|
DataSourceTransactionManager | 支持JdbcTemplate、MyBatis、Hibernate 等事务管理 |
JtaTransactionManager | 支持分布式事务管理 |
第一步: 在spring配置文件
中引入tx
的命名空间及其约束文件,spring-jdbc
依赖中关联了事务相关的依赖
第二步:在spring配置文件中配置事务管理器
,如果在Spring6中使用JdbcTemplate
就要使用DataSourceTransactionManager
事务管理器来管理事务
- 由于事务管理器DataSourceTransactionManager底层利用的还是
Connection连接
连接对象开启关闭事务,所以需要给事务管理器配置数据源
第三步: 在spring配置文件中开启事务注解驱动器
告诉Spring框架采用注解的方式控制事务
- 事务管理器就是我们的切面类,
@Transactional
注解所标识的方法就是连接点(可以织入切面的位置)
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
<!--组件扫描-->
<context:component-scan base-package="com.powernode.bank"/>
<!--配置数据源-->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/spring6"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</bean>
<!--配置jdbcTemplate-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"/>
</bean>
<!--配置事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!--开启事务注解驱动器告诉Spring框架采用注解的方式控制事务-->
<tx:annotation-driven transaction-manager="transactionManager"/>
</beans>
第四步: 在Service层中的业务类上或业务方法上添加@Transactional
注解,表示当前类/方法开启了事务,业务方法中批量的DML操作可以保证同时成功或失败
- 在业务类上添加注解: 表示该类中所有的方法都开启事务
- 在业务方法上添加注解: 表示只有当前方法开启事务
@Service("accountService")
@Transactional// 开启事务
public class AccountServiceImpl implements AccountService {
@Resource(name = "accountDao")
private AccountDao accountDao;
@Override
public void transfer(String fromActno, String toActno, double money) {
// 查询账户余额是否充足
Account fromAct = accountDao.selectByActno(fromActno);
if (fromAct.getBalance() < money) {
throw new RuntimeException("账户余额不足");
}
// 余额充足,开始转账
Account toAct = accountDao.selectByActno(toActno);
fromAct.setBalance(fromAct.getBalance() - money);
toAct.setBalance(toAct.getBalance() + money);
int count = accountDao.update(fromAct);
// 模拟异常
String s = null;
s.toString();
count += accountDao.update(toAct);
if (count != 2) {
throw new RuntimeException("转账失败,请联系银行");
}
}
}
事务的属性
Transactional
注解的源码
public interface Transactional{
@AliasFor("transactionManager")
String value() default "";
@ATiasFor("value")
String transactionManager() default"";
String[] label() default {};
// 事务传播行为
Propagation propagation() default Propagation.REOUIRED;
// 事务隔离级别
Isolation isolation() default Isolation.DEFAULT;
// 事务超时时间 , 默认-1 , 即没有时间限制
int timeout() default -1;
String timeoutString() default "";
// 只读事务(不能出现增删改的DML语句)
boolean readOnly() default false;
// 设置出现哪些异常回滚事务
class<? extends Throwable>[] rollbackFor() default {};
String[] rollbackForclassName() default {};
// 设置出现哪些异常不回滚事务
class<? extends Throwable>[] noRollbackFor() default {};
String[] noRolbackForclassName() default {};
}
属性名 | 功能 |
---|---|
Propagation(枚举类型) | 设置事务传播行为 |
Isolation(枚举类型) | 设置事务隔离级别 |
timeout(默认值-1) | 设置事务超时时间(以最后一条DML语句执行所耗的时间为准) , 默认值是-1 表示没有时间限制 |
readOnly(默认fassle) | 设置事务是否是只读事务,只读事务内只能出现查询语句,执行DML(增删改)语句时会报错,该特性可以提供查询的效率 |
rollbackFor | 设置出现哪些异常后回滚事务 |
noRollbackFor | 设置出现哪些异常后不回滚事务 |
事务传播行为
在XxxService类中的a()方法和b()方法都有事务,若a()方法在执行过程中调用了b()方法,此时就会出现事务传递
的行为,所以我们要对这个行为设定一个处理方式
事务传播行为属性Propagation
在Spring框架中被定义为枚举类型,一共有七种传播行为
属性值 | 功能 |
---|---|
REQUIRED(默认) | 支持当前所在的事务,如果当前没有开启事务就新建一个 |
SUPPORTS | 支持当前所在的事务,如果当前没有开启事务,就以非事务方式 执行DML操作 |
MANDATORY | 支持当前所在的事务,如果当前没有开启事务将抛出一个异常 |
REQUIRES_NEW | 直接开启一个新的事务 ,如果当前也开启了事务则将当前事务挂起(暂停) |
NOT_SUPPORTED | 支持以非事务方式 执行DML操作,如果当前开启了事务则将当前事务挂起(暂停) |
NEVER | 支持以非事务方式执行DML操作,如果当前开启了事务则抛出异常 |
NESTED | 如果当前开启了事务,就在当前事务里再嵌套一个完全独立的事务,嵌套的事务可以独立于外层事务进行提交或回滚 如果当前事务不存在,就像REQUIRED一样新建了一个事务,因为嵌套事务的外层事务不存在 |
实现: 在AccountServiceImpl1
中的声明了事务的方法调用AccountServiceImpl2
中声明了事务传播行为的方法
第一步: 编写Dao接口及其实现类
public interface AccountDao {
// 保存账户信息
int insert(Account act);
}
@Repository("accountDao")
public class AccountDaoImpl implements AccountDao {
@Resource(name = "jdbcTemplate")
private JdbcTemplate jdbcTemplate;
@Override
public int insert(Account act) {
String sql = "insert into t_act values(?,?)";
return jdbcTemplate.update(sql, act.getActno(), act.getBalance());
}
}
第二步: 编写Service接口及其实现类AccountServiceImpl1
和AccountServiceImpl2
,在业务类1中的业务方法调用业务类2中的业务方法
REQUIRES
: 业务2处于业务1的事务中,即业务1和2在同一个事务中,若2出现了异常需要事务回滚那么1肯定也要回滚(1和2是同一个事务,1捕捉异常也没用)REQUIRES_NEW
: 业务类2处于自己的事务中,业务类1的事务会被挂起,若2发生了异常事务肯定回滚,但若1捕捉了2的异常那么1的事务就不会回滚(两个事务)
public interface AccountService {
// 保存账户信息方法
void save(Account act);
}
@Service("accountService")
public class AccountServiceImpl1 implements AccountService {
@Resource(name = "accountDao")
private AccountDao accountDao;
@Resource(name = "accountService2")
private AccountService accountService;
@Override
@Transactional(propagation = Propagation.REQUIRED)
public void save(Account act) {
// 调用accountDao的insert方法保存act-003账户信息
accountDao.insert(act);
// 创建账户对象
Account act2 = new Account("act-004", 1000.0);
// 如果业务类1捕捉到了业务类2的save方法出现的异常,当前事务就不会回滚,因为业务1和业务2就不是同一个事务
try {
// 调用AccountServiceImpl2的save方法保存act-004账户信息
accountService.save(act2);
} catch (Exception e) {
}
// 业务类1如果捕捉到了业务类2的save方法出现的异常,所以后续的DML操作可以继续执行...........
}
}
@Service("accountService2")
public class AccountServiceImpl2 implements AccountService {
@Resource(name = "accountDao")
private AccountDao accountDao;
@Override
// 直接开启一个新的事务,如果当前也开启了事务则将当前事务挂起(暂停)
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void save(Account act) {
// 调用accountDao的insert方法保存act-004账户信息
accountDao.insert(act);
// 模拟出现异常,虽然事务还没有结束,但由于出现了异常,当前事务就会回滚那么后续的其他DML语句就无法执行到
String s = null;
s.toString();
// ...............
}
}
第三步: 集成Log4j2
日志框架(引入依赖和配置文件
),把警告级别改成DEBUG级别
,在日志信息中查看事务对象的创建情况
@Test
public void testPropagation() {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
// 获取1号service对象
AccountService accountService = applicationContext.getBean("accountService", AccountService.class);
Account act = new Account("act-003", 1000.0);
accountService.save(act);
}
事务隔离级别
事务隔离级别属性Isolation
在Spring框架中被定义为枚举类型,一共四种隔离级别
隔离级别 | 描述 | 脏读 | 不可重复读 | 幻读 | 加锁读 |
---|---|---|---|---|---|
读未提交(理论级别)(READ_UNCOMMITTED) | 在一个事务中可以看到其他事务未提交的修改数据 ,大多数的数据库隔离级别都是二档起步 | 有 | 有 | 有 | 不加锁 |
读已提交 (Oracle默认级别) (READ_COMMITTED) | 在一个事务中只能看到其他事务已经提交的修改数据 ,这种隔离级别每次读到的都是真实数据 | 无 | 有 | 有 | 不加锁 |
可重复读(MySQL默认级别) (REPEATABLE_READ) | 一个事务中多次执行相同的SELECT语句得到的是相同的结果,永远读取的都是自己刚开启事务时的数据 ,不管其他事务是否提交了修改数据 | 无 | 无 | 有 | 不加锁 |
序列化(最高隔离级别) SERIALIZABLE | 一个事务与其他事务完全地隔离,每一次读取到的数据都是最真实的,但是所有事务只能排队执行,不支持并发所以效率最低 | 无 | 无 | 无 | 加锁 |
第一步: 编写dao层接口及其实现类
public interface AccountDao {
// 根据账号查询账户信息
Account selectByActno(String actno);
// 保存账户信息
int insert(Account act);
}
@Repository("accountDao")
public class AccountDaoImpl implements AccountDao {
@Resource(name = "jdbcTemplate")
private JdbcTemplate jdbcTemplate;
@Override
public Account selectByActno(String actno) {
String sql = "select actno, balance from t_act where actno = ?";
Account account = jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<>(Account.class), actno);
return account;
}
@Override
public int insert(Account act) {
String sql = "insert into t_act values(?,?)";
return jdbcTemplate.update(sql, act.getActno(), act.getBalance());
}
}
第二步: 编写Service层接口及其实现类IsolationService1
和IsolationService2
,业务类1负责查询账户,业务类2负责插入账户同时模拟延迟
READ_UNCOMMITTED
: 表示当前事务可以读取到其他的事务没有提交的数据
,没有提交就代表事务可能回滚将来数据可能不存在READ_COMMITTED
: 在一个事务中只能看到其他事务已经提交的修改数据
读不到则报异常,这种隔离级别每次读到的都是真实数据
public interface AccountService {
// 根据账号查询账户信息
void getByActno(String actno);
// 保存账户信息方法
void save(Account act);
}
@Service("i1")
public class IsolationService1 {
@Resource(name = "accountDao")
private AccountDao accountDao;
// 当前事务可以读取到其他事务没有提交的数据
//@Transactional(isolation = Isolation.READ_UNCOMMITTED)
// 当前事务只能看到其他事务已经提交的修改数据读不到则报异常
@Transactional(isolation = Isolation.READ_COMMITTED)
public void getByActno(String actno) {
Account account = accountDao.selectByActno(actno);
System.out.println("查询到的账户信息:" + account);
}
}
@Service("i2")
public class IsolationService2 {
@Resource(name = "accountDao")
private AccountDao accountDao;
@Transactional
public void save(Account act) {
accountDao.insert(act);
// 睡眠一会让当前事务晚点提交,在睡眠期间看其他事务能否查到当前事务未提交的数据
try {
Thread.sleep(1000 * 20);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
测试先执行业务类2的save方法保存一条账户数据,然后看业务类1中是否能查询到业务类2保存的账户信息
// 先调用业务类2的save方法先保存一条账户数据
@Test
public void testIsolation2(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
IsolationService2 i2 = applicationContext.getBean("i2", IsolationService2.class);
Account act = new Account("act-004", 1000.0);
i2.save(act);
}
// 在业务类2的save方法事务还没有提交的期间内,查看业务类2保存的账户信息
@Test
public void testIsolation1(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
IsolationService1 i1 = applicationContext.getBean("i1", IsolationService1.class);
i1.getByActno("act-004");
}
事务超时
设置事务的超时时间后(默认没有时间限制),当如果该事务中执行最后一条DML语句时一共所耗的时间
已经超过了规定的时间事务会选择回滚
// -1是默认值,表示执行事务没有时间限制
@Transactional(timeout = -1)
// 表示超过10秒如果该事务中所有的DML语句还没有执行完毕的话事务会选择回滚
@Transactional(timeout = 10)
事务的超时时间是指执行最后一条DML语句时一共所耗的时间,最后一条DML语句后面的业务代码执行所耗的时间不会被计入超时时间内
@Transactional(timeout = 10) // 设置事务超时时间为10秒。
public void save(Account act) {
accountDao.insert(act);
// 这段睡眠时间是不会被计入事务的超时时间内的
try {
Thread.sleep(1000 * 15);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
如果想让整个方法的所有代码都计入超时时间的话,可以在方法最后一行添加一行无关紧要的DML语句
@Transactional(timeout = 10) // 设置事务超时时间为10秒。
public void save(Account act) {
// 这段睡眠时间是会被计入事务的超时时间内的
try {
Thread.sleep(1000 * 15);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 该DML语句之前执行所耗的时间也会被计入事务的超时时间内
accountDao.insert(act);
}
只读事务
只读事务表示在当前事务执行过程中只允许select语句
执行,执行DML(增删改)语句时会报错
- 虽然执行select语句可以不用加事务控制,但若该事务中确实没有增删改操作设置为只读事务后可以
启动Spring的优化策略从而提高select语句的执行效率
// 将当前事务设置为只读事务,默认值为false
@Transactional(readOnly = true)
异常回滚事务
设置只有发生Xxx异常
或该异常的子类异常
时事务才会回滚,发生其他异常时事务都不会回滚
// 只有发生RuntimeException异常或该异常的子类异常时事务才会回滚
@Transactional(rollbackFor = RuntimeException.class)
异常不回滚事务
设置只有发生Xxx异常
或该异常的子类异常
时事务才不回滚,发生其他异常时事务都会回滚
// 只有发生NullPointerException异常或该异常的子类异常时事务才不回滚
@Transactional(noRollbackFor = NullPointerException.class)