前面讲解了怎么使用@Transactional注解声明PersonServiceBean底下所有的业务方法需要事务管理,那么事务是如何来管理的呢?
我们知道当每个业务方法执行的时候,它都会打开事务,在业务方法执行结束之后,它就会结束事务。那么它什么时候决定这个事务提交,什么时候决定这个事务回滚呢?原先我们手工控制事务的时候,通常这个事务的提交或回滚是由我们来操纵的,那现在我们采用容器的声明式事务管理,那我们如何知道事务什么时候提交,什么时候回滚呢?答案是:Spring容器默认情况下对运行时异常,它会进行事务的回滚,如果它碰到的是用户异常,如检查时异常(checked exception),这时事务就不会回滚。
现在我们就来做一个实验。假设person表里面有如下记录:
如果现在我们要删除person表中id为5的记录,但是在PersonServiceBean类的delete()方法中,人为地抛出一个运行时异常,如下:
public void delete(Integer personid) {
jdbcTemplate.update("delete from person where id=?", new Object[]{personid},
new int[]{java.sql.Types.INTEGER});
throw new RuntimeException("运行期异常");
}
此时测试PersonServiceTest类中的delete()方法:
@Test
public void delete() {
personService.delete(5);
}
会发现Eclipse控制台打印出一个异常,立马查看person表,发现id为5的记录没有删除掉,这就说明了Spring容器默认情况下对运行时异常,它会进行事务的回滚。
如果在PersonServiceBean类的delete()方法中,人为地抛出一个检查时异常,如下:
public void delete(Integer personid) throws Exception {
jdbcTemplate.update("delete from person where id=?", new Object[]{personid},
new int[]{java.sql.Types.INTEGER});
throw new Exception("检查时异常");
}
为了不报错,我们还须将PersonService接口中的delete()方法签名修改为:
/**
* 删除指定id的person
*/
public void delete(Integer personid) throws Exception;
此时测试PersonServiceTest类中的delete()方法:
@Test
public void delete() {
try {
personService.delete(5);
} catch (Exception e) {
e.printStackTrace();
}
}
会发现Eclipse控制台打印出一个异常,立马查看person表,发现id为5的记录被删除掉,这就说明了如果Spring容器碰到的是用户异常,如检查时异常(checked exception),这时事务就不会回滚。
当然我们也可改变这种规则:
当Spring容器碰到用户异常——如检查时异常(checked exception)时,让事务回滚。
那到底该怎么办呢?此时就需要用到事务的rollbackFor属性了。我们将PersonServiceBean类的delete()方法修改为:
@Transactional(rollbackFor=Exception.class)
public void delete(Integer personid) throws Exception {
jdbcTemplate.update("delete from person where id=?", new Object[]{personid},
new int[]{java.sql.Types.INTEGER});
throw new Exception("检查时异常");
}
此时测试PersonServiceTest类中的delete()方法:
@Test
public void delete() {
try {
personService.delete(4);
} catch (Exception e) {
e.printStackTrace();
}
}
会发现Eclipse控制台打印出一个异常,立马查看person表,发现id为4的记录没有被删除掉。
当Spring容器碰到运行时异常时,不让它进行事务的回滚,而是提交事务。
那到底该怎么办呢?此时就需要用到事务的rollbackFor属性了。我们将PersonServiceBean类的delete()方法修改为:
@Transactional(noRollbackFor=RuntimeException.class)
public void delete(Integer personid) throws Exception {
jdbcTemplate.update("delete from person where id=?", new Object[]{personid},
new int[]{java.sql.Types.INTEGER});
throw new RuntimeException("运行期异常");
}
此时测试PersonServiceTest类中的delete()方法, 会发现Eclipse控制台打印出一个异常,立马查看person表,发现id为4的记录已经被删除掉了。
事务还有一些其他的特点,如在业务bean——PersonServiceBean中,有些业务方法是不需要进行事务管理的,比方说获取数据的方法,那么这个时候我们就需要用到事物的propagation属性了,该属性指定了事务的传播行为。所以,我们应将getPerson()方法的代码修改为:
/**
* 使用JdbcTemplate获取一条记录
*/
@Transactional(propagation=Propagation.NOT_SUPPORTED)
public Person getPerson(Integer personid) {
return jdbcTemplate.queryForObject("select * from person where id=?", new Object[]{personid},
new int[]{java.sql.Types.INTEGER}, new PeronRowMapper());
}
还要将getPersons()方法的代码修改为:
/**
* 使用JdbcTemplate获取多条记录
*/
@Transactional(propagation=Propagation.NOT_SUPPORTED)
public List getPersons() {
return jdbcTemplate.query("select * from person", new PeronRowMapper());
}
这样,当这2个业务方法执行的时候,它都不会开启事务,能节约资源,提供效率。
下面,我们就来对事务传播属性做一个总结:
接下来,我们着重介绍事务传播属性——NESTED。如有下面一段代码:
@Resource OtherService otherService;
public void xxx() {
stmt.executeUpdate("update person set name='888' where id=1");
otherService.update(); // OtherService的update()方法的事务传播属性为NESTED
stmt.executeUpdate("delete from person where id=9");
}
将以上代码展开,可能就变成了如下这样一段代码:
Connection conn = null;
try {
conn.setAutoCommit(false);
Statement stmt = conn.createStatement();
stmt.executeUpdate("update person set name='888' where id=1");
Savepoint savepoint = conn.setSavepoint(); // 保存点
try{
conn.createStatement().executeUpdate("update person set name='222' where sid=2");
}catch(Exception ex){
conn.rollback(savepoint);
}
stmt.executeUpdate("delete from person where id=9");
conn.commit();
stmt.close();
} catch (Exception e) {
conn.rollback();
}finally{
try {
if(null!=conn && !conn.isClosed()) conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
其中,OtherService中标注事务传播属性为NESTED的update()方法,就相当于这样一段代码:
Savepoint savepoint = conn.setSavepoint(); // 保存点
try{
conn.createStatement().executeUpdate("update person set name='222' where sid=2");
}catch(Exception ex){
conn.rollback(savepoint);
}
我们也就明白了内部事务的回滚不会对外部事务造成影响。
除了事务传播属性外,事务还有一些其他的属性:
事务的isolation属性指定了事务的隔离级别,实际上事务的隔离级别并不是由Spring容器决定的,而是由底层数据库决定的。
数据库系统提供了四种事务隔离级别供用户选择。不同的隔离级别采用不同的锁类型来实现,在四种隔离级别中,Serializable的隔离级别最高,但对并发访问数据库的性能影响最大。Read Uncommited的隔离级别最低。大多数据库默认的隔离级别为Read Commited,如SqlServer,当然也有少部分数据库默认的隔离级别为Repeatable Read,如Mysql。
脏读:一个事务读取到另一事务未提交的更新新据。前提是并发的两个或多个事务。
不可重复读:在同一事务中,多次读取同一数据返回的结果有所不同。换句话说就是,后续读取可以读到另一事务已提交的更新数据。相反,“可重复读”在同一事务中多次读取数据时,能够保证所读数据一样,也就是,后续读取不能读到另一事务已提交的更新数据。目前要实现可重复读的话,一般数据库采用快照技术,在某一时刻(点),当你访问数据的时候,它把这个数据作为一个镜像,以后在同一个事务中再去读取相同记录的数据时,它都可以从快照里面返回这个数据,不管外部怎么样对它操作,在多次读取的时候都不会受到影响。
幻读:一个事务读取到另一事务已提交的insert数据。