一、 事务
事务管理对于企业应用而言是非常重要的,事务的存在保证了用户的每一次操作都是可靠的,当用户操作出现异常时也不至于破坏了后台的数据。例如银行的自动取款机,万一你在转账的时候出现了异常,事务机制会保证你后台的数据还是出异常操作之前的数据,也就是是你出异常的这些操作失效。
事务就是一组由于逻辑上紧密关联而合并成一个整体(工作单元)的多个数据库操作,这些操作要么都执行,要么都不执行。
银行转账操作:开启事务,就是保证转账的操作要么都执行,要么都不执行。
如果在你的账户减去转账金额后出现异常,不能只是你的账户扣钱,对方账户加钱,这不就赔大了吗;如果此处出现异常,因为该事务的操作并没有全执行完,事务就会回退到转账操作前,也就是“自己账户减去 转账金额”的这个操作失效,不会执行了,这就保证了你账户的数据不被破坏,等系统好了你就可以再次执行转账操作了。
从上面的例子我们看出:事务就必须确保出故障前对账户的操作不生效,就像用户刚才完全没有使用过取款机一样,以保证用户和银行的利益都不受损失。
二、事务的四个关键属性(ACID)
原子性(atomicity):事务的原子性表现为一个事务所涉及到的多个操作是捆绑在一起的,要求事务中的所有操作要么都执行,要么都不执行,就像上面例子中的转账操作。
一致性(consistency):一个事务中不管涉及到多少个操作,都必须保证事务执行之前数据是正确的,事务执行之后数据也是正确,如果在事务执行过程中,存在操作失败的操作则其他所有操作都必须撤销,将数据恢复到事务执行之前的状态,这个过程叫“回滚”。
隔离性(isolation):要求多个事务在并发执行过程中不会互相干扰。在应用程序实际执行的过程中,事务往往是兵法执行的,所以很可能许多事务会同时处理相同的数据,因此每个事务都应该与其他事务分开执行,不然同一个数据同时在多个事务中都被修改,那这个数据不就乱了吗?
持久性(durability):持久性是事务执行完成后,他对数据的修改是永久保存的,不会因为时间长,或者出现故障而受影响。也就是说并不能你执行转账操作后过了一段时间后你转出去的金额自己又回到你的账户,也就想想吧。
三、JDBC中的事务:
当在一个事务中执行多个操作时,要么所有的事务都被提交(commit),要么整个事务回滚(rollback)到最初状态
在JDBC中,事务默认是自动提交的,每次执行一个 SQL 语句时,如果执行成功,就会向数据库自动提交,而不能回滚
为了让多个 SQL 语句作为一个事务执行:
调用 Connection 对象的 setAutoCommit(false); 以取消自动提交事务
在所有的 SQL 语句都成功执行后,调用 commit(); 方法提交事务
在出现异常时,调用 rollback(); 方法回滚事务
可以通过Connection的getAutoCommit()方法来获得当前事务的提交方式
package cn.itcast.cd; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import org.junit.Test; import cn.itcast.cd.utils.DbUtils; public class TestTransaction { /* * 需求:张三给李四转账1000元 * * 转账成功的条件: * 1、判断张三的账号余额是否>=1000 * 2、从张三的账号中转出1000. * 3、给李四的账号中转入1000. */ @Test public void testTrans(){ Connection connection = DbUtils.getConnection(); try { //开启事务,默认的情况下是true, 只要执行sql就开启事务物,执行完sql后就提交事务connection.setAutoCommit(false);
PreparedStatement preparedStatement = null; ResultSet resultSet = null; String sql = ""; try { sql = "select * from accounts where name=?"; preparedStatement = connection.prepareStatement(sql); preparedStatement.setString(1, "张三"); resultSet = preparedStatement.executeQuery(); if (resultSet.next()){ Double sum = resultSet.getDouble("sum"); //判断余额 if (sum < 1000){ System.out.println("余额不足,操作失败!"); return; } //转出 sql = "update accounts set sum=sum-? where name=?"; preparedStatement = connection.prepareStatement(sql); preparedStatement.setInt(1, 1000); preparedStatement.setString(2, "张三"); preparedStatement.executeUpdate(); //创建异常,没有事务则转出了1000,发生异常,转入失败,整个转账操作失败! int a = 2/0; //转入 sql = "update accounts set sum=sum+? where name=?"; preparedStatement = connection.prepareStatement(sql); preparedStatement.setInt(1, 1000); preparedStatement.setString(2, "李四"); preparedStatement.executeUpdate(); //提交事务,表明整个转账操作成功,将修改的数据更新到数据库中, 事务就又变成了自动提交. connection.commit(); } } catch (SQLException e) { e.printStackTrace(); } finally{ DbUtils.close(connection, preparedStatement, resultSet); } } catch (Exception e) { e.printStackTrace(); //发生异常,事务回滚到原始状态. try { connection.rollback(); } catch (SQLException e1) { e1.printStackTrace(); } } } }
四、spring事务
主要有两种方式实现,一种是在代码中编写(编程式事务管理),一种是声明式事务(又可分为AOP式事务管理和注解式事务管理)。在代码中编写要更加细粒度,而很多时候我们只需要简单的事务处理,那就可以用声明式事务。
从图中可以看出,spring并不直接管理事务,而是提供了一个事务接口,实现事务的具体方法是由底层自己本身决定,如果管理JDBC DataSource事务就使用DataSourceTransactionManager事务管理器,如果管理Hibernate事务,则使用HibernateTransactionManager事务管理器;也就是说Spring管理机制是一种策略模式,他只提供接口,用哪种具体的事务管理器由持久层的实现框架决定。
五、编程式事务 :
编程式事务就是用编码的方式实现事务,比较类似于JDBC编程实现事务。
下面实例使用DataSourceTransactionManager来管理JDBC事务。
spring实现编程式事务的方式方式中提供了一种模板类,能够帮助我们实现DataSourceTransactionManager管理事务,这中模板类似于JDBCTemplate模板,将数据操作封装在模板类中。
实例:实现账号
1)创建数据库
2)配置好spring环境
<!-- 引入外部属性文件 --> <context:property-placeholder location="classpath:jdbc.properties"/> <!-- 配置C3p0 --> <bean id="comboPooledDataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"> <property name="user" value="${jdbc.username}"></property> <property name="password" value="${jdbc.password}"></property> <property name="driverClass" value="${jdbc.driver}"></property> <property name="jdbcUrl" value="${jdbc.url}"></property> </bean> <!-- 配置业务层类 --> <bean id="accountService" class="com.neuedu.spring.demo1.AccountService"> <property name="accountDao" ref="accountDao"></property> <!-- 注入事务管理的模板类 --> <property name="atTemplate" ref="transactionTemplate"></property> </bean> <!-- 配置DAO类 --> <bean id="accountDao" class="com.neuedu.spring.demo1.AccountDaoImpl"> <!-- 注入连接池 --> <property name="dataSource" ref="comboPooledDataSource"></property> </bean> <!-- 配置事务管理器 --> <bean id="dataSourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="comboPooledDataSource"></property> </bean> <!-- 配置事务管理的模板:spring为了简化事务管理代码提供的模板 --> <bean id="transactionTemplate" class="org.springframework.transaction.support.TransactionTemplate"> <property name="transactionManager" ref="dataSourceTransactionManager"></property> </bean>
3)DAO层实现类
package com.neuedu.spring.demo1; import org.springframework.jdbc.core.support.JdbcDaoSupport; /* * 项目名称:spring-BankAccount * @author:wzc * @date 创建时间:2017年9月12日 下午5:48:06 * @Description:实现转账DAO * @parameter * */ public class AccountDaoImpl extends JdbcDaoSupport implements AccountDao { /*Method:outMoney *Description:实现从数据库中将账户转账金额扣除 *param:out,转账账户 *param:money,转账金额 */ @Override public void outMoney(String out, Double money) { String sql="update t_account set salary=salary-? where name=?"; this.getJdbcTemplate().update(sql, money,out); } /*Method:inMoney *Description:实现从数据库中向账户转入转账金额 *param:in,收款账户 *param:money,转账金额 */ @Override public void inMoney(String in, Double money) { String sql="update t_account set salary=salary+? where name=?"; this.getJdbcTemplate().update(sql, money,in); } }
4)业务层实现类
package com.neuedu.spring.demo1; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.support.TransactionCallbackWithoutResult; import org.springframework.transaction.support.TransactionTemplate; /* * 项目名称:spring-BankAccount * @author:wzc * @date 创建时间:2017年9月12日 下午5:39:04 * @Description:转账业务层实现 * @parameter * */ public class AccountService implements AccountServiceIn{ //注入accountDao private AccountDaoImpl accountDao; public void setAccountDao(AccountDaoImpl accountDao) { this.accountDao = accountDao; } //注入事务管理的模板 private TransactionTemplate atTemplate; public void setAtTemplate(TransactionTemplate atTemplate) { this.atTemplate = atTemplate; } /*Method:transfer *Description:实现转账操作 *param:out ,转出账户 *param:in,转入账户 *param:money 转账金额 */ @Override public void transfer(String out, String in, Double money) { atTemplate.execute(new TransactionCallbackWithoutResult() { @Override protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) { //调用DAO层实现具体转账操作 accountDao.outMoney(out, 200d); //人为制造Debug //int i=10/0; accountDao.inMoney(in, 200d); } }); } }
测试:
@Test public void test() { ApplicationContext ioc=new ClassPathXmlApplicationContext("spring.xml"); AccountService aService = ioc.getBean(AccountService.class); aService.transfer("孙悟空", "猪八戒", 200d); }
结果:
现在给转账操作人为制作bug,在测试一下事务是否正常。
六、spring事务的概念
(一)基本原理:AOP
[1]前置通知:开启事务
[2]返回通知:提交事务
[3]异常通知:回滚事务
[4]后置通知:释放资源
(二)事务的传播行为
当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。事务的传播行为可以由传播属性指定,在TransactionDefinition定义了7种类传播行为。
(三)事务的隔离级别
数据库事务并发存在的问题
假设现在有两个事务:Transaction01和Transaction02并发执行。
①脏读
[1]Transaction01将某条记录的AGE值从20修改为30。
[2]Transaction02读取了Transaction01更新后的值:30。
[3]Transaction01回滚,AGE值恢复到了20。
[4]Transaction02读取到的30就是一个无效的值。
②不可重复读
[1]Transaction01读取了AGE值为20。
[2]Transaction02将AGE值修改为30。
[3]Transaction01再次读取AGE值为30,和第一次读取不一致。
③幻读
[1]Transaction01读取了STUDENT表中的一部分数据。
[2]Transaction02向STUDENT表中插入了新的行。
[3]Transaction01读取了STUDENT表时,多出了一些行。
数据库系统必须具有隔离并发运行各个事务的能力,使它们不会相互影响,避免各种并发问题。一个事务与其他事务隔离的程度称为隔离级别。SQL标准中规定了多种事务隔离级别,不同隔离级别对应不同的干扰程度,隔离级别越高,数据一致性就越好,但并发性越弱。
TransactionDefinition 接口中定义了五个表示隔离级别的常量,其中DEFAULT:这是默认值,表示使用底层数据库的默认隔离级别。对大部分数据库而言,通常这值就是READ_COMMITTED。以下是其他四种的具体情况:
①读未提交:READ UNCOMMITTED
允许Transaction01读取Transaction02未提交的修改。
②读已提交:READ COMMITTED
要求Transaction01只能读取Transaction02已提交的修改。
③可重复读:REPEATABLE READ
确保Transaction01可以多次从一个字段中读取到相同的值,即Transaction01执行期间禁止其它事务对这个字段进行更新。
④串行化:SERIALIZABLE
确保Transaction01可以多次从一个表中读取到相同的行,在Transaction01执行期间,禁止其它事务对这个表进行添加、更新、删除操作。可以避免任何并发问题,但性能十分低下
各个隔离级别解决并发问题的能力见下表
(四)事务超时
所谓事务超时,就是指一个事务所允许执行的最长时间,如果超过该时间限制但事务还没有完成,则自动回滚事务。在 TransactionDefinition 中以 int 的值来表示超时时间,其单位是秒。
默认设置为底层事务系统的超时值,如果底层数据库事务系统没有设置超时值,那么就是none,没有超时限制。
(五)事务只读属性
只读事务属性: 表示这个事务只读取数据但不更新数据, 这样可以帮助数据库引擎优化事务。
七、声明式事务:
声明式事务是建立在AOP之上的。其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。声明式事务最大的优点就是不需要通过编程的方式管理事务,这样就不需要在业务逻辑代码中掺杂事务管理的代码,只需在配置文件中做相关的事务规则声明(或通过基于@Transactional注解的方式),便可以将事务规则应用到业务逻辑中。
方法一:基于注解的声明式事务(最简单)
1)数据库扔用上面的数据库;
2)配置spring
<!-- 配置业务层类 --> <bean id="accountService" class="com.neuedu.spring.demo2.AccountService"> <property name="accountDao" ref="accountDao"></property> </bean> <!-- 配置DAO类 --> <bean id="accountDao" class="com.neuedu.spring.demo2.AccountDaoImpl"> <!-- 注入连接池 --> <property name="dataSource" ref="comboPooledDataSource"></property> </bean><!-- 配置事务管理器 --> <bean id="dataSourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="comboPooledDataSource"></property> </bean><!-- 开启基于注解的事务管理 --> <tx:annotation-driven transaction-manager="dataSourceTransactionManager"/>
3)DAO层不变
package com.neuedu.spring.demo2; import org.springframework.jdbc.core.support.JdbcDaoSupport; /* * 项目名称:spring-BankAccount * @author:wzc * @date 创建时间:2017年9月12日 下午5:48:06 * @Description:实现转账DAO * @parameter * */ public class AccountDaoImpl extends JdbcDaoSupport implements AccountDao { /*Method:outMoney *Description:实现从数据库中将账户转账金额扣除 *param:out,转账账户 *param:money,转账金额 */ @Override public void outMoney(String out, Double money) { String sql="update t_account set salary=salary-? where name=?"; this.getJdbcTemplate().update(sql, money,out); } /*Method:inMoney *Description:实现从数据库中向账户转入转账金额 *param:in,收款账户 *param:money,转账金额 */ @Override public void inMoney(String in, Double money) { String sql="update t_account set salary=salary+? where name=?"; this.getJdbcTemplate().update(sql, money,in); } }
4)在业务层service类上加入注解
package com.neuedu.spring.demo2; import org.springframework.transaction.annotation.Transactional; /* * 项目名称:spring-BankAccount * @author:wzc * @date 创建时间:2017年9月12日 下午5:39:04 * @Description:转账业务层实现 * @parameter * */ @Transactional() public class AccountService implements AccountServiceIn{ //注入accountDao private AccountDaoImpl accountDao; public void setAccountDao(AccountDaoImpl accountDao) { this.accountDao = accountDao; } /*Method:transfer *Description:实现转账操作 *param:out ,转出账户 *param:in,转入账户 *param:money 转账金额 */ @Override public void transfer(String out, String in, Double money) { //调用DAO层实现具体转账操作 accountDao.outMoney(out, 200d); //人为制造Debug //int i=10/0; accountDao.inMoney(in, 200d); } }
因为刚才正常执行后悟空八戒分别是800,1200,接着执行异常情况,我并没有恢复数据库数据,执行结果是800,1200,说明因为异常没有再转账。
方法二:基于AspectJ的XML声明式事务:
基于AspectJ的XML声明式事务,实际就是AOP的XML实现方式。
spring配置:
<!-- 引入外部属性文件 --> <context:property-placeholder location="classpath:jdbc.properties"/> <!-- 配置C3p0 --> <bean id="comboPooledDataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"> <property name="user" value="${jdbc.username}"></property> <property name="password" value="${jdbc.password}"></property> <property name="driverClass" value="${jdbc.driver}"></property> <property name="jdbcUrl" value="${jdbc.url}"></property> </bean> <!-- 配置业务层类 --> <bean id="accountService" class="com.neuedu.spring.demo3.AccountService"> <property name="accountDao" ref="accountDao"></property> </bean> <!-- 配置DAO类 --> <bean id="accountDao" class="com.neuedu.spring.demo3.AccountDaoImpl"> <!-- 注入连接池 --> <property name="dataSource" ref="comboPooledDataSource"></property> </bean><!-- 配置事务管理器 --> <bean id="dataSourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="comboPooledDataSource"></property> </bean> <!-- 配置事务的通知,因为事务是建立在AOP上的 --> <tx:advice id="txAdivice" transaction-manager="dataSourceTransactionManager"> <tx:attributes> <!-- 配置事务管理的方法 --> <tx:method name="transfer" propagation="REQUIRED"/> </tx:attributes> </tx:advice> <!-- 配置切面 --> <aop:config> <!-- 配置切入点表达式 --> <aop:pointcut expression="execution(* com.neuedu.spring.demo3.AccountService+.*(..))" id="pointcut"/> <!-- 配置切面 --> <aop:advisor advice-ref="txAdivice" pointcut-ref="pointcut"/> </aop:config>
然后DAO,Service都是业务正常的代码,里面没有关于事务的任何代码,显然声明式事务管理要优于编程式事务管理,这正是spring倡导的非侵入式的开发方式,使业务代码不受污。
dao
package com.neuedu.spring.demo3; import org.springframework.jdbc.core.support.JdbcDaoSupport; /* * 项目名称:spring-BankAccount * @author:wzc * @date 创建时间:2017年9月12日 下午5:48:06 * @Description:实现转账DAO * @parameter * */ public class AccountDaoImpl extends JdbcDaoSupport implements AccountDao { /*Method:outMoney *Description:实现从数据库中将账户转账金额扣除 *param:out,转账账户 *param:money,转账金额 */ @Override public void outMoney(String out, Double money) { String sql="update t_account set salary=salary-? where name=?"; this.getJdbcTemplate().update(sql, money,out); } /*Method:inMoney *Description:实现从数据库中向账户转入转账金额 *param:in,收款账户 *param:money,转账金额 */ @Override public void inMoney(String in, Double money) { String sql="update t_account set salary=salary+? where name=?"; this.getJdbcTemplate().update(sql, money,in); } }
service
package com.neuedu.spring.demo3; import org.springframework.transaction.annotation.Transactional; /* * 项目名称:spring-BankAccount * @author:wzc * @date 创建时间:2017年9月12日 下午5:39:04 * @Description:转账业务层实现 * @parameter * */ public class AccountService implements AccountServiceIn{ //注入accountDao private AccountDaoImpl accountDao; public void setAccountDao(AccountDaoImpl accountDao) { this.accountDao = accountDao; } /*Method:transfer *Description:实现转账操作 *param:out ,转出账户 *param:in,转入账户 *param:money 转账金额 */ @Override public void transfer(String out, String in, Double money) { //调用DAO层实现具体转账操作 accountDao.outMoney(out, 200d); //人为制造Debug //int i=10/0; accountDao.inMoney(in, 200d); } }
而此时如何配置事务属性呢?在配置事务管理方法的时候可以添加事务的属性。