十三、Spring对事务的支持
迪丽瓦拉
2024-05-29 17:57:01
0

1 事务概述

  • 什么是事务
    • 在一个业务流程当中,通常需要多条DML(insert delete update)语句共同联合才能完成,这多条DML语句必须同时成功,或者同时失败,这样才能保证数据的安全。
    • 多条DML要么同时成功,要么同时失败,这叫做事务。
    • 事务:Transaction(tx)
  • 事务的四个处理过程:
    • 第一步:开启事务 (start transaction)
    • 第二步:执行核心业务代码
    • 第三步:提交事务(如果核心业务处理过程中没有出现异常)(commit transaction)
    • 第四步:回滚事务(如果核心业务处理过程中出现异常)(rollback transaction)
  • 事务的四个特性:
    • A 原子性:事务是最小的工作单元,不可再分。
    • C 一致性:事务要求要么同时成功,要么同时失败。事务前和事务后的总量不变。
    • I 隔离性:事务和事务之间因为有隔离性,才可以保证互不干扰。
    • D 持久性:持久性是事务结束的标志。

2 Spring对事务的支持


Spring实现事务的两种方式

  • 编程式事务
    • 通过编写代码的方式来实现事务的管理。
  • 声明式事务
    • 基于注解方式
    • 基于XML配置方式

Spring事务管理API

Spring对事务的管理底层实现方式是基于AOP实现的。采用AOP的方式进行了封装。所以Spring专门针对事务开发了一套API,API的核心接口如下:
在这里插入图片描述
PlatformTransactionManager接口:spring事务管理器的核心接口。在Spring6中它有两个实现:

  • DataSourceTransactionManager:支持JdbcTemplate、MyBatis、Hibernate等事务管理。
  • JtaTransactionManager:支持分布式事务管理。

声明式事务之注解实现方式

以银行账户转账为例学习事务。两个账户act-001和act-002。act-001账户向act-002账户转账10000,必须同时成功,或者同时失败。(一个减成功,一个加成功, 这两条update语句必须同时成功,或同时失败。)
连接数据库的技术采用Spring框架的JdbcTemplate。
采用三层架构搭建:
在这里插入图片描述

第一步:准备数据库表

在这里插入图片描述
在这里插入图片描述

第二步:引入依赖

4.0.0org.examplespring6-010-tx-bank1.0-SNAPSHOTjarorg.springframeworkspring-context6.0.2org.springframeworkspring-jdbc6.0.2mysqlmysql-connector-java8.0.30com.alibabadruid1.2.13jakarta.annotationjakarta.annotation-api2.1.1junitjunit4.13.2test1717
第三步:创建包结构

com.powernode.bank.pojo
com.powernode.bank.service
com.powernode.bank.service.impl
com.powernode.bank.dao
com.powernode.bank.dao.impl

第四步:准备POJO类
package com.powernode.bank.pojo;/*** 银行账户类*/
public class Account {private String actno;private Double balance;// 省略构造方法,get、set方法,toString方法
第五步:编写持久层
package com.powernode.bank.dao;import com.powernode.bank.pojo.Account;/*** 专门负责账户信息的CRUD操作* DAO中只执行SQL语句,没有任何业务逻辑*/
public interface AccountDao {/*** 根据账号查询账户信息* @param actno* @return*/Account selectByActno(String actno);/*** 更新账户信息* @param act* @return*/int update(Account act);
}
package com.powernode.bank.dao.impl;import com.powernode.bank.dao.AccountDao;
import com.powernode.bank.pojo.Account;
import jakarta.annotation.Resource;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;@Repository("accountDao")
public class AccountDaoImpl implements AccountDao {@Resource(name = "jdbcTemplate")private JdbcTemplate jdbcTemplate;@Overridepublic 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;}@Overridepublic int update(Account act) {String sql = "update t_act set balance = ? where actno = ?";int count = jdbcTemplate.update(sql, act.getBalance(), act.getActno());return count;}
}
第六步:编写业务层
package com.powernode.bank.service;/*** 业务接口* 事务是在这个接口下控制的*/
public interface AccountService {/*** 转账业务方法* @param fromActno 转出账户* @param toActno 转入账户* @param money 转账金额*/void transfer(String fromActno, String toActno, double money);
}

在service类上或方法上添加@Transactional注解
在类上添加该注解,该类中所有的方法都有事务。在某个方法上添加该注解,表示只有这个方法使用事务。

package com.powernode.bank.service.impl;import com.powernode.bank.dao.AccountDao;
import com.powernode.bank.pojo.Account;
import com.powernode.bank.service.AccountService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;@Service("accountService")
@Transactional
public class AccountServiceImpl implements AccountService {@Resource(name = "accountDao")private AccountDao accountDao;@Override// 控制事务//@Transactionalpublic 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配置文件

在spring配置文件中配置事务管理器。
在spring配置文件中引入tx命名空间。
在spring配置文件中配置“事务注解驱动器”,开始注解的方式控制事务。




第八步:编写表示层(测试程序)
package com.powernode.bank.test;import com.powernode.bank.service.AccountService;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;public class SpringTXTest {@Testpublic void testSpringTX(){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();}}
}

在这里插入图片描述
数据变化:
在这里插入图片描述
模拟异常

在这里插入图片描述
在这里插入图片描述

事务属性

事务中的重点属性:

  • 事务传播行为
    Propagation propagation() default Propagation.REQUIRED;
  • 事务隔离级别
    Isolation isolation() default Isolation.DEFAULT;
  • 事务超时
    int timeout() default -1;
  • 只读事务
    boolean readOnly() default false;
  • 设置出现哪些异常回滚事务
    Class[] rollbackFor() default {};
  • 设置出现哪些异常不回滚事务
    Class[] noRollbackFor() default {};
事务传播行为

什么是事务的传播行为?

在service类中有a()方法和b()方法,a()方法上有事务,b()方法上也有事务,当a()方法执行过程中调用了b()方法,事务是如何传递的?合并到一个事务里?还是开启一个新的事务?这就是事务传播行为。

事务传播行为在spring框架中被定义为枚举类型:

public enum Propagation {REQUIRED(0),SUPPORTS(1),MANDATORY(2),REQUIRES_NEW(3),NOT_SUPPORTED(4),NEVER(5),NESTED(6);

一共有七种传播行为:

  • REQUIRED:支持当前事务,如果不存在就新建一个(默认)【没有就新建,有就加入
  • SUPPORTS:支持当前事务,如果当前没有事务,就以非事务方式执行【有就加入,没有就不管了
  • MANDATORY:必须运行在一个事务中,如果当前没有事务正在发生,将抛出一个异常【有就加入,没有就抛异常
  • REQUIRES_NEW:开启一个新的事务,如果一个事务已经存在,则将这个存在的事务挂起【不管有没有,直接开启一个新事务,开启的新事务和之前的事务不存在嵌套关系,之前事务被挂起
  • NOT_SUPPORTED:以非事务方式运行,如果有事务存在,挂起当前事务【不支持事务,存在就挂起
  • NEVER:以非事务方式运行,如果有事务存在,抛出异常【不支持事务,存在就抛异常
  • NESTED:如果当前正有一个事务在进行中,则该方法应当运行在一个嵌套式事务中。被嵌套的事务可以独立于外层事务进行提交或回滚。如果外层事务不存在,行为就像REQUIRED一样。【有事务的话,就在这个事务里再嵌套一个完全独立的事务,嵌套的事务可以独立的提交和回滚。没有事务就和REQUIRED一样。
@Transactional(propagation = Propagation.REQUIRED)
事务隔离级别

事务隔离级别类似于教室A和教室B之间的那道墙,隔离级别越高表示墙体越厚。隔音效果越好。

数据库中读取数据存在的三大问题:(三大读问题)

  • 脏读:读取到没有提交到数据库的数据,叫做脏读。
  • 不可重复读:在同一个事务当中,第一次和第二次读取的数据不一样。
  • 幻读:读到的数据是假的。

事务隔离级别包括四个级别:

  • 读未提交:READ_UNCOMMITTED
    • 这种隔离级别,存在脏读问题,所谓的脏读(dirty read)表示能够读取到其它事务未提交的数据。
  • 读提交:READ_COMMITTED
    • 解决了脏读问题,其它事务提交之后才能读到,但存在不可重复读问题。
  • 可重复读:REPEATABLE_READ
    • 解决了不可重复读,可以达到可重复读效果,只要当前事务不结束,读取到的数据一直都是一样的。但存在幻读问题。
  • 序列化:SERIALIZABLE
    • 解决了幻读问题,事务排队执行。不支持并发。
隔离级别脏读不可重复读幻读
读未提交
读提交
可重复读
序列化

在Spring代码中如何设置隔离级别?
隔离级别在spring中以枚举类型存在:

public enum Isolation {DEFAULT(-1),READ_UNCOMMITTED(1),READ_COMMITTED(2),REPEATABLE_READ(4),SERIALIZABLE(8);
@Transactional(isolation = Isolation.READ_COMMITTED)
事务超时
@Transactional(timeout = 10)

以上代码表示设置事务的超时时间为10秒。

表示超过10秒如果该事务中所有的DML语句还没有执行完毕的话,最终结果会选择回滚。

默认值-1,表示没有时间限制。

这里有个坑,事务的超时时间指的是哪段时间?

在当前事务当中,最后一条DML语句执行之前的时间。如果最后一条DML语句后面很有很多业务逻辑,这些业务代码执行的时间不被计入超时时间。

以下代码的超时不会被计入超时时间

@Transactional(timeout = 10) // 设置事务超时时间为10秒。
public void save(Account act) {accountDao.insert(act);// 睡眠一会try {Thread.sleep(1000 * 15);} catch (InterruptedException e) {e.printStackTrace();}
}

以下代码超时时间会被计入超时时间

@Transactional(timeout = 10) // 设置事务超时时间为10秒。
public void save(Account act) {// 睡眠一会try {Thread.sleep(1000 * 15);} catch (InterruptedException e) {e.printStackTrace();}accountDao.insert(act);
}

如果想让整个方法的所有代码都计入超时时间的话,可以在方法最后一行添加一行无关紧要的DML语句。


只读事务
@Transactional(readOnly = true)

将当前事务设置为只读事务,在该事务执行过程中只允许select语句执行,delete insert update均不可执行。

该特性的作用是:启动spring的优化策略。提高select语句执行效率。

如果该事务中确实没有增删改操作,建议设置为只读事务。

设置哪些异常回滚事务
@Transactional(rollbackFor = RuntimeException.class)

表示只有发生RuntimeException异常或该异常的子类异常才回滚。

设置哪些异常不回滚事务
@Transactional(noRollbackFor = NullPointerException.class)

表示发生NullPointerException或该异常的子类异常不回滚,其他异常则回滚。

事务的全注解式开发

编写一个类来代替配置文件

package com.powernode.bank;import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;import javax.sql.DataSource;// 代替spring.xml文件 在这个类中完成配置
@Configuration
// 组件扫描
@ComponentScan("com.powernode.bank")
// 开启事务注解
@EnableTransactionManagement
public class Spring6Config {// Spring框架,看到这个@Bean注解后,会调用这个被标注的方法,这个方法的返回值是一个java对象,这个java对象会自动纳入IoC容器管理。// 返回的对象就是Spring容器当中的一个Bean了。// 并且这个bean的名字是:dataSource@Bean(name = "dataSource")public DruidDataSource getDataSource(){DruidDataSource dataSource = new DruidDataSource();dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");dataSource.setUrl("jdbc:mysql://localhost:3306/spring6");dataSource.setUsername("root");dataSource.setPassword("root");return dataSource;}@Bean(name = "jdbcTemplate")//Spring在调用这个方法的时候会自动给我们传递过来一个dataSource对象。public JdbcTemplate getJdbcTemplate(DataSource dataSource){JdbcTemplate jdbcTemplate = new JdbcTemplate();jdbcTemplate.setDataSource(dataSource);return jdbcTemplate;}@Bean(name = "txManager")public DataSourceTransactionManager getDataSourceTransactionManager(DataSource dataSource){DataSourceTransactionManager txManager = new DataSourceTransactionManager();txManager.setDataSource(dataSource);return txManager;}
}

测试程序

@Test
public void testNoXML(){ApplicationContext applicationContext = new AnnotationConfigApplicationContext(Spring6Config.class);AccountService accountService = applicationContext.getBean("accountService", AccountService.class);try{accountService.transfer("act-001","act-002",10000);System.out.println("转账成功");}catch (Exception e){e.printStackTrace();}
}

声明式事务之XML实现方式

添加aspectj的依赖


org.springframeworkspring-aspects6.0.2

Spring配置文件




相关内容