Java交易處理全解析(二)——失敗的案例

來源:互聯網
上載者:User

在本系列的上一篇文章中,我們講到了Java交易處理的基本問題,並且講到了Service層和DAO層,在本篇文章中,我們將以BankService為例學習一個交易處理失敗的案例。

請通過以下方式下載github原始碼:

git clone https://github.com/davenkin/java_transaction_workshop.git

 

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 (\n" +                "BANK_ID INT,\n" +                "BANK_AMOUNT INT,\n" +                "PRIMARY KEY(BANK_ID)\n" +                ");");        statement.execute("CREATE TABLE INSURANCE_ACCOUNT (\n" +                "INSURANCE_ID INT,\n" +                "INSURANCE_AMOUNT INT,\n" +                "PRIMARY KEY(INSURANCE_ID)\n" +                ");");        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對象的方式達到交易處理的目的。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.