问题描述:一个经典的转账问题

业务需求:数据表如下,现让tom向jerry转账100元,看一下能引发哪些问题

mysql手动提交事物 mysql设置手动提交事务_jdbc

环境搭建:项目框架主要是spring,持久层框架暂时没用mybtis,用的是spring 的JdbcTemplate,连接池c3p0

项目结构:

mysql手动提交事物 mysql设置手动提交事务_mysql手动提交事物_02

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手动提交事物 mysql设置手动提交事务_mysql_03


我们再来查看一下数据库的当前的引擎是否支持业务

mysql手动提交事物 mysql设置手动提交事务_jdbc_04

mysql-5.1版本之前默认引擎是MyISAM,之后是InnoDB,InnoDB是支持业务的。如果不是InnoDB,可以使用alter table 表名 engine=innodb;修改指定表的引擎。

此时的引擎也支持业务,但为什么就是回滚不成功呢?在网上找了好久,终于找到了错误所在。手动提交事务时不能保证事务一致性的! 因为jdbcTemplate.getDataSource().getConnection()获取的connection与每次jdbcTemplate.update用到的connection都是从连接池中获取的,不能保证是一个connection。下面我们来验证一下

对dao层AccountDaoImpl进行以下改造,打印一下当前Connection对象的地址

mysql手动提交事物 mysql设置手动提交事务_spring_05

在AccountServiceImpl中也填加上这一行代码,这里先注释掉之前造的异常,让程序顺利执行完,便于观察结果

mysql手动提交事物 mysql设置手动提交事务_mysql手动提交事物_06

输出结果如下:

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);
    }

把数据表里的数据恢复原样后,再次进行测试,引发异常后,查看数据表里的数据没有发生任何变化,成功的进行了事务的控制