Spring
1.Spring JdbcTemplate的使用
Spring对不同持久化技术的支持,Spring为各种支持的持久化技术,都提供了简单操作的模板和回调
ORM持久化技术 | 模板类 |
---|---|
JDBC | org.springframework.jdbc.core.JdbcTemplate |
Hibernate5.0 | org.springframework.orm.hibernate5.HibernateTemplate |
IBatis(MyBatis) | org.springframework.orm.ibatis.SqlMapClientTemplate |
JPA | org.springframework.orm.jpa.JpaTemplate |
之前我们如果要使用JDBC来连接数据库, 要进行六个步骤的操作
- 注册驱动:DriverManager.registerDriver(new com.mysql.jdbc.Driver());
- 获取连接:Connection conn = DriverManager.getConnection(url, user,password);
- 获取数据库操作对象:Statement stmt = conn.createStatement();
- 执行sql语句:int affectLine = stmt.executeUpdate(sql);
- 处理查询结果集
- 释放资源:stmt.close(), conn.close();
而Spring提供了一个更快捷的jdbc操作目标:JdbcTemplate, 用来简化jdbc的操作
1.1 JdbcTemplate入门
一、 提供依赖包
主要是spring-jdbc包
<dependencies>
<!-- unit测试 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>compile</scope>
</dependency>
<!-- jdbc包 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
<!-- 事务相关 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
<!-- mysql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.29</version>
</dependency>
</dependencies>
二、动手实现jdbcTemplate
public class JdbcTemplateTest {
@Test
public void test(){
//目标:使用jdbctemplate执行一段sql
//1.构建数据源 DriverManagerDataSource
//spring内置了一个数据源
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://127.0.0.1:3306/my_database");
dataSource.setUsername("root");
dataSource.setPassword("root");
//2.创建jdbctemplate实例
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
//3.执行sql,创建表test001
jdbcTemplate.execute("create table test001(id int,name varchar(20))");
}
}
三、存在的问题
上述代码中的dataSource以及jdbcTemplate都需要我们手动创建 ,能不能将这两个对象个交给Spring管理?
1.2 Spring管理DataSource和JdbcTemplate
一、xml方式
注入数据源: 此处我们使用druid连接池作为数据源
<!-- druid连接池 -->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://127.0.0.1:3306/my_database"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</bean>
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<!-- 注入数据源 -->
<property name="dataSource" ref="dataSource"/>
</bean>
可以使用引入外部文件(druid.properties)来配置数据源(解耦)
jdbc.driverClass=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://127.0.0.1:3306/my_database
jdbc.username=root
jdbc.password=root
<!-- druid连接池 -->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="${jdbc.driverClass}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
二、注解方式
1.3 基于JdbcTemplate实现DAO
为了方便Dao中注入JdbcTemplate,Spring为每一个持久化技术都提供了支持类,如图
此处我们使用JDBC的支持类:JdbcDAOSupport
第一步:编写dao类,并继承JdbcDAOSupport
public class BookDao extends JdbcDaoSupport {
//保存图书
public void save(Book book) {
String sql = "insert into book values(null,?,?)";
// 调用jdbctemplate
// jdbcTemplate.update(sql, book.getName(),book.getPrice());
super.getJdbcTemplate().update(sql, book.getName(), book.getPrice());
}
// 更新
public void update(Book book) {
String sql = "update book set name =? ,price =? where id =?";
super.getJdbcTemplate().update(sql, book.getName(), book.getPrice(), book.getId());
}
// 删除
public void delete(Book book) {
super.getJdbcTemplate().update("delete from book where id =?", book.getId());
}
//根据id查询
public Book findById(Integer id) {
String sql = "select * from book where id = ?";
return super.getJdbcTemplate().queryForObject(sql, BeanPropertyRowMapper.newInstance(Book.class), id);
}
//查询所有
public List<Book> findAll() {
String sql = "select * from book";
return super.getJdbcTemplate().query(sql, BeanPropertyRowMapper.newInstance(Book.class));
}
//条件查询: 根据图书名称模糊查询信息
public List<Book> findByNameLike(String name) {
String sql = "select * from book where name like ?";
return super.getJdbcTemplate().query(sql, BeanPropertyRowMapper.newInstance(Book.class), "%" + name + "%");
}
}
第二步:在Spring配置文件中,将jdbcTemplate注入到dao类(或者直接注入数据源)
<!-- jdbctemplate对象 -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<!-- 注入数据源 -->
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 配置dao,注入jdbctemplate -->
<bean id="bookDao" class="cn.itcast.spring.dao.BookDao">
<!-- 方案一:在BookDao中提供jdbcTempate属性,通过set方法注入 jdbcTemplate-->
<!-- <property name="jdbcTemplate" ref="jdbcTemplate"/> -->
<!-- 方案二:BookDao类继承JdbcDaoSupport,直接注入数据源,就拥有了jdbctempate对象 -->
<property name="dataSource" ref="dataSource"/>
</bean>
2.Spring的事务管理机制
Spring事务管理高层抽象主要包括3个接口,Spring的事务主要是由他们共同完成的:
- PlatformTransactionManager 事务管理器
主要用于平台相关事务的管理 - TransactionDefinition 事务定义信息
通过配置如何进行事务管理。(隔离、传播、超时、只读) - TransactionStatus 事务具体运行状态
事务管理过程中,每个时间点事务的状态信息。
2.1 PlatformTransactionManager 事务管理器
Spring为不同的持久化框架提供了不同PlatformTransactionManager接口实现:
事务 | 说明 |
---|---|
org.springframework.jdbc.datasource.DataSourceTransactionManager | 使用Spring JDBC 或iBatis 进行持久化数据时使用 |
org.springframework.orm.hibernate5.HibernateTransactionManager | 使用Hibernate5.0 版本进行持久化数据时使用 |
org.springframework.orm.jpa.JpaTransactionManager | 使用JPA进行持久化时使用 |
org.springframework.jdo.JdoTransactionManager | 当持久化机制是Jdo时使用 |
org.springframework.transaction.jta.JtaTransactionManager | 使用一个JTA实现来管理事务,在一个事务跨越多个资源时必须使用 |
我们需要根据项目使用的持久化框架来配置不同的事务管理器(如果是SpringBoot, 则不需要手动定义事务管理器)
2.1.1 配置事务管理器
- 配置事务管理器
- 往事务管理器中注入数据源
一、xml方式
- DataSourceTransactionManager
<!-- 第一步:定义具体的平台事务管理器(DataSource事务管理器) -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!-- 注入数据源 -->
<property name="dataSource" ref="dataSource"/>
</bean>
- HibernateTransactionManager
<bean id="transactionManager" class="org.springframework.orm.hibernate5.HibernateTransactionManager">
<!-- 注入数据源 -->
<property name="dataSource" ref="dataSource"/>
</bean>
二、配置方式
在Spring配置类中,注册事务管理器
//配置事务管理器,mybatis使用的是jdbc事务
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource){
// HibernateTransactionManager dtm = new HibernateTransactionManager();
DataSourceTransactionManager dtm = new DataSourceTransactionManager();
transactionManager.setDataSource(dataSource);
return transactionManager;
}
用户根据选择和使用的持久层技术,来选择对应的事务管理器
2.2 TransactionDefinition 事务定义信息
- 定义事务隔离级别
- 定义事务传播行为
2.2.1 IsolationLevel 事务的隔离级别
我们要了解事务的隔离级别, 首先要搞懂脏读、幻读、不可重复读
2.2.1.1 脏读、幻读、不可重复读
一、脏读 (读取未提交数据)
一个事务读取了另一个事务改写但还未提交的数据,如果这些数据被回滚,则读到的数据是无效的。
简单理解:事务A读取到事务B修改了但未提交的数据
二、不可重复读 (前后多次读取,数据内容不一致)
事务A在执行读取操作,由整个事务A比较大,前后读取同一条数据需要经历很长的时间 。而在事务A第一次读取数据,比如此时读取了小明的年龄为20岁,事务B执行更改操作,将小明的年龄更改为30岁,此时事务A第二次读取到小明的年龄时,发现其年龄是30岁,和之前的数据不一样了,也就是数据不重复了,系统不可以读取到重复的数据,成为不可重复读。
简单理解:事务A前后两次读取同一条数据,数据内容不一致, 原因是其他事务对这条数据的内容进行了修改并提交
三、幻读 (前后多次读取,数据总量不一致)
事务A在执行读取操作,需要两次统计数据的总量,前一次查询数据总量后,此时事务B执行了新增数据的操作并提交后,这个时候事务A读取的数据总量和之前统计的不一样,就像产生了幻觉一样,平白无故的多了几条数据,成为幻读。
简单理解:事务A前后两次读取到的数据总量不一致, 原因是其他事务新增、删除了数据
2.2.1.2 四种隔离级别
一、读未提交(Read uncommitted)
在这种隔离级别下,所有事务能够读取其他事务未提交的数据。读取其他事务未提交的数据,会造成脏读。因此在该种隔离级别下,不能解决脏读、不可重复读和幻读。
读未提交可能会产生脏读的现象,那么怎么解决脏读呢?那就是使用读已提交。
二、读已提交(Read committed)
在这种隔离级别下,所有事务只能读取其他事务已经提交的内容。能够彻底解决脏读的现象。但在这种隔离级别下,会出现一个事务的前后多次的查询中却返回了不同内容的数据的现象,也就是出现了不可重复读。
注意:这是大多数数据库系统默认的隔离级别,例如Oracle和SQL Server,但mysql不是。
已提交可能会产生不可重复读的现象,我们可以使用可重复读。
三、可重复读(Repeatable read)
在这种隔离级别下,所有事务前后多次的读取到的数据内容是不变的。也就是某个事务在执行的过程中,不允许其他事务进行update操作,但允许其他事务进行add操作,造成某个事务前后多次读取到的数据总量不一致的现象,从而产生幻读。
注意:这才是mysql的默认事务隔离级别
可重复读依然会产生幻读的现象,此时我们可以使用串行化来解决。
四、可串行化(Serializable)
在这种隔离级别下,所有的事务顺序执行,所以他们之间不存在冲突,从而能有效地解决脏读、不可重复读和幻读的现象。但是安全和效率不能兼得,这样事务隔离级别,会导致大量的操作超时和锁竞争,从而大大降低数据库的性能,一般不使用这样事务隔离级别。
下面用一张表格来表示他们能够解决的问题
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
读未提交(Read uncommitted) | ×(未解决) | × | × |
读已提交(Read committed) | √(解决) | × | × |
可重复读(Repeatable read) | √ | √ | × |
可串行化(Serializable) | √ | √ | √ |
2.2.1.3 悲观锁和乐观锁
事务的隔离级别,只是用来管理事务与事务间的感知关系,它并不能解决并发操作带来的数据不一致性问题
可能会引发很多种类的问题,这里举两个例子
情况一:更新数据不一致
情况二:数据丢失
2.2.1.3.1 悲观锁
一、原理:基于数据库的锁机制实现
悲观锁总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题(比如共享数据被修改),所以每次在获取资源操作的时候都会上锁,这样其他线程想拿到这个资源就会阻塞直到锁被上一个持有者释放。
数据库中的行锁,表锁,读锁(共享锁),写锁(排他锁),以及syncronized实现的锁均为悲观锁
二、具体实现:
- 事务中基于索引进行update操作时,会自动触发行锁(自动实现)
- 事务中进行select操作时,手动设置行数(基于索引, 需要手动实现)
2.2.1.3.1.1 表锁
- 表共享锁(表读锁)
自己和其他线程只能读(select)取该表, 自己和其他线程不能对表进行写(insert、update、delete)操作
//给表加读锁
mysql> lock table test read;
Query OK, 0 rows affected (0.00 sec)
mysql> insert into test(name,adress) values('ygz','zhongxian');
ERROR 1099 (HY000): Table 'test' was locked with a READ lock and can't be updated
- 表排他锁(表写锁)
该线程可以对这个表进行读写,其他线程对该表的读和写都受到阻塞;
//给表加读锁
mysql> lock table test write;
Query OK, 0 rows affected (0.00 sec)
// 释放锁
unlock tables
2.2.1.3.1.2 行锁
行锁是在引擎层由各个引擎自己实现的,有的引擎并不支持行锁,比如MyISAM就不支持行锁,这意味着:
- 并发控制只能使用表锁,对于这种引擎(MyISAM)的表,同一张表上任何时刻只能有一个更新在执行,这严重影响了并发度;
- InnoDB是支持行锁的,这也是MyISAM被InnoDB代替的主要原因
InnoDB存储引擎默认采用行锁, InnoDB的行锁是针对索引加的锁,不是针对记录加的锁,并且该索引不能失效,否则都会从行锁升级为表锁;
一、验证mysql的行锁
开启两个cdm窗口,启动两个事物A、B,在事物A中更改表中的一行数据,此时未提交事物A,在事物B中更改其他行,看是否能成功
可以看到,在事务A未提交的情况下
- B中不能更新A中更新的那一行(会受到阻塞,一定时间如果还没获取到行锁会自动放弃更新),其他行能进行更新
- 例如执行update book set price = 400 where id = 2
- 当A一提交,B中更新A中更新的那一行就会不再阻塞,执行完毕;
二、验证行锁是建立在索引上
我们在事务A中不用id更新book表(id是主键,所以是有索引的)
因为这里事物A中的更新没有基于索引(name没加索引),所以这里由行锁会降级成表锁,所以在事物B中不能对该表进行任何更新,只能读;
2.2.1.3.1.3 悲观锁的实现方式
上文我们展示的是在事务A进行修改时,触发了行锁,那我们能不能在查询的时候就加上行数呢?
select * from tab_with_index where id = 1 for update;
for update可以根据条件来完成行锁锁定,并且 ID 是有索引键的列,如果 ID不是索引键那么InnoDB将完成表锁。
- 悲观锁实现行锁(查询条件有索引)
- where id = 1 for for update
- 悲观锁实现表锁(查询条件没有索引)
- where name = ‘语文’ for update
2.2.1.3.1.4 悲观锁的存在的问题
首先是效率问题:悲观锁会锁行、锁表, 性能低
其次它能保证当前的A事务中,前后操作的一致性,但是可能影响其他事务
当然我们在B事务中查询id=1时加上for update也能规避这个问题
2.2.1.3.2 乐观锁
使用乐观锁就不需要借助数据库的锁机制了。
乐观锁主要分为两步走
- 冲突检测
- 数据更新
例如库存表中,id为1的记录,它的库存数为18. 我们在更新数据之前,先查一下库存表中的当前库存数,然后以这个值作为一个条件去修改
-- 查询出商品的库存树 quantity = 18
select quantity from good where id = 1;
-- 修改商品库存为6
update good set quantity = 2 where id = 1 and quantity = 18;
当提交更新的时候,判断数据库表中对应记录的当前库存数与第一次取出来的库存树进行对比,如果两者一致就进行更新,如果不一致,则认为是过期数据
2.2.1.3.2.1 ABA问题
一、以上更新语句存在一个比较严重的问题,即传说中的ABA问题:
;如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然是A值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回A,那CAS操作就会误认为它从来没有被修改过。这个问题被称为CAS操作的 "ABA"问题。
- 比如说线程A从数据库中取出库存数3,这时候线程B也从数据库中取出库存数3,并且线程B进行了一些操作变成了2。
- 然后线程B又将库存数变成3,这时候线程A进行 CAS 操作发现数据库中仍然是3,然后线程one操作成功。
- 尽管线程one的 CAS 操作成功,但是不代表这个过程就是没有问题的。(可能整个过程中操作了很多其他业务)
二、解决ABA问题
我们需要加上一个版本号(Version),在每次提交的时候将版本号+1操作,那么下个线程去提交修改的时候,会带上版本号去判断,如果版本修改了,那么线程重试或者提示错误信息~
2.2.1.3.2.2 乐观锁的实现方式
使用版本控制字段,再利用行锁的特性实现乐观锁:
此处我们可以告知B重新处理
2.2.1.3.3 死锁
数据库使用乐观锁导致产生死锁:
事务A
update user set age = 21 where id = 1
update user set age = 22 where id = 2
事务B
update user set age = 21 where id = 2
update user set age = 22 where id = 1
假设在两个事务中有以上两个操作,同时修改order表中两条数据, 事务A在执行完第一条update的时候,刚好事务B也执行完第一条update, 此时, 事务A中user表中的id = 1的行被锁住, 事务B中user表中id = 2的行被锁住, 两个事务继续往下执行事务A中第二条update执行需要user表中id = 2的行数据, 而事务B中第二条update执行需要id = 1的行数据, 两条update往下执行的条件都需要对方事务中已经被锁住的行, 于是陷入无限等待, 形成死锁。
解决死锁的产生:
指定锁的执行顺序,比如把以上两事务稍作修改
事务A
update user set age = 22 where id = 2
update user set age = 21 where id = 1
事务B
update user set age = 21 where id = 2
update user set age = 22 where id = 1
事务A执行第一条update时,id = 2 的行被锁住,此时,事务B想修改id = 2的行,只能等待事务A执行完成,当事务A执行完成时,事务B再执行, 这样就不会产生死锁了。
2.2.2 PropagationBehavior 事务的传播行为
事务传播行为(propagation behavior)指的就是当一个事务方法被另一个事务方法调用时,这个事务方法应该如何进行。
例如:methodA事务方法调用methodB事务方法时,methodB是继续在调用者methodA的事务中运行呢,还是为自己开启一个新事务运行,这就是由methodB的事务传播行为决定的。
一、PROPAGATION_REQUIRED
如果存在一个事务,则支持当前事务。如果没有事务则开启一个新的事务。
可以把事务想像成一个胶囊,在这个场景下方法B用的是方法A产生的胶囊(事务)。
举例有两个方法:
@Transactional(propagation = Propagation.REQUIRED)
public void methodA() {
methodB();
// do something
}
@Transactional(propagation = Propagation.REQUIRED)
public void methodB() {
// do something
}
单独调用methodB方法时,因为当前上下文不存在事务,所以会开启一个新的事务。
调用methodA方法时,因为当前上下文不存在事务,所以会开启一个新的事务。当执行到methodB时,methodB发现当前上下文有事务,因此就加入到当前事务中来。
二、PROPAGATION_SUPPORTS
;如果存在一个事务,支持当前事务。如果没有事务,则非事务的执行。但是对于事务同步的事务管理器,PROPAGATION_SUPPORTS与不使用事务有少许不同。
@Transactional(propagation = Propagation.REQUIRED)
public void methodA() {
methodB();
// do something
}
// 事务属性为SUPPORTS
@Transactional(propagation = Propagation.SUPPORTS)
public void methodB() {
// do something
单纯的调用methodB时,methodB方法是非事务的执行的。当调用methdA时,methodB则加入了methodA的事务中,事务地执行。
三、PROPAGATION_MANDATORY
如果已经存在一个事务,支持当前事务。如果没有一个活动的事务,则抛出异常。
@Transactional(propagation = Propagation.REQUIRED)
public void methodA() {
methodB();
// do something
}
// 事务属性为MANDATORY
@Transactional(propagation = Propagation.MANDATORY)
public void methodB() {
// do something
当单独调用methodB时,因为当前没有一个活动的事务,则会抛出异常throw new IllegalTransactionStateException(“Transaction propagation ‘mandatory’ but no existing transaction found”);
当调用methodA时,methodB则加入到methodA的事务中,事务地执行。
四、PROPAGATION_REQUIRES_NEW
如果一个事务已经存在,则先将这个存在的事务挂起。它会开启一个新的事务,使用PROPAGATION_REQUIRES_NEW,需要使用 JtaTransactionManager作为事务管理器。
@Transactional(propagation = Propagation.REQUIRED)
public void methodA() {
doSomeThingA();
methodB();
doSomeThingB();
// do something else
}
// 事务属性为REQUIRES_NEW
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodB() {
// do something
}
相当于调用
main(){
TransactionManager tm = null;
try{
//获得一个JTA事务管理器
tm = getTransactionManager();
tm.begin();//开启一个新的事务
Transaction ts1 = tm.getTransaction();
doSomeThingA();
tm.suspend();//挂起当前事务
try{
tm.begin();//重新开启第二个事务
Transaction ts2 = tm.getTransaction();
methodB();
ts2.commit();//提交第二个事务
} Catch(RunTimeException ex) {
ts2.rollback();//回滚第二个事务
} finally {
//释放资源
}
//methodB执行完后,恢复第一个事务
tm.resume(ts1);
doSomeThingB();
ts1.commit();//提交第一个事务
} catch(RunTimeException ex) {
ts1.rollback();//回滚第一个事务
} finally {
//释放资源
}
}
在这里,我把ts1称为外层事务,ts2称为内层事务。从上面的代码可以看出,ts2与ts1是两个独立的事务,互不相干。ts2是否成功并不依赖于 ts1。如果methodA方法在调用methodB方法后的doSomeThingB方法失败了,而methodB方法所做的结果依然被提交。而除了 methodB之外的其它代码导致的结果却被回滚了
五、PROPAGATION_NOT_SUPPORTED
PROPAGATION_NOT_SUPPORTED 总是非事务地执行,并挂起任何存在的事务。使用PROPAGATION_NOT_SUPPORTED,也需要使用JtaTransactionManager作为事务管理器
六、PROPAGATION_NOT_SUPPORTED
总是非事务地执行,如果存在一个活动事务,则抛出异常。
七、PROPAGATION_NESTED
如果一个活动的事务存在,则运行在一个嵌套的事务中。 如果没有活动事务, 则按TransactionDefinition.PROPAGATION_REQUIRED 属性执行。
这是一个嵌套事务,使用JDBC 3.0驱动时,仅仅支持DataSourceTransactionManager作为事务管理器。
需要JDBC 驱动的java.sql.Savepoint类。使用PROPAGATION_NESTED,还需要把PlatformTransactionManager的nestedTransactionAllowed属性设为true(属性值默认为false)。
这里关键是嵌套执行
@Transactional(propagation = Propagation.REQUIRED)
methodA(){
doSomeThingA();
methodB();
doSomeThingB();
}
@Transactional(propagation = Propagation.NEWSTED)
methodB(){
……
}
如果单独调用methodB方法,则按REQUIRED属性执行。如果调用methodA方法,相当于下面的效果:
main(){
Connection con = null;
Savepoint savepoint = null;
try{
con = getConnection();
con.setAutoCommit(false);
doSomeThingA();
savepoint = con2.setSavepoint();
try{
methodB();
} catch(RuntimeException ex) {
con.rollback(savepoint);
} finally {
//释放资源
}
doSomeThingB();
con.commit();
} catch(RuntimeException ex) {
con.rollback();
} finally {
//释放资源
}
}
当methodB方法调用之前,调用setSavepoint方法,保存当前的状态到savepoint。如果methodB方法调用失败,则恢复到之前保存的状态。但是需要注意的是,这时的事务并没有进行提交,如果后续的代码(doSomeThingB()方法)调用失败,则回滚包括methodB方法的所有操作。嵌套事务一个非常重要的概念就是内层事务依赖于外层事务。外层事务失败时,会回滚内层事务所做的动作。而内层事务操作失败并不会引起外层事务的回滚。
扩展:PROPAGATION_NESTED 与PROPAGATION_REQUIRES_NEW的区别:
;它们非常类似,都像一个嵌套事务,如果不存在一个活动的事务,都会开启一个新的事务。
使用 PROPAGATION_REQUIRES_NEW时,内层事务与外层事务就像两个独立的事务一样,一旦内层事务进行了提交后,外层事务不能对其进行回滚。两个事务互不影响。两个事务不是一个真正的嵌套事务。同时它需要JTA事务管理器的支持。
使用PROPAGATION_NESTED时,外层事务的回滚可以引起内层事务的回滚。而内层事务的异常并不会导致外层事务的回滚,它是一个真正的嵌套事务。DataSourceTransactionManager使用savepoint支持PROPAGATION_NESTED时,需要JDBC 3.0以上驱动及1.4以上的JDK版本支持。其它的JTATrasactionManager实现可能有不同的支持方式。
PROPAGATION_REQUIRES_NEW 启动一个新的, 不依赖于环境的 “内部” 事务. 这个事务将被完全 commited 或 rolled back 而不依赖于外部事务, 它拥有自己的隔离范围, 自己的锁, 等等. 当内部事务开始执行时, 外部事务将被挂起, 内务事务结束时, 外部事务将继续执行。
另一方面, PROPAGATION_NESTED 开始一个 “嵌套的” 事务, 它是已经存在事务的一个真正的子事务. 潜套事务开始执行时, 它将取得一个 savepoint. 如果这个嵌套事务失败, 我们将回滚到此 savepoint. 潜套事务是外部事务的一部分, 只有外部事务结束后它才会被提交。
由此可见, PROPAGATION_REQUIRES_NEW 和 PROPAGATION_NESTED 的最大区别在于, PROPAGATION_REQUIRES_NEW 完全是一个新的事务, 而 PROPAGATION_NESTED 则是外部事务的子事务, 如果外部事务 commit, 嵌套事务也会被 commit, 这个规则同样适用于 roll back.
总结
- PROPAGATION_REQUIRED(默认值)、PROPAGATION_SUPPORTS、PROPAGATION_MANDATORY
支持当前事务, A调用B,如果A事务存在,B和A处于同一个事务 。
事务默认传播行为 REQUIRED。最常用的。 - PROPAGATION_REQUIRES_NEW、PROPAGATION_NOT_SUPPORTED、PROPAGATION_NEVER
不会支持原来的事务 ,A调用B, 如果A事务存在, B肯定不会和A处于同一个事务。
常用的事务传播行为:PROPAGATION_REQUIRES_NEW - PROPAGATION_NESTED
嵌套事务 ,只对DataSourceTransactionManager有效 ,底层使用JDBC的SavePoint机制,允许在同一个事务设置保存点,回滚保存点
2.3 TransactionStatus 事务状态
事务运行过程中,每个时间点 事务状态信息 !
- flush(),给hibernate使用,底层发出sql的
- hasSavepoint():判断是否有保留点
- isCompleted():判断事务是否结束
- isNewTransaction():判断当前事务是否是新开的一个事务。
- isRollbackOnly():判断事务是否只能回滚
- setRollbackOnly():设置事务是否回滚
3.Spring事务管理的实现
Spring的声明式事务是通过AOP实现的(环绕通知)
前期准备
CREATE TABLE `t_account` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`name` VARCHAR(20) NOT NULL,
`money` DOUBLE DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
INSERT INTO `t_account` VALUES (1, 'Tom', 1000);
INSERT INTO `t_account` VALUES (2, 'Jack', 1100);
INSERT INTO `t_account` VALUES (3, 'Rose', 1200);
public interface IAccountDao {
//(存入)转入
public void in(String name,Double money);
//(取出)转出
public void out(String name,Double money);
}
//账户操作持久层
//技术方案:jdbctempate
public class AccountDaoImpl extends JdbcDaoSupport implements IAccountDao {
//(存入)转入
public void in(String name,Double money){
String sql="update t_account set money = money+ ? where name = ?";
super.getJdbcTemplate().update(sql, money,name);
}
//(取出)转出
public void out(String name,Double money){
String sql="update t_account set money = money- ? where name = ?";
super.getJdbcTemplate().update(sql, money,name);
}
}
public interface IAccountService {
void transfer(String outName,String inName,Double money);
}
//掌握操作的业务层
public class AccountServiceImpl implements IAccountService{
//注入dao
private IAccountDao accountDao;
public void setAccountDao(IAccountDao accountDao) {
this.accountDao = accountDao;
}
//转账操作的业务逻辑
public void transfer(String outName,String inName,Double money){
//调用dao层
//先取出
accountDao.out(outName, money);
//再转入
accountDao.in(inName, money);
}
}
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations={"classpath:applicationContext.xml"})
public class SpringTest {
//注入测试的service
@Autowired
private IAccountService accountService;
//需求:账号转账,Tom账号取出1000元,存放到Jack账号上
@Test
public void testTransfer(){
accountService.transfer("Tom", "Jack", 1000d);
System.out.println("转账成功!");
}
}
3.1 xml方式配置事务管理
一、引入依赖
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
<!-- 事务相关 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
<!-- 事务相关 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
二、applicationContext.xml
- 定义数据源
- 定义数据管理器
- 配置AOP切面
<?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:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd">
<!-- druid连接池 -->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://127.0.0.1:3306/my_database"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</bean>
<!-- 第一步:定义具体的平台事务管理器(DataSource事务管理器) -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!-- 注入数据源 -->
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 第二步:定义通知,通知中要处理的就是事务 -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<!-- 配置事务的属性定义 -->
<tx:attributes>
<!-- 配置具体的方法的事务属性
isolation//事务的隔离级别,默认是按数据库的隔离级别来
propagation//事务的传播行为,默认是同一个事务
timeout="-1":事务的超时时间,默认值使用数据库的超时时间。
read-only="false":事务是否只读,默认可读写。
rollback-for:遇到哪些异常就回滚,其他的都不回滚
no-rollback-for:遇到哪些异常不回滚,其他的都回滚。和上面互斥的
-->
<tx:method name="transfer" isolation="DEFAULT" propagation="REQUIRED" timeout="-1" read-only="false"/>
<!-- 支持通配符
要求service中 方法名字必须符合下面的规则
-->
<tx:method name="save*"/>
<tx:method name="update*"/>
<tx:method name="delete*"/>
<tx:method name="find*" read-only="true"/>
</tx:attributes>
</tx:advice>
<!-- 第三步:配置切入点,让通知关联切入点,即事务控制业务层的方法 -->
<aop:config>
<!-- 切入点 -->
<aop:pointcut expression="bean(*Service)" id="txPointcut"/>
<!-- 切面 -->
<aop:advisor advice-ref="txAdvice" pointcut-ref="txPointcut"/>
</aop:config>
<!-- dao -->
<bean id="accountDAO" class="com.wxf.dao.impl.AccountDaoImpl">
<property name="dataSource" ref="dataSource" />
</bean>
<!-- service -->
<bean id="accountService" class="com.wxf.service.impl.AccountServiceImpl">
<property name="accountDao" ref="accountDAO" />
</bean>
</beans>
【声明式事务处理的原理图】
没有添加事务:
【注意】
如果不配置,则走默认的事务(默认事务是每个数据库操作都是一个事务,相当于没事务),所以我们开发时需要配置事务。
2.2 注解方式配置事务管理
步骤:
- 1.在需要管理事务的方法或者类上面 添加@Transactional 注解
- 2.配置注解驱动事务管理(事务管理注解生效的作用)(需要配置对特定持久层框架使用的事务管理器)
<?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:p="http://www.springframework.org/schema/p"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
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/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd">
<!-- 引入外部属性文件 -->
<context:property-placeholder location="classpath:db.properties" />
<!-- 配置数据源 -->
<!-- c3p0连接池 -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="${jdbc.className}" />
<property name="jdbcUrl" value="${jdbc.url}" />
<property name="user" value="${jdbc.user}" />
<property name="password" value="${jdbc.password}" />
</bean>
<!-- 配置bean注解扫描 -->
<context:component-scan base-package="cn.itcast.spring.anntx"/>
<!-- 定义具体的平台事务管理器(DataSource事务管理器) -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!-- 注入数据源 -->
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 配置事务注解驱动 :识别事务的注解@tr。。。
transaction-manager:具体的平台事务管理器
-->
<!-- <tx:annotation-driven transaction-manager="transactionManager"/> -->
<!-- 默认的平台事务管理器的名字叫:transactionManager,此时transaction-manager="transactionManager"可以不写 -->
<tx:annotation-driven transaction-manager="transactionManager"/>
</beans>
//掌握操作的业务层
/**
* @Service("accountService")
* 相当于spring容器中定义:<bean id="accountService" class="cn.itcast.spring.anntx.service.AccountServiceImpl">
*/
@Service("accountService")
@Transactional//会对该类中,所有的共有的方法,自动加上事务--全局的设置,默认是可写
public class AccountServiceImpl implements IAccountService{
//注入dao
@Autowired
private IAccountDao accountDao;
//转账操作的业务逻辑
// @Transactional//在方法上添加事务
public void transfer(String outName,String inName,Double money){
//调用dao层
//先取出
accountDao.out(outName, money);
int d = 1/0;
//再转入
accountDao.in(inName, money);
}
@Transactional(readOnly=true)//使用局部覆盖全局的
public void findAccount(){
System.out.println("查询帐号的信息了");
}
}
【注意】:数据源的注解注入 需要自己添加set方法
//账户操作持久层
//技术方案:jdbctempate
/**
* @Repository("accountDao")
* 相当于容易中定义<bean id="accountDao" class="cn.itcast.spring.anntx.dao.AccountDaoImpl"/>
*/
@Repository("accountDao")
public class AccountDaoImpl extends JdbcDaoSupport implements IAccountDao {
//注入数据源
@Autowired
//private DataSource dataSource;//没有注入数据源成功~
原理:放到属性上的的注解相当于,自动生成setter方法上加注解
//@Autowired //自动到spring的容器中寻找类型是参数类型(DataSource)的bean
//public void setDataSource(DataSource dataSource){
// this.dataSource=dataSource;
//}
@Autowired//当初始化dao的时候,会调用该方法啊,通过set方法的形参注入数据源
//方法名无所谓
public void setSuperDataSource(DataSource dataSource){
//调用父类的方法
super.setDataSource(dataSource);
}
//(存入)转入
public void in(String name,Double money){
String sql="update t_account set money = money+ ? where name = ?";
super.getJdbcTemplate().update(sql, money,name);
}
//(取出)转出
public void out(String name,Double money){
String sql="update t_account set money = money- ? where name = ?";
super.getJdbcTemplate().update(sql, money,name);
}
}
(1)在需要管理事务的方法或者类上面 添加@Transactional 注解
(2)配置事务的定义属性信息,在注解中直接配置:
如果 @Transactional 标注在 Class 上面, 那么将会对这个 Class 里面所有的 public 方法都包装事务方法。等同于该类的每个公有方法都放上了@Transactional。
//掌握操作的业务层
/**
* @Service("accountService")
* 相当于spring容器中定义:<bean id="accountService" class="cn.itcast.spring.anntx.service.AccountServiceImpl">
*/
@Service("accountService")
@Transactional()//会对该类中,所有的共有的方法,自动加上事务--全局的设置,默认是可写
public class AccountServiceImpl implements IAccountService{
//注入dao
@Autowired
private IAccountDao accountDao;
//转账操作的业务逻辑
@Transactional(readOnly=false)//在方法上添加事务
public void transfer(String outName,String inName,Double money){
//调用dao层
//先取出
accountDao.out(outName, money);
int d = 1/0;
//再转入
accountDao.in(inName, money);
}
@Transactional(readOnly=true)//使用局部覆盖全局的
public void findAccount(){
System.out.println("查询帐号的信息了");
}
}