Spring中的事务管理
事务(Transaction)是访问并可能更新数据库中各种数据项的一个程序执行单元(unit)。事务通常由高级数据库操纵语言或编程语言(如SQL,C++或Java)书写的用户 程序的执行所引起,并用形如begin transaction和end transaction语句(或函数调用)来界定。事务由事务开始(begin transaction)和事务结束(end transaction)之间执行的全体操作组成。
简单点说,事务就是一组逻辑操作,这组逻辑操作拥有ACID4个属性规范:
- 原子性(atomicity):一个事务是一个不可分割的工作单位,事务中包括的诸操作要么都做,要么都不做。(要么成功,要么失败)
- 一致性(consistency):事务必须是使数据库从一个一致性状态变到另一个一致性状态。一致性与原子性是密切相关的。
- 隔离性(isolation):一个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。
- 持久性(durability):持久性也称永久性(permanence),指一个事务一旦提交,它对数据库中数据的改变就应该是永久性的。接下来的其他操作或故障不应该对其有任何影响。
也就是说事务的概念来源于对数据库的访问及操作,而事务并发问题使得事务管理成为了开发中必须关注的一个点。
spring作为java开发的一个顶级的一站式开发框架,其中就包含了声明式事务的支持。详细如下图:
事务中的几个重要属性:
隔离问题:
- 脏读:一个事务读到另一个事务没有提交的数据
- 不可重复读:一个事务读到另一个事务已提交的数据(update)
- 虚读(幻读):一个事务读到另一个事务已提交的数据(insert)
隔离级别:
read uncommitted:读未提交。存在3个问题
read committed:读已提交。解决脏读,存在2个问题
repeatable read:可重复读。解决:脏读、不可重复读,存在1个问题。
serializable :串行化。都解决,单事务。
- mysql 事务操作--简单:假设事务包含a,b,c,d4个数据库操作,那么这个事务的逻辑代码一般是这样
-
Connection conn = null; try{ //1 获得连接 conn = ...; //2 开启事务 conn.setAutoCommit(false); A B C D //3 提交事务 conn.commit(); } catche(){ //4 回滚事务 conn.rollback(); }
Connection conn = null; try{ //1 获得连接 conn = ...; //2 开启事务 conn.setAutoCommit(false); A B C D //3 提交事务 conn.commit(); } catche(){ //4 回滚事务 conn.rollback(); }
- mysql 事务操作--Savepoint
-
需求:AB(必须),CD(可选) Connection conn = null; Savepoint savepoint = null; //保存点,记录操作的当前位置,之后可以回滚到指定的位置。(可以回滚一部分) try{ //1 获得连接 conn = ...; //2 开启事务 conn.setAutoCommit(false); A B savepoint = conn.setSavepoint(); C D //3 提交事务 conn.commit(); } catche(){ if(savepoint != null){ //CD异常 // 回滚到CD之前 conn.rollback(savepoint); // 提交AB conn.commit(); } else{ //AB异常 // 回滚AB conn.rollback(); } }
1.1 Spring中事务管理介绍
1.1.1 导入jar包
transaction --> tx
1.1.2 三个顶级接口
- PlatformTransactionManager 平台事务管理器,spring要管理事务,必须使用事务管理器
进行事务配置时,必须配置事务管理器。
- TransactionDefinition:事务详情(事务定义、事务属性),spring用于确定事务具体详情,
例如:隔离级别、是否只读、超时时间 等
进行事务配置时,必须配置详情。spring将配置项封装到该对象实例。
- TransactionStatus:事务状态,spring用于记录当前事务运行状态。例如:是否有保存点,事务是否完成。
spring底层根据状态进行相应操作。
1.1.3 PlatformTransactionManager 事务管理器
- 导入jar包:需要时平台事务管理器的实现类
- 常见的事务管理器
DataSourceTransactionManager ,jdbc开发时事务管理器,采用JdbcTemplate
HibernateTransactionManager,hibernate开发时事务管理器,整合hibernate
- api详解
TransactionStatus getTransaction(TransactionDefinition definition) ,事务管理器 通过“事务详情”,获得“事务状态”,从而管理事务。
void commit(TransactionStatus status) 根据状态提交
void rollback(TransactionStatus status) 根据状态回滚
1.1.4 TransactionStatus
1.1.5 TransactionDefinition
- 传播行为:在两个业务之间如何共享事务。
PROPAGATION_REQUIRED , required , 必须 【默认值】
支持当前事务,A如果有事务,B将使用该事务。
如果A没有事务,B将创建一个新的事务。
PROPAGATION_SUPPORTS ,supports ,支持
支持当前事务,A如果有事务,B将使用该事务。
如果A没有事务,B将以非事务执行。
PROPAGATION_MANDATORY,mandatory ,强制
支持当前事务,A如果有事务,B将使用该事务。
如果A没有事务,B将抛异常。
PROPAGATION_REQUIRES_NEW , requires_new ,必须新的
如果A有事务,将A的事务挂起,B创建一个新的事务
如果A没有事务,B创建一个新的事务
PROPAGATION_NOT_SUPPORTED ,not_supported ,不支持
如果A有事务,将A的事务挂起,B将以非事务执行
如果A没有事务,B将以非事务执行
PROPAGATION_NEVER ,never,从不
如果A有事务,B将抛异常
如果A没有事务,B将以非事务执行
PROPAGATION_NESTED ,nested ,嵌套
A和B底层采用保存点机制,形成嵌套事务。
- 掌握:PROPAGATION_REQUIRED、PROPAGATION_REQUIRES_NEW、PROPAGATION_NESTED
-
转账案例
- 业务逻辑:甲方给乙方转账,甲方和乙方得账户应该同时改变,如果中途断电断网,那么就要考虑事务回滚了。
-
- 创建数据库:
-
/* Navicat MySQL Data Transfer Source Server : gg Source Host : localhost:3306 Source Database : spring_database Target Server Type : MYSQL File Encoding : 65001 Date: 2017-03-10 13:57:02 */ SET FOREIGN_KEY_CHECKS=0; -- ---------------------------- -- Table structure for account -- ---------------------------- DROP TABLE IF EXISTS `account`; CREATE TABLE `account` ( `_id` int(32) NOT NULL AUTO_INCREMENT COMMENT '账户id', `username` varchar(32) NOT NULL COMMENT '账户名', `money` int(32) NOT NULL COMMENT '账户余额', PRIMARY KEY (`_id`) ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8; -- ---------------------------- -- Records of account -- ---------------------------- INSERT INTO `account` VALUES ('1', 'jake', '82000'); INSERT INTO `account` VALUES ('2', 'make', '212000'); INSERT INTO `account` VALUES ('3', 'tom', '879800'); -- ---------------------------- -- Table structure for t_user -- ---------------------------- DROP TABLE IF EXISTS `t_user`; CREATE TABLE `t_user` ( `_id` int(11) NOT NULL AUTO_INCREMENT, `username` varchar(50) DEFAULT NULL, `userpwd` varchar(32) DEFAULT NULL, PRIMARY KEY (`_id`) ) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8; -- ---------------------------- -- Records of t_user -- ---------------------------- INSERT INTO `t_user` VALUES ('1', 'jake', '123456'); INSERT INTO `t_user` VALUES ('2', 'rose', '123456789'); INSERT INTO `t_user` VALUES ('3', 'tom', '999'); INSERT INTO `t_user` VALUES ('4', 'tom', '999'); INSERT INTO `t_user` VALUES ('5', 'tom', '362427');
- 环境搭建
- 引入jar包:jar包下载地址:http://pan.baidu.com/s/1nvJOz6D
- dao接口设计
-
package com.heima.dao; public interface AccountDao { /** * 汇款 * @param outer * @param money */ public void out(String outer, Integer money); /** * 收款 * @param outer * @param money */ public void in(String outer, Integer money); }
- dao具体实现类设计
-
package com.heima.dao.impl; import org.springframework.jdbc.core.support.JdbcDaoSupport; import com.heima.dao.AccountDao; public class AccountDaoImpl extends JdbcDaoSupport implements AccountDao { @Override public void out(String outer, Integer money) { this.getJdbcTemplate().update("update account set money=money-? where username=?",money, outer); } @Override public void in(String outer, Integer money) { this.getJdbcTemplate().update("update account set money=money+? where username=?",money, outer); } }
-
service接口设计
-
package com.heima.service; public interface AccountService { /** * * @param outer 转账 * @param inner 收款人 * @param money 转账金额 */ public void transfer(String outer,String inner,Integer money); }
- service具体实现类设计
-
package com.heima.service.impl; import com.heima.dao.AccountDao; import com.heima.service.AccountService; public class AccountServiceImpl implements AccountService{ private AccountDao accountdao; @Override public void transfer(String outer, String inner, Integer money) { accountdao.out(outer, money); /* //断电 int i = 1/0;*/ accountdao.in(inner, money); } public AccountDao getAccountdao() { return accountdao; } public void setAccountdao(AccountDao accountdao) { this.accountdao = accountdao; } }
- application.xml配置
-
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <!-- 加载jdbcinfo.properties文件 --> <context:property-placeholder location="classpath:jdbcinfo.properties"/> <!-- dataSource 数据源--> <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"> <property name="jdbcUrl" value="${jdbc.jdbcUrl}"></property> <property name="DriverClass" value="${jdbc.driverClass}"></property> <property name="user" value="${jdbc.user}"></property> <property name="password" value="${jdbc.password}"></property> </bean> <!-- dao --> <bean id="accountdao" class="com.heima.dao.impl.AccountDaoImpl"> <property name="dataSource" ref="dataSource"></property> </bean> <!-- service --> <bean id="accountservice" class="com.heima.service.impl.AccountServiceImpl"> <property name="accountdao" ref="accountdao"></property> </bean> </beans>
- jdbcinfo.properties
-
jdbc.driverClass=com.mysql.jdbc.Driver jdbc.jdbcUrl=jdbc:mysql://localhost:3306/spring_database jdbc.user=root jdbc.password=362427gg
- 测试:
-
package com.heima; import org.junit.Test; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; import com.heima.service.AccountService; public class TestApp { @Test public void testdamo() { String xmlpath = "applicationContext.xml"; ApplicationContext applicationContext = new ClassPathXmlApplicationContext(xmlpath); AccountService service = applicationContext.getBean("accountservice",AccountService.class); service.transfer("jake", "make", 1000); } }
运行发现这样子的话一旦遇到了加入断电,断网异常时将会导致数据库不一致,也就是说例如:甲方账户转账成功给乙方后忽然断电而乙方账户没有加。
解决这一问题的方法就是引入事务,让转账这一组操作成为一个事务。
spring提供了申明式的事务支持,当然在这以前我们先使用编程式的方法解决这个问题:
-
使用spring自带的TransactionTemplate事务模板
1.service 需要获得 TransactionTemplate
2.spring 配置模板,并注入给service
3.模板需要注入事务管理器
4.配置事务管理器:DataSourceTransactionManager ,需要注入DataSource
修改serviceimpl为如下:
-
package com.heima.service.impl; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.support.TransactionCallbackWithoutResult; import org.springframework.transaction.support.TransactionTemplate; import com.heima.dao.AccountDao; import com.heima.service.AccountService; public class AccountServiceImpl implements AccountService{ private AccountDao accountdao; private TransactionTemplate transtemplate; @Override public void transfer(String outer, String inner, Integer money) { transtemplate.execute(new TransactionCallbackWithoutResult() { @Override protected void doInTransactionWithoutResult(TransactionStatus arg0) { accountdao.out(outer, money); //断电 int i = 1/0; accountdao.in(inner, money); } }); } public AccountDao getAccountdao() { return accountdao; } public void setAccountdao(AccountDao accountdao) { this.accountdao = accountdao; } public TransactionTemplate getTranstemplate() { return transtemplate; } public void setTranstemplate(TransactionTemplate transtemplate) { this.transtemplate = transtemplate; } }
修改applicationContext.xml配置文件如下
-
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <!-- 加载jdbcinfo.properties文件 --> <context:property-placeholder location="classpath:jdbcinfo.properties"/> <!-- dataSource --> <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"> <property name="jdbcUrl" value="${jdbc.jdbcUrl}"></property> <property name="DriverClass" value="${jdbc.driverClass}"></property> <property name="user" value="${jdbc.user}"></property> <property name="password" value="${jdbc.password}"></property> </bean> <!-- dao --> <bean id="accountdao" class="com.heima.dao.impl.AccountDaoImpl"> <property name="dataSource" ref="dataSource"></property> </bean> <!-- 创建事务管理器,需要注入数据源从而拿到连接池对象获取事务管理器 --> <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"></property> </bean> <!-- 创建模板 --> <bean id="transtemplate" class="org.springframework.transaction.support.TransactionTemplate"> <property name="transactionManager" ref="txManager"></property> </bean> <!-- service --> <bean id="accountservice" class="com.heima.service.impl.AccountServiceImpl"> <property name="accountdao" ref="accountdao"></property> <property name="transtemplate" ref="transtemplate"></property> </bean> </beans>
运行可以发现一旦事务运行期间出错数据库将不会改变。保证了数据的一致性,符合正常的业务逻辑。
2.使用spring的代理类org.springframework.transaction.interceptor.TransactionProxyFactoryBean
service 代理对象
- proxyInterfaces 接口
- target 目标类
- transactionManager 事务管理器
- transactionAttributes 事务属性(事务详情)
prop.key :确定哪些方法使用当前事务配置
prop.text:用于配置事务详情
格式:PROPAGATION,ISOLATION,readOnly,-Exception,+Exception
传播行为 隔离级别 是否只读 异常回滚 异常提交
例如:
<prop key="transfer">PROPAGATION_REQUIRED,ISOLATION_DEFAULT</prop> 默认传播行为,和隔离级别
<prop key="transfer">PROPAGATION_REQUIRED,ISOLATION_DEFAULT,readOnly</prop> 只读
<prop key="transfer">PROPAGATION_REQUIRED,ISOLATION_DEFAULT,+java.lang.ArithmeticException</prop> 有异常扔提交
实现逻辑就是:通过返回一个代理类去执行业务操作:
将serviceimpl恢复到最原始的状态
-
package com.heima.service.impl; import com.heima.dao.AccountDao; import com.heima.service.AccountService; public class AccountServiceImpl implements AccountService{ private AccountDao accountdao; @Override public void transfer(String outer, String inner, Integer money) { accountdao.out(outer, money); accountdao.in(inner, money); } public AccountDao getAccountdao() { return accountdao; } public void setAccountdao(AccountDao accountdao) { this.accountdao = accountdao; } }
- applicationContext.xml配置文件
-
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/context/spring-context.xsd"> <!-- 加载jdbcinfo.properties文件 --> <context:property-placeholder location="classpath:jdbcinfo.properties"/> <!-- dataSource --> <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"> <property name="jdbcUrl" value="${jdbc.jdbcUrl}"></property> <property name="DriverClass" value="${jdbc.driverClass}"></property> <property name="user" value="${jdbc.user}"></property> <property name="password" value="${jdbc.password}"></property> </bean> <!-- dao --> <bean id="accountdao" class="com.heima.dao.impl.AccountDaoImpl"> <property name="dataSource" ref="dataSource"></property> </bean> <!-- service --> <bean id="accountservice" class="com.heima.service.impl.AccountServiceImpl"> <property name="accountdao" ref="accountdao"></property> </bean> <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"></property> </bean> <bean id="proxyaccountservice" class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean"> <!-- 事务管理器 --> <property name="transactionManager" ref="txManager"></property> <!-- 要代理的目标类 --> <property name="target" ref="accountservice"></property> <!-- 配置事务详情 1.传播行为 2.隔离级别 3.是否只读 4.-Exception 遇到异常回滚 5.+Exception 遇到异常要提交事务 --> <property name="transactionAttributes"> <props> <prop key="transfer">PROPAGATION_REQUIRED</prop> </props> </property> </bean> </beans>
3.基于tx/aop实现方式
1.配置事务管理器
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
2.对事物管理器进行增强
<tx:advice transaction-manager="txManager" id="myadvice">
<tx:attributes>
<tx:method name="transfer" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
3.将通知与目标类关联
<aop:config>
<aop:pointcut expression="execution(* com.heima.service.impl.*.*(..))" id="mypointcut"/>
<aop:advisor advice-ref="myadvice" pointcut-ref="mypointcut"/>
</aop:config>
- 修改applicationContext.xml配置文件
-
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd"> <!-- 加载jdbcinfo.properties文件 --> <context:property-placeholder location="classpath:jdbcinfo.properties"/> <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"> <property name="jdbcUrl" value="${jdbc.jdbcUrl}"></property> <property name="DriverClass" value="${jdbc.driverClass}"></property> <property name="user" value="${jdbc.user}"></property> <property name="password" value="${jdbc.password}"></property> </bean> <bean id="accountdao" class="com.heima.dao.impl.AccountDaoImpl"> <property name="dataSource" ref="dataSource"></property> </bean> <bean id="accountservice" class="com.heima.service.impl.AccountServiceImpl"> <property name="accountdao" ref="accountdao"></property> </bean> <!-- 配置事务管理器--> <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"></property> </bean> <!-- 对事务管理器进行增强 --> <tx:advice transaction-manager="txManager" id="myadvice"> <tx:attributes> <tx:method name="transfer" propagation="REQUIRED"/> </tx:attributes> </tx:advice> <!-- 对事件进行切入 --> <aop:config> <aop:pointcut expression="execution(* com.heima.service.impl.*.*(..))" id="mypointcut"/> <aop:advisor advice-ref="myadvice" pointcut-ref="mypointcut"/> </aop:config> </beans>
4.基于注解实现
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
- 在xml中配置事务管理器
<property name="dataSource" ref="dataSource"></property>
</bean>
- 把它交给spring容器中注册驱动便可<tx:annotation-driven transaction-manager="txManager"/>
- 最后在类或方法上加入注解 @Transactional(propagation=Propagation.REQUIRED) 事务详情放在注解里面配置便可
- 修改application.xml
-
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd"> <!-- 加载jdbcinfo.properties文件 --> <context:property-placeholder location="classpath:jdbcinfo.properties"/> <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"> <property name="jdbcUrl" value="${jdbc.jdbcUrl}"></property> <property name="DriverClass" value="${jdbc.driverClass}"></property> <property name="user" value="${jdbc.user}"></property> <property name="password" value="${jdbc.password}"></property> </bean> <bean id="accountdao" class="com.heima.dao.impl.AccountDaoImpl"> <property name="dataSource" ref="dataSource"></property> </bean> <bean id="accountservice" class="com.heima.service.impl.AccountServiceImpl"> <property name="accountdao" ref="accountdao"></property> </bean> <!-- 配置事务管理器--> <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"></property> </bean> <tx:annotation-driven transaction-manager="txManager"/> </beans>
只要在要使用的类或方法上加上注解便可
-
或者:
-