Spring事务管理——使用XML配置声明式事务
林炳文Evankaka原创作品。转自https://blog.csdn.net/Evankaka/article/details/45478007
一、事务介绍
事务简介:
事务管理是企业级应用程序开发中必不可少的技术,用来确保数据的完整性和一致性
事务就是一系列的动作,它们被当作一个单独的工作单元。这些动作要么全部完成,要么全部不起作用。
事务的四个关键属性(ACID)
① 原子性(atomicity):事务室一个原子操作,有一系列动作组成。事务的原子性确保动作要么全部完成,要么完全不起作用
② 一致性(consistency):一旦所有事务动作完成,事务就被提交。数据和资源就处于一种满足业务规则的一致性状态中
③ 隔离性(isolation):可能有许多事务会同时处理相同的数据,因此每个事物都应该与其他事务隔离开来,防止数据损坏
④ 持久性(durability):一旦事务完成,无论发生什么系统错误,它的结果都不应该受到影响。通常情况下,事务的结果被写到持久化存储器中
数据隔离级别
隔离级别 | 脏读 | 不可重复读 | 幻象读 | 第一类丢失更新 | 第二类丢失更新 |
READ UNCOMMITED | 允许 | 允许 | 允许 | 不允许 | 允许 |
READ COMMITED | 不允许 | 允许 | 允许 | 不允许 | 允许 |
REPEATABLE READ | 不允许 | 不允许 | 允许 | 不允许 | 不允许 |
SERIALIZABLE | 不允许 | 不允许 | 不允许 | 不允许 | 不允许 |
SqlServer2008R2的默认隔离级别是“READ COMMITED”,MySQL的默认隔离级别是“REPEATABLE READ”。
Spring中的事务管理
作为企业级应用程序框架,Spring在不同的事务管理API之上定义了一个抽象层。而应用程序开发人员不必了解底层的事务管理API,就可以使用Spring的事务管理机制。
Spring既支持编程式事务管理,也支持声明式的事务管理
编程式事务管理:将事务管理代码嵌入到业务方法中来控制事务的提交和回滚,在编程式事务中,必须在每个业务操作中包含额外的事务管理代码
声明式事务管理:大多数情况下比编程式事务管理更好用。它将事务管理代码从业务方法中分离出来,以声明的方式来实现事务管理。事务管理作为一种横切关注点,可以通过AOP方法模块化。Spring通过Spring AOP框架支持声明式事务管理。
Spring事务的传播属性:
当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。
事务的传播行为可以由传播属性指定。Spring定义了7种传播行为:
PROPAGATION_REQUIRED | 如果当前没有事务,就新建一个事务,如果已经存在一个事务,加入到这个事务中。这是最常见的选择。 |
PROPAGATION_SUPPORTS | 支持当前事务,如果当前没有事务,就以非事务方式执行。 |
PROPAGATION_MANDATORY | 使用当前的事务,如果当前没有事务,就抛出异常。 |
PROPAGATION_REQUIRES_NEW | 新建事务,如果当前存在事务,把当前事务挂起。 |
PROPAGATION_NOT_SUPPORTED | 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。 |
PROPAGATION_NEVER | 以非事务方式执行,如果当前存在事务,则抛出异常。 |
PROPAGATION_NESTED | 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。 |
并发事务所导致的问题
在同一个应用程序或者不同应用程序中的多个事务在同一个数据集上并发执行时,可能会出现许多意外的问题。
并发事务所导致的问题可以分为以下三类:
① 脏读:脏读发生在一个事务读取了另一个事务改写但尚未提交的数据时。如果改写在稍后被回滚了,那么第一个事务获取的数据就是无效的。
② 不可重复读:不可重复读发生在一个事务执行相同的查询两次或两次以上,但是每次都得到不同的数据时。这通常是因为另一个并发事务在两次查询期间更新了数据
③ 幻读:幻读与不可重复读类似。它发生在一个事务(T1)读取了几行数据,接着另一个并发事务(T2)插入了一些数据时。在随后的查询中,第一个事务(T1)就会发现多了一些原本不存在的记录
④ 数据库第一类第二类丢失更新:详情介绍及解决办法见https://blog.csdn.net/paopaopotter/article/details/79259686
二、实例开发
Spring声明式事务让我们从复杂的事务处理中得到解脱,使得我们再也不必去处理获得连接、关闭连接、事务提交和回滚等这些操作,再也无需我们在与事务相关的方法中处理大量的try…catch…finally代码。
我们在使用Spring声明式事务时,有一个非常重要的概念就是事务属性。事务属性通常由事务的传播行为、事务的隔离级别、事务的超时值、事务只读标志组成。我们在进行事务划分时,需要进行事务定义,也就是配置事务的属性。 Spring在TransactionDefinition接口中定义这些属性,以供PlatfromTransactionManager使用,PlatfromTransactionManager是spring事务管理的核心接口。
下面用一个例子来说明Spring事务管理的好处
新建立一个java工程,导入相关的包,事个工程最终目录如下:
这里应用到了mysql,需要建立数据库test及其下的数据表book_table
-
create database test;
-
use test;
-
CREATE TABLE book_table (bookname varchar(100) NOT NULL, bookid int(11) NOT NULL PRIMARY KEY) ;
1、数据表对应的model类:
-
package com.mucfc.model;
-
-
public class Book {
-
private String bookName;
-
private int bookId;
-
-
public Book(String bookName, int bookId) {
-
super();
-
this.bookName = bookName;
-
this.bookId = bookId;
-
}
-
public Book(){
-
-
}
-
@Override
-
public String toString() {
-
return "Book [bookName=" + bookName + ", bookId=" + bookId + "]";
-
}
-
public String getBookName() {
-
return bookName;
-
}
-
public void setBookName(String bookName) {
-
this.bookName = bookName;
-
}
-
public int getBookId() {
-
return bookId;
-
}
-
public void setBookId(int bookId) {
-
this.bookId = bookId;
-
}
-
-
}
2、接着是DAO层:
-
package com.mucfc.dao;
-
import com.mucfc.model.Book;
-
/**
-
* 图书馆DAO抽象类
-
* @author linbingwen
-
* @2015年5月8日10:36:29
-
*/
-
public interface LibraryDao {
-
/**
-
* 取得书
-
* @param name
-
* @return book
-
*/
-
public Book getBook(String name);
-
/**
-
* 增加书
-
* @param book
-
*/
-
public void addBook(Book book);
-
/**
-
* 删除书
-
* @param book
-
*/
-
public void deleteBook(String name);
-
-
}
实现类:
-
package com.mucfc.dao;
-
import java.sql.ResultSet;
-
import java.sql.SQLException;
-
import java.util.*;
-
-
import org.springframework.jdbc.core.JdbcTemplate;
-
import org.springframework.jdbc.core.RowMapper;
-
-
import com.mucfc.model.Book;
-
/**
-
* 图书馆DAO实现类
-
* @author linbingwen
-
* @2015年5月8日10:36:29
-
*/
-
public class LibraryDaoImpl implements LibraryDao{
-
private JdbcTemplate jdbcTemplate;
-
public JdbcTemplate getJdbcTemplate() {
-
return jdbcTemplate;
-
}
-
-
public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
-
this.jdbcTemplate = jdbcTemplate;
-
}
-
-
@Override
-
public Book getBook(String name) {
-
String sql="SELECT * FROM book_table WHERE bookname=?";
-
Book mBook = (Book)jdbcTemplate.queryForObject(sql,new Object[]{name},new RowMapper<Object>(){
-
@Override
-
public Object mapRow(ResultSet arg0, int arg1) throws SQLException {
-
Book book=new Book();
-
book.setBookId(arg0.getInt("bookid"));
-
book.setBookName(arg0.getString("bookname"));
-
return book;
-
}
-
});
-
return mBook;
-
}
-
-
@Override
-
public void addBook(Book book) {
-
String sql="INSERT INTO book_table VALUES(?,?)";
-
jdbcTemplate.update(sql, book.getBookName(),book.getBookId());
-
// jdbcTemplate.update(sql, book.getBookName(),book.getBookId());
-
}
-
-
@Override
-
public void deleteBook(String name) {
-
String sql="DELETE FROM book_table WHERE bookname=?";
-
jdbcTemplate.update(sql,name);
-
}
-
-
}
3、事务配置
-
<?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:aop="http://www.springframework.org/schema/aop" xmlns:p="http://www.springframework.org/schema/p"
-
xsi:schemaLocation="
-
http://www.springframework.org/schema/beans
-
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
-
http://www.springframework.org/schema/aop
-
http://www.springframework.org/schema/aop/spring-aop-3.0.xsd
-
http://www.springframework.org/schema/context
-
http://www.springframework.org/schema/context/spring-context-3.0.xsd">
-
<!-- 配置数据源 -->
-
<bean id="dataSource"
-
class="org.springframework.jdbc.datasource.DriverManagerDataSource">
-
<property name="driverClassName" value="com.mysql.jdbc.Driver" />
-
<property name="url" value="jdbc:mysql://localhost:3306/test" />
-
<property name="username" value="root" />
-
<property name="password" value="[email protected]" />
-
</bean>
-
<!--配置一个JdbcTemplate实例,并将这个“共享的”,“安全的”实例注入到不同的DAO类中去 -->
-
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
-
<property name="dataSource" ref="dataSource" />
-
</bean>
-
<!-- 声明事务管理器 -->
-
<bean id="txManager"
-
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
-
<property name="dataSource" ref="dataSource" />
-
</bean>
-
<!-- 需要实施事务增强的目标业务Bean -->
-
<bean id="libraryTarget" class="com.mucfc.dao.LibraryDaoImpl"
-
p:jdbcTemplate-ref="jdbcTemplate" />
-
-
<!-- 使用事务代理工厂类为目标业务Bean提供事务增强 -->
-
<bean id="libraryFactory"
-
class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean"
-
p:transactionManager-ref="txManager" p:target-ref="libraryTarget">
-
<!-- 事务属性配置 -->
-
<property name="transactionAttributes">
-
<props>
-
<!-- 以get开头的方法采用只读型事务控制类型 -->
-
<prop key="get*">PROPAGATION_REQUIRED,readOnly</prop>
-
<!-- 所有方法均进行事务控制,如果当前没有事务,则新建一个事务 -->
-
<prop key="addBook">PROPAGATION_REQUIRED</prop>
-
</props>
-
</property>
-
-
</bean>
-
</beans>
4、测试使用:
-
package com.mucfc.service;
-
import org.springframework.context.ApplicationContext;
-
import org.springframework.context.support.ClassPathXmlApplicationContext;
-
import com.mucfc.dao.LibraryDao;
-
import com.mucfc.model.Book;
-
-
public class Test {
-
-
public static void main(String[] args) {
-
ApplicationContext ctx=new ClassPathXmlApplicationContext("beans.xml");
-
Book book1=new Book("西游记",1);
-
Book book2=new Book("红楼梦",2);
-
Book book3=new Book("金瓶梅",3);
-
Book book4=new Book("三国演义",4);
-
Book book5=new Book("水浒传",5);
-
LibraryDao libraryDaoImpl=(LibraryDao)ctx.getBean("libraryFactory");
-
libraryDaoImpl.addBook(book1);
-
System.out.println(libraryDaoImpl.getBook("西游记"));
-
-
}
-
-
}
输出结果:
这里只输出了一条
数据库中的结果:
5、上面是正常时的情况,现在我们想让它出现异常,并自动回滚。简单。
LibraryDaoImpl类中插入的时候多插一个相同的数据
-
@Override
-
public void addBook(Book book) {
-
String sql="INSERT INTO book_table VALUES(?,?)";
-
jdbcTemplate.update(sql, book.getBookName(),book.getBookId());
-
jdbcTemplate.update(sql, book.getBookName(),book.getBookId());
-
}
然后先把数据库内容清空:
delete from test.book_table;
其它地方都不变:
执行程序后:
检查数据库:
发现一条数据都没有插入进去,所以说全部都回滚了。
如果这时我们不用Spring的事务管理,插入还是插入两条相同的,结果又会怎么样?
把Test类中的
LibraryDao library=(LibraryDao)ctx.getBean("libraryFactory");
改成
LibraryDao library=(LibraryDao)ctx.getBean("libraryTarget");
其它还是不变
-
@Override
-
public void addBook(Book book) {
-
String sql="INSERT INTO book_table VALUES(?,?)";
-
jdbcTemplate.update(sql, book.getBookName(),book.getBookId());
-
jdbcTemplate.update(sql, book.getBookName(),book.getBookId());
-
}
执行后
发现在抛出异常后,还是插入了一条数据,说明没有回滚事务。
三、基于tx/aop的配置
之前学了AOP,可以在想要的类或方法前面或后面织入增强,同理也可以用到这里来
上面的beans.xml文件可以改写成如下:
-
<?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:aop="http://www.springframework.org/schema/aop" xmlns:p="http://www.springframework.org/schema/p"
-
xmlns:tx="http://www.springframework.org/schema/tx"
-
xsi:schemaLocation="
-
http://www.springframework.org/schema/beans
-
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
-
http://www.springframework.org/schema/tx
-
http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
-
http://www.springframework.org/schema/aop
-
http://www.springframework.org/schema/aop/spring-aop-3.0.xsd
-
http://www.springframework.org/schema/context
-
http://www.springframework.org/schema/context/spring-context-3.0.xsd">
-
<!-- 配置数据源 -->
-
<bean id="dataSource"
-
class="org.springframework.jdbc.datasource.DriverManagerDataSource">
-
<property name="driverClassName" value="com.mysql.jdbc.Driver" />
-
<property name="url" value="jdbc:mysql://localhost:3306/test" />
-
<property name="username" value="root" />
-
<property name="password" value="[email protected]" />
-
</bean>
-
<!--配置一个JdbcTemplate实例,并将这个“共享的”,“安全的”实例注入到不同的DAO类中去 -->
-
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
-
<property name="dataSource" ref="dataSource" />
-
</bean>
-
<!-- 声明事务管理器 -->
-
<bean id="txManager"
-
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
-
<property name="dataSource" ref="dataSource" />
-
</bean>
-
<!-- 需要实施事务增强的目标业务Bean -->
-
<bean id="libraryTarget" class="com.mucfc.dao.LibraryDaoImpl"
-
p:jdbcTemplate-ref="jdbcTemplate" />
-
-
<!-- 使用tx/aop来配置 -->
-
<aop:config>
-
<!-- 通过aop定义事务增强切面 -->
-
<aop:pointcut id="serviceMethod"
-
expression="execution(* com.mucfc.dao.LibraryDaoImpl.*(..))" />
-
<!-- 引用事务增强 -->
-
<aop:advisor pointcut-ref="serviceMethod" advice-ref="txAdvice" />
-
</aop:config>
-
-
<!--事务增强 -->
-
<tx:advice id="txAdvice" transaction-manager="txManager">
-
<!-- 事务属性定义 -->
-
<tx:attributes>
-
<tx:method name="get*" read-only="false" />
-
<tx:method name="add*" rollback-for="Exception" />
-
<tx:method name="del*" />
-
</tx:attributes>
-
</tx:advice>
-
</beans>
AOP会自动将事务织入到相应的类的方法之上。重点代码如下
-
<!-- 使用tx/aop来配置 -->
-
<aop:config>
-
<!-- 通过aop定义事务增强切面 -->
-
<aop:pointcut id="serviceMethod"
-
expression="execution(* com.mucfc.dao.LibraryDaoImpl.*(..))" />
-
<!-- 引用事务增强 -->
-
<aop:advisor pointcut-ref="serviceMethod" advice-ref="txAdvice" />
-
</aop:config>
-
-
<!--事务增强 -->
-
<tx:advice id="txAdvice" transaction-manager="txManager">
-
<!-- 事务属性定义 -->
-
<tx:attributes>
-
<tx:method name="get*" read-only="false" />
-
<tx:method name="add*" rollback-for="Exception" />
-
<tx:method name="del*" />
-
</tx:attributes>
-
</tx:advice>
上面的这一段是重点。
接下来要来测试一上,把之前的LibraryDaoImpl类中的方法改成如下:
-
@Override
-
public void addBook(Book book) throws Exception{
-
String sql="INSERT INTO book_table VALUES(?,?)";
-
int num=(int)book.getBookId();
-
for(int i=1;i<=20;i++){
-
jdbcTemplate.update(sql, book.getBookName(),num++);
-
if(i==8) throw new Exception();
-
}
-
-
}
这里就是想插入20条数据,但是第8条插入时抛出异常,所以说最终应该会回滚,也就是说,一条数据也没有。
测试类:
-
package com.mucfc.service;
-
import org.springframework.context.ApplicationContext;
-
import org.springframework.context.support.ClassPathXmlApplicationContext;
-
-
import com.mucfc.dao.LibraryDao;
-
import com.mucfc.model.Book;
-
-
public class Test {
-
-
public static void main(String[] args) {
-
ApplicationContext ctx=new ClassPathXmlApplicationContext("beans.xml");
-
Book book1=new Book("西游记",1);
-
LibraryDao library=(LibraryDao)ctx.getBean("libraryTarget");
-
try {
-
library.addBook(book1);
-
} catch (Exception e) {
-
e.printStackTrace();
-
}
-
-
}
-
}
结果:
然后看看数据库中有没有插入
那如果注释掉这一段,结果又会如何呢?把AOP增强给注释,其它都不变。理论来讲应该会提交了8条数据
-
<!-- 使用tx/aop来配置 -->
-
<!-- <aop:config>
-
通过aop定义事务增强切面
-
<aop:pointcut id="serviceMethod"
-
expression="execution(* com.mucfc.dao.LibraryDaoImpl.*(..))" />
-
引用事务增强
-
<aop:advisor pointcut-ref="serviceMethod" advice-ref="txAdvice" />
-
</aop:config>
-
-
事务增强
-
<tx:advice id="txAdvice" transaction-manager="txManager">
-
事务属性定义
-
<tx:attributes>
-
<tx:method name="get*" read-only="false" />
-
<tx:method name="add*" rollback-for="Exception" />
-
<tx:method name="del*" />
-
</tx:attributes>
-
</tx:advice> -->
其它都不变,执行结果:
结果表明,如果 不进行事务管理,如果 出现错误,还是提交了数据操作。这不符合要求!
四、一些说明
-
<tx:advice id="defaultTxAdvice">
-
<tx:attributes>
-
<tx:method name="get*" read-only="true"/>
-
<tx:method name="*"/>
-
<tx:method name="add*" propagation="REQUIRED"/>
-
</tx:attributes>
-
</tx:advice>
需要注意的地方:
(1) advice(建议)的命名:由于每个模块都会有自己的Advice,所以在命名上需要作出规范,初步的构想就是模块名+Advice(只是一种命名规范)。
(2) tx:attribute标签所配置的是作为事务的方法的命名类型。
如<tx:method name="save*" propagation="REQUIRED"/>
其中*为通配符,即代表以save为开头的所有方法,即表示符合此命名规则的方法作为一个事务。
propagation="REQUIRED"代表支持当前事务,如果当前没有事务,就新建一个事务。这是最常见的选择。
(3) aop:pointcut标签配置参与事务的类,由于是在Service中进行数据库业务操作,配的应该是包含那些作为事务的方法的Service类。
首先应该特别注意的是id的命名,同样由于每个模块都有自己事务切面,所以我觉得初步的命名规则因为 all+模块名+ServiceMethod。而且每个模块之间不同之处还在于以下一句:
expression="execution(* com.test.testAda.test.model.service.*.*(..))"
其中第一个*代表返回值,第二*代表service下子包,第三个*代表方法名,“(..)”代表方法参数。
(4) aop:advisor标签就是把上面我们所配置的事务管理两部分属性整合起来作为整个事务管理。
<tx:method/>
有关的设置
属性 | 是否需要? | 默认值 | 描述 |
---|---|---|---|
name |
是 | 与事务属性关联的方法名。通配符(*)可以用来指定一批关联到相同的事务属性的方法。如: |
|
propagation |
不 | REQUIRED | 事务传播行为(配置文章开头) |
isolation |
不 | DEFAULT | 事务隔离级别(配置看文章开头)propagation="REQUIRED"代表支持当前事务,如果当前没有事务,就新建一个事务。这是最常见的选择。 |
timeout |
不 | -1 | 事务超时的时间(以秒为单位) |
read-only |
不 | false | 事务是否只读? |
rollback-for |
不 | 将被触发进行回滚的 |
|
no-rollback-for |
不 | 不 被触发进行回滚的 |