虽然我们在用Spring的事务管理,但总体感觉使用的时候未知(未搞懂)的概念还是挺多的,心里总是没有底,在使用的过程中也比较容易用错误的方式来实现。
关于Spring声明式事务,具体可以参考下面这篇文章:
目前我们使用的是第五种方式,即全注解方式,这种方式使用比较方便,在spring的配置文件中声明:
...... <context:annotation-config /> <context:component-scan base-package="com.bluesky" /> <tx:annotation-driven transaction-manager="transactionManager" /> <bean id="sessionFactory" class="org.springframework.orm.hibernate3.LocalSessionFactoryBean"> <property name="configLocation" value="classpath:hibernate.cfg.xml" /> <property name="configurationClass" value="org.hibernate.cfg.AnnotationConfiguration" /> </bean> <!-- 定义事务管理器(声明式的事务) --> <bean id="transactionManager" class="org.springframework.orm.hibernate3.HibernateTransactionManager"> <property name="sessionFactory" ref="sessionFactory" /> </bean>
在Service层中需要加入事务的地方加入:
@Transactional @Component("userDao") public class UserDaoImpl extends HibernateDaoSupport implements UserDao { public List<User> listUsers() { return this.getSession().createQuery("from User").list(); } ...... }
我们使用@Transactional的时候,都是使用其默认值,但其中的行为如下:
传播属性
@Transactional注解中,默认的传播属性可以在源码中看到,属于PROPAGATION_REQUIRED--支持当前事务,如果当前没有事务,就新建一个事务,这是最常见的选择。
Propagation propagation() default Propagation.REQUIRED;
- PROPAGATION_REQUIRED--支持当前事务,如果当前没有事务,就新建一个事务。这是最常见的选择。
- PROPAGATION_SUPPORTS--支持当前事务,如果当前没有事务,就以非事务方式执行。
- PROPAGATION_MANDATORY--支持当前事务,如果当前没有事务,就抛出异常。
- PROPAGATION_REQUIRES_NEW--新建事务,如果当前存在事务,把当前事务挂起。
- PROPAGATION_NOT_SUPPORTED--以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
- PROPAGATION_NEVER--以非事务方式执行,如果当前存在事务,则抛出异常。
此外,在Spring中额外提供了另外一个事务管理方式,在EJB中并未对应,只在Spring的事务管理中存在:
PROPAGATION_NESTED:理解Nested的关键是savepoint。他与PROPAGATION_REQUIRES_NEW的区别是,PROPAGATION_REQUIRES_NEW另起一个事务,将会与他的父事务相互独立,而Nested的事务和他的父事务是相依的,他的提交是要等和他的父事务一块提交的。也就是说,如果父事务最后回滚,他也要回滚的,而Nested事务的好处是他有一个savepoint。
ServiceA { //事务属性配置为 PROPAGATION_REQUIRED void methodA() { try { //savepoint ServiceB.methodB(); //PROPAGATION_NESTED 级别 } catch (SomeException) { // 执行其他业务, 如 ServiceC.methodC(); } } }
也就是说ServiceB.methodB失败回滚,那么ServiceA.methodA也会回滚到savepoint点上,ServiceA.methodA可以选择另外一个分支,比如ServiceC.methodC,继续执行,来尝试完成自己的事务。但是这个事务并没有在EJB标准中定义。
问题:我们当前的所有查询其实都使用的默认传播属性REQUIRED,其实是不合理的,这会造成性能的略微下降;
隔离级别
事务的隔离级别属于数据库的支持,通常分为下面几类,由隔离级别从低到高:
1. ISOLATION_DEFAULT:这是一个PlatfromTransactionManager默认的隔离级别,使用数据库默认的事务隔离级别。另外四个与JDBC的隔离级别相对应
2. ISOLATION_READ_UNCOMMITTED:这是事务最低的隔离级别,它充许令外一个事务可以看到这个事务未提交的数据。这种隔离级别会产生脏读,不可重复读和幻像读。
3. ISOLATION_READ_COMMITTED: 保证一个事务不能读到另一个并行事务已修改但未提交的数据。数据提交后才能被读取。避免了脏数据。该级别适应于大多数系统。大多数主流数据库默认的级别。
4. ISOLATION_REPEATABLE_READ:它除了保证一个事务不能读取另一个事务未提交的数据外,还保证了避免下面的情况产生(不可重复读)。避免了脏读,不可重复读。但是可能出现幻像读。但是也带来更多的性能损失。
5. ISOLATION_SERIALIZABLE 事务被处理为顺序执行。除了防止脏读,不可重复读外,还避免了幻像读。这是花费最高代价但是最可靠的事务隔离级别。
我们当前的数据库隔离级别,如果没有设置,就为Isolation.DEFAULT,定义显示为使用底层数据源的默认隔离级别。除非DBA有额外设置,对于MySQL InnoDB引擎默认是可重复读的(REPEATABLE READ)
/** * Use the default isolation level of the underlying datastore. * All other levels correspond to the JDBC isolation levels. * @see java.sql.Connection */ int ISOLATION_DEFAULT = -1;
READ-ONLY
对于@Transactional注解中的read-only属性,之前感觉我们的理解都不算太透彻,可以参考下面这篇文章:
其概念为:从这一点设置的时间点开始(时间点a)到这个事务结束的过程中,其他事务所提交的数据,该事务将看不见!(查询中不会出现别人在时间点a之后提交的数据);
对于只读查询,可以指定事务类型为readonly,即只读事务,据同事讲,这个时候可以在其中操纵数据库,但是并不会commit到数据源中。
关于只读事务和无事务
那么问题来了,什么时候使用只读事务,什么时候将事务的传播属性改为SUPPORT?
由于只读事务不存在数据的修改,因此数据库将会为只读事务提供一些优化手段,例如Oracle对于只读事务,不启动回滚段,不记录回滚log。
但是如果连事务都不启动,从理论上讲性能应该更佳,参考其中的一篇blog来讲述这个问题:
- 如果你一次执行单条查询语句,则没有必要启用事务支持,数据库默认支持SQL执行期间的读一致性;
- 如果你一次执行多条查询语句,例如统计查询,报表查询,在这种场景下,多条查询SQL必须保证整体的读一致性,否则,在前条SQL查询之后,后条SQL查询之前,数据被其他用户改变,则该次整体的统计查询将会出现读数据不一致的状态,此时,应该启用事务支持。 【注意是一次执行多次查询来统计某些信息,这时为了保证数据整体的一致性,要用只读事务】
多数据源
如果多个数据源之间不存在分布式事务的话(即此时不会跨数据源对某个服务执行增删改等操作,查询无所谓),数据源之间可以采用直接在spring的配置文件中配置多个数据源,然后分别进行事务的配置。
在单数据源时,如果事务配置类型为SUPPORTS且没有嵌套在事务级别中,无论成功执行与否,方法都不会报错,只是无法进行数据库的增删改操作。
如果没有配置事务(服务上没有定义@Transactional注解),则会执行每一步就提交,异常不会影响之前已经提交的修改。
如果设置成REQUIRED,出现异常会导致整体事务的回退,如果不出现异常,一起提交。
比如我们有两个数据源,一个为master,一个为slave,这时就需要声明将两个transactionManager纳入事务管理:
<tx:annotation-driven transaction-manager="transactionManager"/> <tx:annotation-driven transaction-manager="transactionManagerSlave"/>
在其中定义两套dataSource, transactionManager, sqlSessionFactory,最重要的是两个MapperScannerConfigurer,通过这个能够正确区分出mapper包使用的事务:
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.api.example.pub.dao"/> <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/> </bean>
这就要求我们要将不同数据源之间使用的Mapper接口,实体类型,以及mybatis mapper文件隔离,以数据源区分。
但此时使用注解声明式事务还测试,还是不能保证正确的事务已经被加上,这是因为当存在多个TransactionManager时,Spring貌似默认使用第一个定义的(加载的)事务管理器,其不知道到底该使用哪个事务管理器。
@Transactional注解加入了一个value属性,用来手动来指定具体使用的事务管理器,可以参考:http://stackoverflow.com/questions/3333133/multiple-transaction-managers-with-transactional-annotation
@Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Inherited @Documented public @interface Transactional { /** * A qualifier value for the specified transaction. * <p>May be used to determine the target transaction manager, * matching the qualifier value (or the bean name) of a specific * {@link org.springframework.transaction.PlatformTransactionManager} * bean definition. */ String value() default "";
加入该属性,就可以正常使用@Transactional对应的事务管理器了,需要注意的是,这个我们所说的多个数据源管理并没有涉及到分布式事务,如果需要分布式事务的话这种配置方式肯定是不支持的。