问题发现
在SimpleJdbcTemplate的UserDaoImpl中的addUser()方法中手动制造一个NullPointerException异常:
public void addUser(User user) {
Map<String, Object> params=new HashMap<String, Object>();
params.put("name", user.getName());
params.put("age", user.getAge());
params.put("pwd", user.getPassword());
jdbcTemplate.update(SQL_INSERT_USER, params);
String a = null;
a.toString();
}
测试addUser()方法:
public class TestJdbc {
public static void main(String[] args) {
ApplicationContext ac
= new ClassPathXmlApplicationContext("applicationContext.xml");
UserDao dao=ac.getBean("userDao",UserDao.class);
User u=new User();
u.setName("testUser");
u.setAge("24");
u.setPassword("123456");
dao.addUser(u);
}
}
执行时,控制台抛出NullPointerException
异常,但是数据插入成功了么?查询数据库:
SQL> select * from lzp.userinfo;
USERI NAME AGE PWD
----- ---------- --- ----------
8 testUser 24 123456
4 SCOTT 25 123456
发现虽然addUser()
方法抛出了异常,但是数据还是被成功的插入,这违背了事务的ACID原则。
Spring支持编程式事务管理和声明式事务管理。
- 编程式事务管理:事务和业务代码耦合度太高。
- 声明式事务管理:侵入性小,把事务从业务代码中抽离出来,使用AOP配置到配置文件中,提高维护性。
这里推荐使用声明式事务。
Spring事务核心接口API
如上图,Spring事务管理高层抽象主要有3个:PlatformTransactionManager
:事务管理器(用来管理事务,包含事务的提交,回滚)TransactionDefinition
:事务定义信息(隔离,传播,超时,只读)TransactionStatus
:事务具体运行状态
选择事务管理器
PlatformTransactionManager 是Spring的事务管理器核心接口。
Spring本身并不支持事务实现,只是负责包装底层事务,应用底层支持什么样的事务策略,Spring就支持什么样的事务策略。
里面提供了常用的操作事务的方法:
TransactionStatus getTransaction(TransactionDefinition definition)
:获取事务状态信息void commit(TransactionStatus status)
:提交事务void rollback(TransactionStatus status)
:回滚事务
Public interface PlatformTransactionManager()...{
// 由TransactionDefinition得到TransactionStatus对象
TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException;
// 提交
Void commit(TransactionStatus status) throws TransactionException;
// 回滚
Void rollback(TransactionStatus status) throws TransactionException;
}
JDBC事务管理器:
如果应用程序中直接使用JDBC来进行持久化,那么应该选择DataSourceTransactionManager
作为事务管理器:
<bean id="transactionManager" class="org.springframework.jdbc.datasourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
DataSource TransactionManager:使用JDBC和iBatis进行持久化数据时使用
Hibernate事务管理器:
<bean id="transactionManager" class="org.springframework.orm.hibernate3.HibernateTransactionManager">
<property name="sessionFactory" ref="sessionFactory" />
</bean>
Hibernate TransactionManager:使用Hibernate 3.0进行持久化数据时使用
Java持久化API事务(JPA):
<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="sessionFactory" ref="sessionFactory" />
</bean>
JpaTransactionManager:使用JPA进行持久化时使用
Java原生API事务:
<bean id="transactionManager" class="org.springframework.transaction.jta.JtaTransactionManager">
<property name="transactionManagerName" value="java:/TransactionManager" />
</bean>
JtaTransactionManager
:如果没有使用上述的事务管理,或者在一个事务跨越多个事务管理源时使用
TransactionDefinition信息对象
该接口定义了一些基本事务属性
public interface TransactionDefinition {
int getPropagationBehavior(); // 返回事务的传播行为
int getIsolationLevel(); // 返回事务的隔离级别,事务管理器根据它来控制另外一个事务可以看到本事务内的哪些数据
int getTimeout(); // 返回事务必须在多少秒内完成
boolean isReadOnly(); // 事务是否只读,事务管理器能够根据这个返回值进行优化,确保事务是只读的
}
TransactionStatus运行状态
该接口定义了事务具体的运行状态
public interface TransactionStatus{
boolean isNewTransaction(); // 是否是新的事物
boolean hasSavepoint(); // 是否有恢复点
void setRollbackOnly(); // 设置为只回滚
boolean isRollbackOnly(); // 是否为只回滚
boolean isCompleted; // 是否已完成
}
事务五大属性
传播行为
传播行为(propagation behavior)定义了客户端与被调用方法之间的事务边界。即何时要创建一个事务,或者何时使用已有的事务:
传播行为 | 含义 |
---|---|
PROPAGATION_MANDATORY | 表示该方法必须在事务中进行,如果当前事务不存在,则会抛出一个异常 |
PROPAGATION_NESTED | 如果当前已存在一个事务,那么该方法在嵌套事务中运行。 嵌套事务可以独立于当前事务进行单独地提交或回滚。如果当前事务不存在,那么行为和 PROPAGATION_REQUIRED一样 |
PROPAGATION_NEVER | 表示当前方法不运行在事务上下文中。如果当前正有一个 事务在运行,则会抛出异常 |
PROPAGATION_NOT_SUPPORTED | 表示该方法不应该运行在事务上下文中,如果存在当前事务,在该方法运行期间,当前事务会被挂起 |
PROPAGATION_REQUIRED | 表示当前方法必须运行在事务中。如果事务不存在,则启动一个新的事务;如果已经存在一个事务中,加入到这个事务中。 |
PROPAGATION_REQUIRED_NEW | 表示当前方法必须运行在它自己的事务中。一个新的事务将启动。如果存在当前事务,当前事务会被挂起 |
PROPAGATION_SUPPORTS | 表示当前方法不需要事务上下文,但是如果存在当前事务的话,那该方法会在这个事务中运行 |
绘制一个表格来表现他们的差异
定义serviceA.methodA()
以PROPAGATION_REQUIRED修饰;
定义serviceB.methodB()
以表格中三种方式修饰;
methodA中调用methodB
异常状态 | PROPAGATION_REQUIRES_NEW(两个独立事务) | PROPAGATION_NESTED (B的事务嵌套在A的事务中) | PROPAGATION_REQUIRED(同一个事务) |
---|---|---|---|
methodA抛异常 methodB正常 | A回滚,B正常提交 | A与B一起回滚 | A与B一起回滚 |
methodA正常 methodB抛异常 | 1.如果A中捕获B的异常,并没有继续向上抛异常,则B先回滚,A再正常提交; 2.如果A未捕获B的异常,默认则会将B的异常向上抛,则B先回滚,A再回滚 | B先回滚,A再正常提交 | A与B一起回滚 |
methodA抛异常 methodB抛异常 | B先回滚,A再回滚 | A与B一起回滚 | A与B一起回滚 |
methodA正常 methodB正常 | B先提交,A再提交 | A与B一起提交 | A与B一起提交 |
隔离级别
隔离级别(isolation level)定义了一个事务可能受其他并发事务的影响程度。并发操作相同的数据可能产生一些问题:
1.脏读(Dirty reads)—— 发生在一个事务读取了另一个事务改写后但未提交的数据,如果改写被回滚了,那么第一个事务获取的数据就是“脏”的。
2.不可重复读(Nonrepeatable read)—— 一个事务执行两次以上相同查询得到不同的数据。这通常是另外一个事务在此期间更新了数据。
3.幻读(Phantom read)—— 一个事务读取了几行数据,另一个事务插入了几条数据,当第一个事务再次读取时发现多了几条原本没有的数据。
隔离级别如下表所示:
隔离级别 | 含义 |
---|---|
ISOLATION_DEFAULT | 使用后端数据库默认的隔离级别 |
ISOLATION_READ_UNCOMMITTED | 允许读取尚未提交的数据表更。可能导致脏读,不可重复 读,幻读 |
ISOLATION_READ_COMMITTED | 允许读取并发事务已经提交的数据,可以阻止脏读,但不可重复读,幻读仍可能发生 |
ISOLATION_REPEATABLE_READ | 对同一字段的多次读取结果一致,除非数据是本事务自己修改的。可以阻止脏读,不可重复读,但仍可能发生幻读 |
REPEATABLE_SERIALIZABLE | 完全服从事务的ACID原则,避免脏读,不可重复读,幻读 |
可以看出,ISOLATION_READ_UNCOMMITTED
隔离级别是最低的,可能导致脏读,不可重复读,幻读。REPEATABLE_SERIALIZABLE
隔离级别最高,但是这会降低数据读取速率,它通常是通过完全锁定事务相关的数据库表来实现的。
只读
只读(read-only) 如果事务只读数据库进行读操作,那么设置该属性为true可以给数据库执行优化措施。
因为只读是在事务启动,由数据库实施的,所以对那些具备启动一个新的事务的传播行为(PROPAGATION_REQUIRED
,PROPAGATION_REQUIRED_NEW
和PROPAGATION_NESTED
)才有意义。
设置只读还会使Hibernate的flush模式被设置为FLUSH_NEVER。
事务超时
超时(timeout)假如事务运行时间过长,则会影响效率,所以可以设置超时属性,超时后执行自动回滚。因为超时时钟会在事务开启时启动,所以只有对那些具备启动一个新的事务的传播行为(PROPAGATION_REQUIRED
,PROPAGATION_REQUIRED_NEW
和PROPAGATION_NESTED
)才有意义。
回滚原则
默认情况遇到运行期异常就回滚。回滚原则可以定义遇到哪些异常不回滚。
编程式事务
编程式和声明式事务的区别
Spring提供了对编程式事务和声明式事务的支持,编程式事务允许用户在代码中精确定义事务的边界,而声明式事务(基于AOP)有助于用户将操作与事务规则进行解耦。
简单地说,编程式事务侵入到了业务代码里面,但是提供了更加详细的事务管理;而声明式事务由于基于AOP,所以既能起到事务管理的作用,又可以不影响业务代码的具体实现。
如何实现编程式事务?
spring提供了两种编程式事务管理,分别是使用TransactionTemplate
和使用PlatformTransactionManager
。
使用TransactionTemplate
采用TransactionTemplate
和采用其他Spring模板一样,使用回调方法,把应用程序从处理取得和释放资源中解放出来。TransactionTemplate
是线程安全的。代码示例如下:
TransactionTemplate tt = new TransactionTemplate(); // 新建一个TransactionTemplate
Object result = tt.execute(
new TransactionCallback(){
public Object doTransaction(TransactionStatus status){
updateOperation();
return resultOfUpdateOperation();
}
}); // 执行execute方法进行事务管理
使用TransactionCallback()
可以返回一个值。如果使用TransactionCallbackWithoutResult
则没有返回值。
使用PlatformTransactionManager
示例代码如下:
DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager(); //定义一个某个框架平台的TransactionManager,如JDBC、Hibernate
dataSourceTransactionManager.setDataSource(this.getJdbcTemplate().getDataSource()); // 设置数据源
DefaultTransactionDefinition transDef = new DefaultTransactionDefinition(); // 定义事务属性
transDef.setPropagationBehavior(DefaultTransactionDefinition.PROPAGATION_REQUIRED); // 设置传播行为属性
TransactionStatus status = dataSourceTransactionManager.getTransaction(transDef); // 获得事务状态
try {
// 数据库操作
dataSourceTransactionManager.commit(status);// 提交
} catch (Exception e) {
dataSourceTransactionManager.rollback(status);// 回滚
}
声明式事务管理
XML中定义事务
使用tx
命名空间配置:
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="add*" propagation="REQUIRED"/>
<tx:method name="*" propagation="SUPPORTS" read-only="true"/>
</tx:attributes>
</tx:advice>
transaction-manager
为上述的事务管理器。
<tx:method>
元素为某个(某些)name属性指定的方法定义事务参数。
<tx:method>
有多个属性来帮助定义方法的事务策略,这些属性对于上述的事务五大属性:
属性 | 含义 |
---|---|
isolation | 指定隔离级别 |
propagation | 指定传播原则 |
read-only | 指定事务为只读 |
回滚原则: rollback-for no-rollback-for | rollback-for指定事务为哪些检查型异常回滚 no-rollback-for指定事务对哪些异常继续运行而不回滚 |
timeout | 设定事务超时时间 |
我们还需设定哪些Bean应该被通知,使用aop定义一个通知器(advisor
):
<aop:config>
<aop:advisor
pointcut="execution(* *..UserDaoImpl.*(..))"
advice-ref="txAdvice"/>
</aop:config>
advice-ref
属性引用了名为txAdvice的通知。
现在测试添加事务后的addUser()
方法:
public class TestTransaction {
public static void main(String[] args) {
ApplicationContext ac
= new ClassPathXmlApplicationContext("applicationContext.xml");
UserDao dao=ac.getBean("userDao",UserDao.class);
User u=new User();
u.setName("testTx");
u.setAge("30");
u.setPassword("123456");
dao.addUser(u);
}
}
控制台抛出异常:
Exception in thread "main" java.lang.NullPointerException
at com.spring.dao.UserDaoImpl.addUser(UserDaoImpl.java:51)
查询数据:
SQL> select * from lzp.userinfo;
USERI NAME AGE PWD
----- ---------- --- ----------
8 testUser 24 123456
4 SCOTT 25 123456
可以发现testTx并没有被插入。
注解事务
除了使用XML定义事务,我们还可以注解事务,通过声明:
<tx:annotation-driven transaction-manager="transactionManager"/>
<tx:annotation-driven>
告诉Spring检查上下文中所有使用@Transaction
注解的Bean,不管这个注解是在类级别上还是方法级别上。
使用注解修改UserDaoImpl:
@Repository("userDao")
@Transactional(propagation=Propagation.SUPPORTS,readOnly=true)
public class UserDaoImpl implements UserDao {
//...
@Transactional(propagation=Propagation.REQUIRED,readOnly=false)
public void addUser(User user) {
Map<String, Object> params=new HashMap<String, Object>();
params.put("name", user.getName());
params.put("age", user.getAge());
params.put("pwd", user.getPassword());
jdbcTemplate.update(SQL_INSERT_USER, params);
String a=null;
a.toString();
}
//...
}
再次运行testTransaction()方法,抛出异常后查询数据库:
SQL> select * from lzp.userinfo;
USERI NAME AGE PWD
----- ---------- --- ----------
8 testUser 24 123456
4 SCOTT 25 123456
事物配置的注解和XML配置的选择
Xml配置 : 代码清晰,配置量少,容易维护
注解配置 : 侵入代码,每个Service层类都得配置,针对不同的方法还得配置事务细节:如查询方法配置只读操作,不容易维护
建议选择xml配置