zoukankan      html  css  js  c++  java
  • Java事务处理全解析(二)——失败的案例

    在本系列的上一篇文章中,我们讲到了Java事务处理的基本问题,并且讲到了Service层和DAO层,在本篇文章中,我们将以BankService为例学习一个事务处理失败的案例。

     

    BankService的功能为:某个用户有两个账户,分别为银行账户和保险账户,并且有各自的账户号,BankService的transfer方法从该用户的银行账户向保险账户转帐,两个DAO分别用于对两个账户表的存取操作。

    定义一个BankService接口如下:

    package davenkin;
    
    public interface BankService {
        public void transfer(int fromId, int toId, int amount);
    }

    在两个DAO对象中,我们通过传入的同一个DataSource获得Connection,然后通过JDBC提供的API直接对数据库进行操作。

    定义操作银行账户表的DAO类如下:

    复制代码
    package davenkin.step1_failure;
    
    import javax.sql.DataSource;
    import java.sql.Connection;
    import java.sql.PreparedStatement;
    import java.sql.ResultSet;
    import java.sql.SQLException;
    
    public class FailureBankDao {
        private DataSource dataSource;
    
        public FailureBankDao(DataSource dataSource) {
            this.dataSource = dataSource;
        }
    
    
        public void withdraw(int bankId, int amount) throws SQLException {
            Connection connection = dataSource.getConnection();
            PreparedStatement selectStatement = connection.prepareStatement("SELECT BANK_AMOUNT FROM BANK_ACCOUNT WHERE BANK_ID = ?");
            selectStatement.setInt(1, bankId);
            ResultSet resultSet = selectStatement.executeQuery();
            resultSet.next();
            int previousAmount = resultSet.getInt(1);
            resultSet.close();
            selectStatement.close();
    
    
            int newAmount = previousAmount - amount;
            PreparedStatement updateStatement = connection.prepareStatement("UPDATE BANK_ACCOUNT SET BANK_AMOUNT = ? WHERE BANK_ID = ?");
            updateStatement.setInt(1, newAmount);
            updateStatement.setInt(2, bankId);
            updateStatement.execute();
    
            updateStatement.close();
            connection.close();
    
        }
    }
    复制代码

    FailureBankDao的withdraw方法,从银行账户表(BANK_ACCOUNT)中帐号为bankId的用户账户中取出数量为amount的金额。

    采用同样的方法,定义保险账户的DAO类如下:

    复制代码
    package davenkin.step1_failure;
    
    import javax.sql.DataSource;
    import java.sql.Connection;
    import java.sql.PreparedStatement;
    import java.sql.ResultSet;
    import java.sql.SQLException;
    
    public class FailureInsuranceDao {
        private DataSource dataSource;
    
        public FailureInsuranceDao(DataSource dataSource){
            this.dataSource = dataSource;
        }
    
        public void deposit(int insuranceId, int amount) throws SQLException {
            Connection connection = dataSource.getConnection();
            PreparedStatement selectStatement = connection.prepareStatement("SELECT INSURANCE_AMOUNT FROM INSURANCE_ACCOUNT WHERE INSURANCE_ID = ?");
            selectStatement.setInt(1, insuranceId);
            ResultSet resultSet = selectStatement.executeQuery();
            resultSet.next();
            int previousAmount = resultSet.getInt(1);
            resultSet.close();
            selectStatement.close();
    
    
            int newAmount = previousAmount + amount;
            PreparedStatement updateStatement = connection.prepareStatement("UPDATE INSURANCE_ACCOUNT SET INSURANCE_AMOUNT = ? WHERE INSURANCE_ID = ?");
            updateStatement.setInt(1, newAmount);
            updateStatement.setInt(2, insuranceId);
            updateStatement.execute();
    
            updateStatement.close();
            connection.close();
        }
    }
    复制代码

    FailureInsuranceDao类的deposit方法向保险账户表(INSURANCE_ACCOUNT)存入amount数量的金额,这样在BankService中,我们可以先调用FailureBankDao的withdraw方法取出一定金额的存款,再调用FailureInsuranceDao的deposit方法将该笔存款存入保险账户表中,一切看似OK,实现BankService接口如下:

    复制代码
    package davenkin.step1_failure;
    
    import davenkin.BankService;
    
    import javax.sql.DataSource;
    import java.sql.Connection;
    import java.sql.SQLException;
    
    public class FailureBankService implements BankService{
        private FailureBankDao failureBankDao;
        private FailureInsuranceDao failureInsuranceDao;
        private DataSource dataSource;
    
        public FailureBankService(DataSource dataSource) {
            this.dataSource = dataSource;
        }
    
        public void transfer(int fromId, int toId, int amount) {
            Connection connection = null;
            try {
                connection = dataSource.getConnection();
                connection.setAutoCommit(false);
    
                failureBankDao.withdraw(fromId, amount);
                failureInsuranceDao.deposit(toId, amount);
    
                connection.commit();
            } catch (Exception e) {
                try {
                    assert connection != null;
                    connection.rollback();
                } catch (SQLException e1) {
                    e1.printStackTrace();
                }
            } finally {
                try
                {
                    assert connection != null;
                    connection.close();
                } catch (SQLException e)
                {
                    e.printStackTrace();
                }
            }
        }
    
        public void setFailureBankDao(FailureBankDao failureBankDao) {
            this.failureBankDao = failureBankDao;
        }
    
        public void setFailureInsuranceDao(FailureInsuranceDao failureInsuranceDao) {
            this.failureInsuranceDao = failureInsuranceDao;
        }
    }
    复制代码

    在FailureBankService的transfer方法中,我们首先通过DataSource获得Connection,然后调用connection.setAutoCommit(false)已开启手动提交模式,如果一切顺利,则commit,如果出现异常,则rollback。 接下来,开始测试我们的BankService吧。

    为了准备测试数据,我们定义个BankFixture类,该类负责在每次测试之前准备测试数据,分别向银行账户(1111)和保险账户(2222)中均存入1000元。BankFixture还提供了两个helper方法(getBankAmount和getInsuranceAmount)帮助我们从数据库中取出数据以做数据验证。我们使用HSQL数据库的in-memory模式,这样不用启动数据库server,方便测试。BankFixture类定义如下:

    复制代码
    package davenkin;
    
    import org.junit.Before;
    import javax.sql.DataSource;
    import java.sql.*;
    
    public class BankFixture
    {
    
        protected final DataSource dataSource = DataSourceFactory.createDataSource();
    
        @Before
        public void setUp() throws SQLException
        {
            Connection connection = dataSource.getConnection();
            Statement statement = connection.createStatement();
    
            statement.execute("DROP TABLE BANK_ACCOUNT IF EXISTS");
            statement.execute("DROP TABLE INSURANCE_ACCOUNT IF EXISTS");
            statement.execute("CREATE TABLE BANK_ACCOUNT (
    " +
                    "BANK_ID INT,
    " +
                    "BANK_AMOUNT INT,
    " +
                    "PRIMARY KEY(BANK_ID)
    " +
                    ");");
    
            statement.execute("CREATE TABLE INSURANCE_ACCOUNT (
    " +
                    "INSURANCE_ID INT,
    " +
                    "INSURANCE_AMOUNT INT,
    " +
                    "PRIMARY KEY(INSURANCE_ID)
    " +
                    ");");
    
            statement.execute("INSERT INTO BANK_ACCOUNT VALUES (1111, 1000);");
            statement.execute("INSERT INTO INSURANCE_ACCOUNT VALUES (2222, 1000);");
    
            statement.close();
            connection.close();
        }
    
        protected int getBankAmount(int bankId) throws SQLException
        {
            Connection connection = dataSource.getConnection();
            PreparedStatement selectStatement = connection.prepareStatement("SELECT BANK_AMOUNT FROM BANK_ACCOUNT WHERE BANK_ID = ?");
            selectStatement.setInt(1, bankId);
            ResultSet resultSet = selectStatement.executeQuery();
            resultSet.next();
            int amount = resultSet.getInt(1);
            resultSet.close();
            selectStatement.close();
            connection.close();
            return amount;
        }
    
        protected int getInsuranceAmount(int insuranceId) throws SQLException
        {
            Connection connection = dataSource.getConnection();
            PreparedStatement selectStatement = connection.prepareStatement("SELECT INSURANCE_AMOUNT FROM INSURANCE_ACCOUNT WHERE INSURANCE_ID = ?");
            selectStatement.setInt(1, insuranceId);
            ResultSet resultSet = selectStatement.executeQuery();
            resultSet.next();
            int amount = resultSet.getInt(1);
            resultSet.close();
            selectStatement.close();
            connection.close();
            return amount;
        }
    
    }
    复制代码

    编写的Junit测试继承自BankFixture类,测试代码如下:

    复制代码
    package davenkin.step1_failure;
    
    import davenkin.BankFixture;
    import org.junit.Test;
    import java.sql.SQLException;
    import static junit.framework.Assert.assertEquals;
    
    public class FailureBankServiceTest extends BankFixture
    {
        @Test
        public void transferSuccess() throws SQLException
        {
            FailureBankDao failureBankDao = new FailureBankDao(dataSource);
            FailureInsuranceDao failureInsuranceDao = new FailureInsuranceDao(dataSource);
    
            FailureBankService bankService = new FailureBankService(dataSource);
            bankService.setFailureBankDao(failureBankDao);
            bankService.setFailureInsuranceDao(failureInsuranceDao);
    
            bankService.transfer(1111, 2222, 200);
    
            assertEquals(800, getBankAmount(1111));
            assertEquals(1200, getInsuranceAmount(2222));
    
        }
    
        @Test
        public void transferFailure() throws SQLException
        {
            FailureBankDao failureBankDao = new FailureBankDao(dataSource);
            FailureInsuranceDao failureInsuranceDao = new FailureInsuranceDao(dataSource);
    
            FailureBankService bankService = new FailureBankService(dataSource);
            bankService.setFailureBankDao(failureBankDao);
            bankService.setFailureInsuranceDao(failureInsuranceDao);
    
            int toNonExistId = 3333;
            bankService.transfer(1111, toNonExistId, 200);
    
            assertEquals(1000, getInsuranceAmount(2222));
            assertEquals(1000, getBankAmount(1111));
        }
    }
    复制代码

    运行测试,第一个测试(transferSuccess)成功,第二个测试(transferFailure)失败。

    分析错误,原因在于:我们分别从FailureBankService,FailureBankDao和FailureInsuranceDao中调用了三次dataSource.getConnection(),亦即我们创建了三个不同的Connection对象,而Java事务是作用于Connection之上的,所以从在三个地方我们开启了三个不同的事务,而不是同一个事务。

    第一个测试之所以成功,是因为在此过程中没有任何异常发生。虽然在FailureBankService中将Connection的提交模式改为了手动提交,但是由于两个DAO使用的是各自的Connection对象,所以两个DAO中的Connection依然为默认的自动提交模式。

    在第二个测试中,我们给出一个不存在的保险账户id(toNonExistId),就是为了使程序产生异常,然后在assertion语句中验证两张表均没有任何变化,但是测试在第二个assertion语句处出错。发生异常时,银行账户中的金额已经减少,而虽然程序发生了rollback,但是调用的是FailureBankService中Connection的rollback,而不是FailureInsuranceDao中Connection的,对保险账户的操作根本就没有执行,所以保险账户中依然为1000,而银行账户却变为了800。

    因此,为了使两个DAO在同一个事务中,我们应该在整个事务处理过程中使用一个Connection对象,在下一篇文章中,我们将讲到通过共享Connection对象的方式达到事务处理的目的。

  • 相关阅读:
    第三十五篇 os模块、sys模块、json模块、pickle模块
    第三十三篇 包
    <词云图>疾风剑豪-亚索词云图
    <爬虫>常见网址的爬虫整理
    <爬虫>反反爬虫的各种知识
    <爬虫>崔庆才的爬虫课
    <随便写>番茄工作法笔记
    <就业指导>为了找到更好的工作
    <人事面试>人事面试整理
    <面试题>面试题整理(101-200)
  • 原文地址:https://www.cnblogs.com/fengjian/p/4209651.html
Copyright © 2011-2022 走看看