问题描述:一个经典的转账问题
业务需求:数据表如下,现让tom向jerry转账100元,看一下能引发哪些问题
环境搭建:项目框架主要是spring,持久层框架暂时没用mybtis,用的是spring 的JdbcTemplate,连接池c3p0
项目结构:
applicationContext.xml文件主要配置:
<!--加载外部的properties配置文件-->
<context:property-placeholder location="classpath:jdbc.properties"></context:property-placeholder>
<!--配置元数据-->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="${jdbc.driver}"></property>
<property name="jdbcUrl" value="${jdbc.url}"></property>
<property name="user" value="${jdbc.username}"></property>
<property name="password" value="${jdbc.password}"></property>
</bean>
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<!--注入依赖-->
<property name="dataSource" ref="dataSource"></property>
</bean>
<bean id="accountDao" class="com.szly.dao.Impl.AccountDaoImpl">
<property name="jdbcTemplate" ref="jdbcTemplate"></property>
</bean>
<bean id="accountService" class="com.szly.service.Impl.AccountServiceImpl">
<property name="accountDao" ref="accountDao"></property>
<property name="jdbcTemplate" ref="jdbcTemplate"></property>
</bean>
dao层实现
import com.szly.dao.AccountDao;
import org.springframework.jdbc.core.JdbcTemplate;
import java.sql.SQLException;
public class AccountDaoImpl implements AccountDao {
private JdbcTemplate jdbcTemplate;
public AccountDaoImpl() {
}
public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
@Override
public void out(String outMan, int money) {
jdbcTemplate.update("update account set balance=balance-? where username = ?", money, outMan);
}
@Override
public void in(String inMan, int money) {
jdbcTemplate.update("update account set balance=balance+? where username = ?", money, inMan);
}
}
service层实现
import com.szly.dao.AccountDao;
import com.szly.service.AccountService;
import org.springframework.jdbc.core.JdbcTemplate;
import java.sql.Connection;
import java.sql.SQLException;
public class AccountServiceImpl implements AccountService {
private AccountDao accountDao;
private JdbcTemplate jdbcTemplate;
public AccountServiceImpl() {
}
public void setAccountDao(AccountDao accountDao) {
this.accountDao = accountDao;
}
public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
@Override
public void transfer(String outMan, String inMan, int money) {
Connection connection = null;
try {
connection = this.jdbcTemplate.getDataSource().getConnection();
// 取消事务的自动提交
connection.setAutoCommit(false);
accountDao.out(outMan, money);
// 制造个异常
int i = 1 / 0;
accountDao.in(inMan, money);
// 提交事务
connection.commit();
} catch (Exception e) {
e.printStackTrace();
try {
// 回滚事务
connection.rollback();
System.out.println("转账失败!");
} catch (Exception e1) {
e1.printStackTrace();
}
}
}
}
controller层实现
import com.szly.service.AccountService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class AccountController {
public static void main(String[] args) {
ApplicationContext app = new ClassPathXmlApplicationContext("applicationContext.xml");
AccountService accountService = app.getBean(AccountService.class);
accountService.transfer("tom", "jerry", 100);
}
}
我们期望在调用transfer业务进行转账时,如果转账过程中遇到了异常,就进行事务的回滚。
所以取消了事务的自动提交,手动造了一个 / by zero的异常,当捕捉到异常时,将不会提交事务,而是进行事务的回滚。因为此时转账失败,所以我们期望的结果是tom和jerry都还有1000的余额。然而,结果和我们的预想并不一样。what ?100元不翼而飞,事务回滚竟然失败了?
我们再来查看一下数据库的当前的引擎是否支持业务
mysql-5.1版本之前默认引擎是MyISAM,之后是InnoDB,InnoDB是支持业务的。如果不是InnoDB,可以使用alter table 表名 engine=innodb;修改指定表的引擎。
此时的引擎也支持业务,但为什么就是回滚不成功呢?在网上找了好久,终于找到了错误所在。手动提交事务时不能保证事务一致性的! 因为jdbcTemplate.getDataSource().getConnection()获取的connection与每次jdbcTemplate.update用到的connection都是从连接池中获取的,不能保证是一个connection。下面我们来验证一下
对dao层AccountDaoImpl进行以下改造,打印一下当前Connection对象的地址
在AccountServiceImpl中也填加上这一行代码,这里先注释掉之前造的异常,让程序顺利执行完,便于观察结果
输出结果如下:
com.mchange.v2.c3p0.impl.NewProxyConnection@38364841
com.mchange.v2.c3p0.impl.NewProxyConnection@67e2d983
com.mchange.v2.c3p0.impl.NewProxyConnection@568bf312
发现三个地址都不相同,那也就说明这三个连接不是同一个连接,因此也就不能正常地手动控制事务了。那么该怎么解决呢?
我们可以使用Spring基于xml的声明式事务控制,下面对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/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">
<!--加载外部的properties配置文件-->
<context:property-placeholder location="classpath:jdbc.properties"></context:property-placeholder>
<!--配置元数据-->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="${jdbc.driver}"></property>
<property name="jdbcUrl" value="${jdbc.url}"></property>
<property name="user" value="${jdbc.username}"></property>
<property name="password" value="${jdbc.password}"></property>
</bean>
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<!--注入依赖-->
<property name="dataSource" ref="dataSource"></property>
</bean>
<bean id="accountDao" class="com.szly.dao.Impl.AccountDaoImpl">
<property name="jdbcTemplate" ref="jdbcTemplate"></property>
</bean>
<!--目标对象,内部的转账方法就是切点-->
<bean id="accountService" class="com.szly.service.Impl.AccountServiceImpl">
<property name="accountDao" ref="accountDao"></property>
<!--<property name="jdbcTemplate" ref="jdbcTemplate"></property>-->
</bean>
<!--配置平台事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!--通知 即事务的增强-->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<!--设置事务的属性信息-->
<tx:attributes>
<!--切点方法的事务参数配置-->
<tx:method name="transfer" isolation="REPEATABLE_READ" propagation="REQUIRED" read-only="false"></tx:method>
</tx:attributes>
</tx:advice>
<!--配置事务的aop织入-->
<aop:config>
<aop:advisor advice-ref="txAdvice" pointcut="execution(* com.szly.service.Impl.*.*(..))"></aop:advisor>
</aop:config>
</beans>
可以将transfer方法中手动提交事务的代码全部删掉,只留下以下代码即可。这样做也就将事务管理的业务从service层中抽取了出来,交给spring来处理,从而也就降低了耦合性,便于后期维护,提高开发效率。
@Override
public void transfer(String outMan, String inMan, int money) {
accountDao.out(outMan, money);
int i = 1/0;
accountDao.in(inMan, money);
}
把数据表里的数据恢复原样后,再次进行测试,引发异常后,查看数据表里的数据没有发生任何变化,成功的进行了事务的控制