声明式事务
4.1 JdbcTemplate
4.1.1简介
spring框架对JDBC进行封装,使用JdbcTemplate方便实现对数据库操作
4.1.2 准备工作
- 添加依赖
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.1</version>
</dependency>
<!-- Spring 持久化层支持jar包 -->
<!-- Spring 在执行持久化层操作、与持久化层技术进行整合过程中,需要使用orm、jdbc、tx三个
jar包 -->
<!-- 导入 orm 包就可以通过 Maven 的依赖传递性把其他两个也导入 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>5.3.1</version>
</dependency>
<!-- Spring 测试相关 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.3.1</version>
</dependency>
<!-- junit测试 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
<!-- MySQL驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.16</version>
</dependency>
<!-- 数据源 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.31</version>
</dependency>
</dependencies>
- 创建jdbc.properties
- 创建spring的配置文件
- 创建测试类测试jdbcTemplate
- @RunWith(SpringJUnit4ClassRunner.class)//指定当前测试类在spring的测试环境中执行,此时就可以通过注入的方式直接获取ioc容器中的bean
- @ContextConfiguration(“classpath:spring-jdbc.xml”)//指定spring的配置文件
4.2 声明式事务概念
4.2.1 编程式事务
事务功能的相关操作全部通过自己编写代码来实现
Connectionconn=...;
try{
//开启事务:关闭事务的自动提交 conn.setAutoCommit(false);
//核心操作
//提交事务
conn.commit();
}catch(Exceptione){
//回滚事务
conn.rollBack();
}finally{
//释放数据库连接
conn.close();
}
编程式的实现方式存在缺陷:
- 细节没有被屏蔽:具体操作过程中,所有细节都需要程序员自己来完成,比较繁琐。
- 代码复用性不高:如果没有有效抽取出来,每次实现功能都需要自己编写代码,代码就没有得到复用。
4.2.2 声明式事务
既然事务控制的代码有规律可循,代码的结构基本是确定的,所以框架就可以将固定模式的代码抽取出来,进行相关的封装。
封装起来后,我们只需要在配置文件中进行简单的配置即可完成操作。
- 好处1:提高开发效率
- 好处2:消除了冗余的代码
- 好处3:框架会综合考虑相关领域中在实际开发环境下有可能遇到的各种问题,进行了健壮性、性能等各个方面的优化
4.3基于注解的声明式事务简单使用
- 创建表
CREATE TABLE `t_book` (
`book_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`book_name` varchar(20) DEFAULT NULL COMMENT '图书名称',
`price` int(11) DEFAULT NULL COMMENT '价格',
`stock` int(10) unsigned DEFAULT NULL COMMENT '库存(无符号)',
PRIMARY KEY (`book_id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
insert into `t_book`(`book_id`,`book_name`,`price`,`stock`) values (1,'斗破苍
穹',80,100),(2,'斗罗大陆',50,100);
CREATE TABLE `t_user` (
`user_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`username` varchar(20) DEFAULT NULL COMMENT '用户名',
`balance` int(10) unsigned DEFAULT NULL COMMENT '余额(无符号)',
PRIMARY KEY (`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
insert into `t_user`(`user_id`,`username`,`balance`) values (1,'admin',50);
- 创建各层组件,controller、dao、service
- controller层
- service层
- dao层
- 创建spring配置文件
- 在测试类中进行测试
这里有三个方法,每个方法都有一个sql语句执行,在mysql中默认一个sql语句独占一个事务操作,且自动提交,出现异常的是第三个方法,前两个方法没有异常,执行成功,所以还是更新了书的数量,这样是不正确的,因为用户的钱不够买书,不应该减少库存中书的数量,这时就需要放在同一个事务中,如果有一个方法执行失败就全部回滚 - 添加事务
- 在配置文件中配置事务管理器
从tx:annotation标签的左边图标来看,这是一个环绕通知,配置事务管理器和开启事务的注解驱动是有关联的,DataSourceTransactionManager是一个切面,里面包含了环绕通知,开始事务的注解驱动就能将该环绕通知作用于事务的连接点的,如何选择连接点呢? - 使用@Transactional注解所标识的方法或类中所有的方法使用事务进行管理,即使用@Transactional注解所标识的地方就是连接点
注意:@Transactional注解是使用在service层的方法上或者类上的,要在@Override注解下面使用,否则无法生效 - 在BookService对应的方法上使用
- 在测试类中测试之前
- 在测试类中进行测试,依旧报错,但是数据库中的数量并没有变
@Transactional注解的其他属性
- 只读属性readOnly
对于一个查询操作来说,如果我们把它设置成只读,就能够明确告诉数据库,这个操作不涉及写操作,这样数据库就能够针对查询操作进行优化
当有其他操作的时候报错
- 超时属性timeout默认是-1,表示一直等待
事务在执行过程中,有可能因为遇到某些问题,导致程序卡住,从而长时间占用数据库资源。而长时间占用资源,大概率是因为程序运行出现了问题(可能是Java程序或MySQL数据库或网络连接等等)。此时这个很可能出问题的程序应该被回滚,撤销它已做的操作,事务结束,把资源让出来,让其他正常程序可以执行。
概括来说就是一句话:超时强制回滚,抛出异常,释放资源。
- 回滚策略
声明式事务默认只针对运行时异常回滚,编译时异常不回滚。
在service层中添加一行代码System.out.println(1/0);
运行测试类,发现会报错,然后事务被回滚了,数据库中的数据没变
使用noRollBackFor属性去过滤掉这个报错,让数据库碰到这个报错的时候不会回滚
运行测试类,发现仍然报错,但是数据库中的数据作出了响应的变化
等价,两个实现的效果是相同的
- 事务的隔离级别
数据库系统必须具有隔离并发运行各个事务的能力,使它们不会相互影响,避免各种并发问题。一个事务与其他事务隔离的程度称为隔离级别。SQL标准中规定了多种事务隔离级别,不同隔离级别对应不同的干扰程度,隔离级别越高,数据一致性就越好,但并发性越弱。- 隔离级别一共有四种
- 读未提交:READ UNCOMMITTED
允许Transaction01读取Transaction02未提交的修改。 - 读已提交:READ COMMITTED
要求Transaction01只能读取Transaction02已提交的修改。 - 可重复读:REPEATABLE READ
确保Transaction01可以多次从一个字段中读取到相同的值,即Transaction01执行期间禁止其它
事务对这个字段进行更新。 - 串行化:SERIALIZABLE
确保Transaction01可以多次从一个表中读取到相同的行,在Transaction01执行期间,禁止其它
事务对这个表进行添加、更新、删除操作。可以避免任何并发问题,但性能十分低下。
各个隔离级别解决并发问题的能力:
各种数据库产品对事务隔离级别的支持程度:
MySQL默认的隔离是可重复读,MySQL避免了幻读的问题,在两个可重复读的事务中,每一个事务中只能读取到当前事务中的操作,不能读取其他事务中的操作
- 读未提交:READ UNCOMMITTED
- 隔离级别一共有四种
- 事务的传播行为
简单来讲就是,有两个事务,a和b,事务a中想调用事务b的方法,如何进行调用就是传播行为
当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。
比如结账方法,本身有一个事务,然后结账方法中调用了两次buyBook方法,那么这个时候是用谁的事务呢?如果使用的是结账方法本身的事务,那么只要有一本书报错就全部回滚,两本书都买不成,如果使用的是buyBook的事务,那么一本书报错,另一本书也可以买成功,这样就会实现不同的效果
在测试类中进行测试,发现报错且事务回滚,数据库中数据并没有变
将数据库中的用户余额改为200
再次测试,成功运行
如果我们想用买书本身的事务,就在buyBook方法上设置propagation默认是REQUIRED
可以使用REQUIRES_NEW表示开启一个新的事务,即使用买书自己的事务
修改数据库中的数据
在测试类中进行测试
仍然报错,数据回滚,但是可以买一本斗破苍穹,所以数据库中发生了变化
4.4基于xml声明事务
- 将之前写的@Transactional注解全注释掉
- 引入aspectj资源
<!-- 基于xml方式的事务必须引入这个-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.3.1</version>
</dependency>
-
创建一个新的spring配置文件
-
测试是否成功
- 测试之前的数据
- 在测试类中进行测试
数据没变
如果想设置事务的一些属性,比如隔离级别、传播方式、回滚机制、超时时间就在tx:advice中进行设置