事务简介
事务是对数据库操作的一组序列。当操作序列中的所有操作都成功执行时,事务执行成功。当序列中任一操作失败时,此时对数据库的操作返回至事务未执行的状态。
事务的4个特性
事务的特性可以总结为ACID
原子性(Atomicity):事务里的操作看做一个整体,是不可分割的原子单位,其所有操作要么全部成功,要么全部失败;
一致性(Consistency):事务一旦完成,其业务结果必须有一致性,而不是部分成功,部分失败;
隔离性(Isolation):不同的事务并行,必须不能互相影响各自事务的运行;
永久性(Durability):事务一旦完成,无论发生什么系统错误,其结果有不可逆性,不受影响。
事务并行带来的可能问题
多个事务并发运行,处理相同的数据,可能会带来一下三个问题:
脏读:一个事务在事务过程中读取另一个事务修改但未提交的数据,如果修改的数据被回滚,那么第一个事务获取的数据就是无效的;
不可重复读:在一次事务中进行了两次或以上的查询,但是两次查询的结果不一致。因为另一个事务在查询过程中进行了更新(重点是修改);
幻读:与不可重复读类似。发生在第一个事务先查询了几条数据,然后另一个事务又插入或者删除了数据,在随后的查询中,第一个事务就多了或者少了一些原本不存在的数据(重点是新增或者删除)。
为了解决上述问题,事务定义了几个隔离级别:
Read uncommitted:读未提交,最低级别的事务隔离级别,基本不用,会产生脏读情况。对于其他事务未提交的修改操作,本事务查询都会查得到。
Read committed:读已提交。一个事务要等待其他事务提交后才能读取数据。但不能避免不可重复读情况,即在事务的两次查询过程中,另一个事务修改了数据,则两次查询结果不一致;
Repeatable read:可重复读。一个事务过程中的多次查询结果是一致的。(幻读:假设表里有十条记录。ab事务各开启,b事务插入一条并提交(此时b事务查询应该是11条),a在当前事务查询仍旧是10条,等a提交事务后再提交,就变成11条??这其实已经解决了幻读问题了吧);
串行化:所有事务必须串行执行。
Spring事务的基本属性
spring通过一些配置,可以定义事务策略如何应用到方法上。其包含了一下5个部分:事务的传播行为,隔离规则,回滚规则,事务超时,是否只读。
回滚规则:
定义了事务方法中哪些异常会导致事务回滚而哪些异常不会回滚。在spring事务中,默认的回滚规则是只有遇到运行时异常才会回滚(即除数为0,数组越界等异常),而已检查异常(即操作文件流等需要进行try。。。catch等的异常)默认是不会回滚的。可以通过在注解中设置@Transactional()rollbackFor=Exception.class)来确保所有的异常都能被回滚。另外,如果异常被try。。。catch了,则spring无法捕获异常了,此时即使有运行时异常也不会回滚,因此在程序中,可以在catch中继续抛出异常throw e来确保事务方法遇到异常能进行回滚。
传播行为:
当事务方法被另一个事务方法调用时,应当制定事务应如何传播。根据是否需要事务环境可以分为以下其中:
PROPAGATION_REQUIRED:表示当前的方法必须运行在事务中,如果当前事务存在,那就运行在该事务中,否则启动一个新的事务。这是默认的传播行为。
PROPAGATION_SUPPORTS:表示当前的方法可以运行在事务中,如果当前事务存在,那就运行在该事务中,否则就在没有事务的环境下运行。
PROPAGATION_MANDATORY:表示当前的方法必须运行在事务中,如果当前事务存在,那就运行在该事务中,否则就报异常。
PROPAGATION_REQUIRED_NEW:表示当前的方法必须运行在自己的事务中,一个新的事务将被启动。如果存在当前事务,在该方法执行期间,当前事务会被挂起。
PROPAGATION_NOT_SUPPORTED:表示当前的方法不应该运行在自己的事务中,,在该方法执行期间,当前事务会被挂起。
PROPAGATION_NEVER:表示当前的方法不应该运行在事务中,在该方法执行期间,如果有事务则会抛出异常。
PROPAGATION_NESTED:表示如果当前已经有一个事务,那么当前方法将会嵌套一个新的事务,新的事务独立于当前事务进行单独的提交或者回滚,如果当前事务不存在,那么其行为与PROPAGATION_REQUIRED一样。
嵌套事务一个非常重要的概念就是内层事务依赖于外层事务。外层事务失败时,会回滚内层事务所做的动作。而内层事务操作失败并不会引起外层事务的回滚。
PROPAGATION_NESTED 与PROPAGATION_REQUIRES_NEW的区别:它们非常类似,都像一个嵌套事务,如果不存在一个活动的事务,都会开启一个新的事务。使用 PROPAGATION_REQUIRES_NEW时,内层事务与外层事务就像两个独立的事务一样,一旦内层事务进行了提交后,外层事务不能对其进行回滚。两个事务互不影响。
具体例子可参考https://segmentfault.com/a/1190000013341344#articleHeader16
事务的隔离级别:
见上,主要是分四种,读未提交,读已提交,可重复读,串行化。
事务的超时
超时设置主要是为了避免事务长时间运行会导致锁住后端的数据库,设置超时后,如果在规定的时间内没有执行完毕,那么就会自动的回滚事务。
是否只读
定义事务是否为只读事务,只读事务不对数据库进行修改。只读事务中,从这一点设置的时间点开始(时间点a)到这个事务结束的过程中,其他事务所提交的数据,该事务将看不见!
事务的配置
jdbc处理事务
/*
* jdbc操作事务
*/
@Test
public void demo6(){
Connection conn =null;
try {
conn = getConn();
conn.setAutoCommit(false);
String sql="update bank set money=money+? where username=?";
PreparedStatement pstmt=(PreparedStatement) conn.prepareStatement(sql);
pstmt.setInt(1, 100);
pstmt.setString(2,"andy");
pstmt.executeUpdate();
int i=1/0;
String sql2="update bank set money=money-? where username=?";
PreparedStatement pstmt2=(PreparedStatement) conn.prepareStatement(sql);
pstmt2.setInt(1, 100);
pstmt2.setString(2,"李琳");
pstmt2.executeUpdate();
conn.commit();
} catch (Exception e) {
// TODO: handle exception
try {
conn.rollback();
} catch (Exception e2) {
// TODO: handle exception
}
}
}
public static Connection getConn(){
DriverManagerDataSource dataSource=new DriverManagerDataSource();
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306/test?characterEncoding=utf-8");
dataSource.setUsername("root");
dataSource.setPassword("123456");
Connection conn=null;
try {
Class.forName("com.mysql.jdbc.Driver");
conn=(Connection) DriverManager.getConnection("jdbc:mysql://localhost:3306/test?characterEncoding=utf-8", "root", "123456");
} catch (Exception e) {
// TODO: handle exception
}
return conn;
}
其标准用法是:
try{
conn = getConn();
conn.setAutoCommit(false);
...
conn.commit();
}catch(){
conn.rollback();
}
编程式事务
jdbc的事务每次都需要commit和rollback等待,spring将这些操作抽象出来建立模板TransactionTemplate,我们可以使用这个模板,通过模板中的回调方式来完成事务中的业务逻辑。
首先在xml中配置事务模板:
<!-- 配置数据库连接池 -->
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
<property name="url" value="jdbc:mysql://localhost:3306/test?characterEncoding=utf-8"></property>
<property name="username" value="root"></property>
<property name="password" value="123456"></property>
</bean>
<!-- 配置jdbctemplate -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!--配置事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!--配置事务模板 -->
<bean id="transcationTemplate" class="org.springframework.transaction.support.TransactionTemplate">
<property name="transactionManager" ref="transactionManager"></property>
</bean>
<!-- 2.Dao -->
<bean name="accountDao" class="com.test.spring.txcode.dao.AccountDaoImpl">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 3.Service -->
<bean name="accountService" class="com.test.spring.txcode.service.AccountServiceImpl">
<property name="ad" ref="accountDao"></property>
<!-- <property name="tt" ref="transactionTemplate" ></property> -->
</bean>
然后在代码中,完成事务模板的回调
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
// TODO Auto-generated method stub
//减钱
ad.decreaseMoney(from, money);
int i = 1/0;
//加钱
ad.increaseMoney(to, money);
}
});
上面的编程式事务虽然解决了jdbc事务中每次的 conn.setAutoCommit(false); conn.commit();等操作,但是仍然需要为每个事务编写回调方法,既麻烦又会有代码耦合的问题,其实spring事务可以通过基于aop配置,通过xml或者注解完成。
基于springAOP的xml配置事务
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p"
xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
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
http://code.alibabatech.com/schema/dubbo http://code.alibabatech.com/schema/dubbo/dubbo.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd">
<!-- 配置数据库连接池 -->
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
<property name="url" value="jdbc:mysql://localhost:3306/test?characterEncoding=utf-8"></property>
<property name="username" value="root"></property>
<property name="password" value="123456"></property>
</bean>
<!-- 配置jdbctemplate -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!--配置事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!--配置事务 -->
<!-- 2.Dao -->
<bean name="accountDao" class="com.test.spring.tx.dao.AccountDaoImpl">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 3.Service -->
<bean name="accountService" class="com.test.spring.tx.service.AccountServiceImpl">
<property name="ad" ref="accountDao"></property>
<!-- <property name="tt" ref="transactionTemplate" ></property> -->
</bean>
<!--配置事务的增强,类似于通知 -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<!--定义每个方法的通知属性 -->
<tx:method name="transfer" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
<!--配置切入点 -->
<aop:config>
<aop:pointcut expression="execution(* com.test.spring.tx.service.AccountServiceImpl.*(..))" id="pointcut1"></aop:pointcut>
<!-- <aop:advice advice-ref="txAdvice" pointcut-ref="pointcut1"></aop:advice> -->
<aop:advisor advice-ref="txAdvice" pointcut-ref="pointcut1" />
</aop:config>
</beans>
通过xml将事务通知切入到需要被事务管理的方法(业务逻辑中)
通过注解配置
只需在xml配置文件中加入如下
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p"
xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
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
http://code.alibabatech.com/schema/dubbo http://code.alibabatech.com/schema/dubbo/dubbo.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd">
<!-- 配置数据库连接池 -->
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
<property name="url" value="jdbc:mysql://localhost:3306/test?characterEncoding=utf-8"></property>
<property name="username" value="root"></property>
<property name="password" value="123456"></property>
</bean>
<!-- 配置jdbctemplate -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!--配置事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!--配置事务 -->
<!-- 2.Dao -->
<bean name="accountDao" class="com.test.spring.tx.dao.AccountDaoImpl">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 3.Service -->
<bean name="accountService" class="com.test.spring.tx.service.AccountServiceImpl">
<property name="ad" ref="accountDao"></property>
<!-- <property name="tt" ref="transactionTemplate" ></property> -->
</bean>
<!--开启事务注解 -->
<tx:annotation-driven transaction-manager="transactionManager"/>
</beans>
然后在需要被事务管理的方法中头上加入@Transactional(isolation=Isolation.DEFAULT)即可。
Spring的事务管理机制实现的原理,其实就是通过一个动态代理对所有需要事务管理的Bean进行加载,并根据配置在invoke方法中对当前调用的 方法名进行判定,并在method.invoke方法前后为其加上合适的事务管理代码,这样就实现了Spring式的事务管理。