文章目录
- 前言
- 六、Spring事务
- 1.Spring事务简介
- 2.入门案例--模拟银行间转账业务
- 3.开启Spring事务的一般步骤
- 4.Spring事务角色
- 5.spring事务属性--rollbackfor
- 6.入门案例进阶--转账业务追加日志
- 7. Spring事务属性--事务传播行为
- 总结
前言
为了巩固所学的知识,作者尝试着开始发布一些学习笔记类的博客,方便日后回顾。当然,如果能帮到一些萌新进行新技术的学习那也是极好的。作者菜菜一枚,文章中如果有记录错误,欢迎读者朋友们批评指正。
(博客的参考源码可以在我主页的资源里找到,如果在学习的过程中有什么疑问欢迎大家在评论区向我提出)
六、Spring事务
1.Spring事务简介
一个数据库事务是一个被视为单一的工作单元的操作序列。这些操作应该要么完整地执行,要么完全不执行。事务管理是一个重要组成部分,RDBMS 面向企业应用程序,以确保数据完整性和一致性。事务的概念可以描述为具有以下四个关键属性说成是 ACID:
原子性:事务应该当作一个单独单元的操作,这意味着整个序列操作要么是成功,要么是失败的。
一致性:这表示数据库的引用完整性的一致性,表中唯一的主键等。
隔离性:可能同时处理很多有相同的数据集的事务,每个事务应该与其他事务隔离,以防止数据损坏。
持久性:一个事务一旦完成全部操作后,这个事务的结果必须是永久性的,不能因系统故障而从数据库中删除。
一个真正的 RDBMS 数据库系统将为每个事务保证所有的四个属性。使用 SQL 发布到数据库中的事务的简单视图如下:
使用 begin transaction 命令开始事务。
使用 SQL 查询语句执行各种删除、更新或插入操作。
如果所有的操作都成功,则执行提交操作,否则回滚所有操作。
Spring 框架在不同的底层事务管理 APIs 的顶部提供了一个抽象层。Spring 的事务支持旨在通过添加事务能力到 POJOs 来提供给 EJB 事务一个选择方案。Spring 支持编程式和声明式事务管理。EJBs 需要一个应用程序服务器,但 Spring 事务管理可以在不需要应用程序服务器的情况下实现,实现接口及实现类如下:
2.入门案例–模拟银行间转账业务
- 案例需求以及实现思路
- 环境准备
- 创建数据库表tbl_account
建表查询语句示例
create database spring_db;
use spring_db;
drop table if exists tbl_account;
create table tbl_account(
id int primary key auto_increment,
name varchar(20),
money varchar(20)
);
INSERT INTO tbl_account VALUES (1, 'Tom', 1000);
INSERT INTO tbl_account VALUES (2, 'Jerry', 1000);
效果
- 在pom.xml文件中导入坐标
<dependencies>
//spring框架
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
//druid数据源
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.16</version>
</dependency>
//mybatisa框架
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.6</version>
</dependency>
//数据库连接
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
//spring操作jdbc
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
//spring整合mybatis
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>1.3.0</version>
</dependency>
//spring整合junit单元测试
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
//单元测试
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
</dependencies>
- 创建spring全局配置文件SpringConfig
@Configuration
@ComponentScan("com.itheima")
@PropertySource("classpath:jdbc.properties")
@Import({JdbcConfig.class,MybatisConfig.class})
//开启注解式事务驱动
@EnableTransactionManagement
public class SpringConfig {
}
- 创建数据库信息文件jdbc.properties,填写数据库连接信息
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/spring_db?useSSL=false
jdbc.username=root
jdbc.password=root
- 创建数据库连接配置类JdbcConfig
public class JdbcConfig {
@Value("${jdbc.driver}")
private String driver;
@Value("${jdbc.url}")
private String url;
@Value("${jdbc.username}")
private String userName;
@Value("${jdbc.password}")
private String password;
@Bean
public DataSource dataSource(){
DruidDataSource ds = new DruidDataSource();
ds.setDriverClassName(driver);
ds.setUrl(url);
ds.setUsername(userName);
ds.setPassword(password);
return ds;
}
//配置事务管理器,mybatis使用的是jdbc事务
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource){
DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
transactionManager.setDataSource(dataSource);
return transactionManager;
}
}
- 创建mybatis创建SqlSessionFactory Bean 和 Mapper 代理开发映射对象配置文件MybatisConfig
public class MybatisConfig {
@Bean
public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource){
SqlSessionFactoryBean ssfb = new SqlSessionFactoryBean();
ssfb.setTypeAliasesPackage("org.example.domain");
ssfb.setDataSource(dataSource);
return ssfb;
}
@Bean
public MapperScannerConfigurer mapperScannerConfigurer(){
MapperScannerConfigurer msc = new MapperScannerConfigurer();
msc.setBasePackage("org.example.dao");
return msc;
}
}
- 根据数据库表创建对应的实体类Account
public class Account{
//此处省略getter、setter和toString方法
private Integer id;
private String name;
private Double money;
}
- 创建AccontDao接口,用注解的方式定义mapper代理开发业务接口,分别为加钱的操作方法 inmoney 和 减钱的操作 outmoney
public interface AccountDao {
@Update("update tbl_account set money = money + #{money} where name = #{name}")
void inMoney(@Param("name") String name, @Param("money") Double money);
@Update("update tbl_account set money = money - #{money} where name = #{name}")
void outMoney(@Param("name") String name, @Param("money") Double money);
}
- 创建业务层接口AccountService,定义转账操作方法transfer
public interface AccountService {
/**
* 转账操作
* @param out 传出方
* @param in 转入方
* @param money 金额
*/
//配置当前接口方法具有事务
@Transactional
public void transfer(String out,String in ,Double money) ;
}
- 创建业务层接口 AccountService 的实现类AccountServiceImpl,调用数据层功能的加钱和减钱方法 inmoney 和 outmoney
@Service
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountDao accountDao;
public void transfer(String out,String in ,Double money) {
//减钱
accountDao.outMoney(out,money);
//加钱
accountDao.inMoney(in,money);
}
}
- 编写测试类
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfig.class)
public class AccountServiceTest {
@Autowired
private AccountService accountService;
@Test
public void testTransfer() throws IOException {
//Tom 向 Jerry 转100元
accountService.transfer("Tom","Jerry",100D);
}
}
- 程序运行结果
- 文件结构参考
- 入门案例分析
-
入门案例完成了简单的模拟转账操作,此时我们在转账接口的实现类AccountServiceImpl减钱和加钱的操作中间添加一行模拟业务运行异常的代码,此时我们希望业务异常,终止操作,即数据库的账户余额不变。
-
添加了模拟异常的代码及运行结果
- 通过观察数据库中的表格数据我们发现异常前面的减钱操作运行了,而异常后面的加钱操作没有运行,导致整体业务功能失败。我们可以通过开启Spring事务来解决这个问题。
- 小结
- Spring注解式事务通常添加在业务层接口中而不会添加到业务层实现类中,降低耦合
- 注解式事务可以添加到业务方法上表示当前方法开启事务,也可以添加到接口上表示当前接口所有方法开启事务
- 事务管理器需要根据实现技术进行选择,mybatis使用的是jdbc事务
3.开启Spring事务的一般步骤
- @Transactional 注解配置对应的接口方法具有事务
@Transactional
public void transfer(String out,String in ,Double money) ;
}
- 配置事务管理器,mybatis使用的是jdbc事务
public class JdbcConfig {
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource){
DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
transactionManager.setDataSource(dataSource);
return transactionManager;
}
}
- @EnableTransactionManagement 注解开启注解式事务驱动
@EnableTransactionManagement
public class SpringConfig {
}
此时在存在模拟异常的情况下再次运行模拟测试类,数据库中的数据不变,业务功能运行正常,Spring事务开启成功。
4.Spring事务角色
- Spring事务角色分为事务管理员和事务协调员。
-
事务管理员:发起事务方,在Spring中通常指代业务层开启事务的方法
-
事务协调员:加入事务方,在Spring中通常指代数据层方法,也可以是业务层方法
- 结合入门案例分析
-
在入门案例中,没加@Transactional注解开启事务的情况下,减钱的outmoney方法、加钱的方法inmoney以及transfer接口都会各自开启一个事务,各自独立。当运行加钱事务出现异常的时候,减钱操作并不会加入加钱操作的事务回滚中,导致整个业务运行失败。
-
当只用@Transactional注解开启事务后,事务减钱事务TI与加钱事务T2都加入事务T,成为一个事务,这样当事务中的加钱事务部分出现问题时,整个事务回滚,程序终止运行,整体业务正常运行成功。
5.spring事务属性–rollbackfor
- 属性简介
属性 | 作用 | 示例 |
readonly | 设置是否为只读事务 | readOnly=true 只读事务 |
timeout | 设置事务超时时间 | timeout=-1(永不超时) |
rollbackfor | 设置事务回滚异常(class) | {NullPointException.class rollbackFor= |
rollbackForClassName | 设置事务回滚异常(string) | 同上格式为字符串 |
noRollbackFor | 设置事务不回滚异常(class) | noRollbackFor= (NullPointException.class} |
noRollbackForClassName | 设置事务不回滚异常(string) | 同上格式为字符串 |
propagation | 设置事务传播行为 | … |
- 添加事务属性格式举例
public interface AccountService {
/**
* 转账操作
* @param out 传出方
* @param in 转入方
* @param money 金额
*/
//配置当前接口方法具有事务
@Transactional(readOnly = true,timeout = -1)
public void transfer(String out,String in ,Double money) ;
}
- rollbackfor 属性介绍
- 在业务层实现类中抛出一个IOException异常
@Service
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountDao accountDao;
public void transfer(String out,String in ,Double money) throws IOException {
accountDao.outMoney(out,money);
//模拟抛出IOException异常
if(true){throw new IOException();}
accountDao.inMoney(in,money);
}
}
- 运行模拟测试类
- 观察数据库表我们发现,开启事务后在遇到抛IOException类型的异常时,事务不会主动回滚,导致整体业务失败,这时候需要主动去配置。
public interface AccountService {
/**
* 转账操作
* @param out 传出方
* @param in 转入方
* @param money 金额
*/
//配置当前接口方法具有事务
@Transactional(rollbackFor = {IOException.class})
public void transfer(String out,String in ,Double money) throws IOException;
}
此时再次运行模拟测试类,当检测到IOExctpition类型的异常时,事务T回滚,数据库表中的数据不变。
- 开启Spring事务只回滚两种事务:ERROR系列(如内存溢出) 和 运行时异常(如NO Point Exception)。其余的均需要通过rollbackfor属性手动配置。
6.入门案例进阶–转账业务追加日志
- 案例需求及实现思路
- 编码环境准备
- 创建数据库表tbl_log
sql建表语句
use spring_db;
drop table if exists tbl_log;
create table tbl_log(
id int primary key auto_increment,
info varchar(100),
createDate varchar(20)
);
INSERT INTO tbl_log VALUES (1, '转账操作由Tom到Jerry,金额:100.0', '20:55:49');
表格示例
- LogDao数据层接口
public interface LogDao {
@Insert("insert into tbl_log (info,createDate) values(#{info},now())")
void log(String info);
}
- LogService 业务层接口
public interface LogService {
@Transactional
void log(String out, String in, Double money);
}
- LogService 业务层接口实现类
@Service
public class LogServiceImpl implements LogService {
@Autowired
private LogDao logDao;
public void log(String out,String in,Double money ) {
logDao.log("转账操作由"+out+"到"+in+",金额:"+money);
}
}
-
将IOException相关的代码注释掉,保留最基础的Spring事务代码
-
文件结构示例
- 用try…finally结构编写业务层实现类AccountServiceImpl 并运行测试类
@Service
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountDao accountDao;
@Autowired
private LogService logService;
public void transfer(String out,String in ,Double money) {
try{
accountDao.outMoney(out,money);
int i = 1/0;
accountDao.inMoney(in,money);
}finally {
logService.log(out,in,money);
}
}
}
- 将数据库中的金额分别手动修改重置到1000,并执行测试类
7. Spring事务属性–事务传播行为
- 探讨添加了异常后,日志能否正常加载
- 运行结果分析
此时再次运行测试类,我们可以发现数据库表中金额表数值由于开启了事务,当出现异常时,事务回滚,金额数值不变。同时日志表的数据也没有发生变化,不符合我们的预期。
- 存在的问题:
日志的记录与转账操作隶属同一个事务,同成功同失败
- 实现效果预期改进:
无论转账操作是否成功,日志必须保留,这时就需要通过事务传播行为来解决这个问题
- spring事务的7种传播行为
required | (默认传播行为),如果当前有事务,其他就用当前事务,不会新增事务。例如:方法A调用方法B,它们用同一个事务。(如果B没有事务,它们会用同一个事务。)(只要有一个回滚,整体就会回滚) |
requires_new | 如果当前有事务,其他不会加入当前事务,会新增事务。即他们的事务没有关系,不是同一个事务。如果其他没有事务,那么以当前事务运行。例如:方法A调用方法B,它们用不同的事务。(B不会用A的事务,会新增事务) |
supports | 当前没有事务,就以非事务运行。当前有事务呢?就以当前事务运行。例如:方法A调用方法B,如果A没有事务,那么B就以非事务运行。如果A有事务就以A事务为准。如果A没有事务,那么B就会以非事务执行。 |
mandatory | 其他没有事务就会抛异常。当前没有事务抛出异常,当前有事务则支持当前事务。支持当前事务,如果当前没有事务就会抛出异常。例如:方法A调用方法B,如果方法A没有事务,那么就会抛出异常。 |
not_supported | 以非事务执行。例如:方法A调用方法B,方法B会挂起事务A以非事务方式执行。 |
never | 以非事务执行,如果存在事务,抛出异常。总是以非事务执行,如果存在事务,那么就抛出异常。 |
nested | 如果当前已经存在一个事务,那么该方法将会在嵌套事务中运行。嵌套的事务可以独立于当前事务进行单独地提交或回滚。如果当前事务不存在,那么其行为与Required一样。例如:方法A中调用了方法B,B中try catch手动回滚,A不会回滚。 |
- 设置日志接口方法的事务传播行为属性
public interface LogService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
void log(String out, String in, Double money);
}
- 运行测试类观察效果
基于以上结果,Spring事务传播行为设置成功,业务功能运行正常。
1.【w3cschool】
2. 【spring教学视频】
总结
欢迎各位留言交流以及批评指正,如果文章对您有帮助或者觉得作者写的还不错可以点一下关注,点赞,收藏支持一下作者,后续还会更新mybatis, springboot,maven高级,微信小程序,等前后端内容的学习笔记。
(博客的参考源码可以在我主页的资源里找到,如果在学习的过程中有什么疑问欢迎大家在评论区向我提出)