Java事务处理全解析(二)—— 失败的案例

来源:互联网 发布:sql identity 编辑:程序博客网 时间:2024/05/17 22:19

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

 

请通过以下方式下载github源代码:

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

 

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

 

定义一个BankService接口如下:

[java] view plain copy
print?在CODE上查看代码片派生到我的代码片
  1. package davenkin;  
  2.                                                
  3. public interface BankService {  
  4.     public void transfer(int fromId, int toId, int amount);  
  5. }  
package davenkin;public interface BankService {    public void transfer(int fromId, int toId, int amount);}

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

 

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

[java] view plain copy
print?在CODE上查看代码片派生到我的代码片
  1. public class FailureBankDao {  
  2.     private DataSource dataSource;  
  3.                                              
  4.     public FailureBankDao(DataSource dataSource) {  
  5.         this.dataSource = dataSource;  
  6.     }  
  7.                                              
  8.                                              
  9.     public void withdraw(int bankId, int amount) throws SQLException {  
  10.         Connection connection = dataSource.getConnection();  
  11.         PreparedStatement selectStatement = connection.prepareStatement(”SELECT BANK_AMOUNT FROM BANK_ACCOUNT WHERE BANK_ID = ?”);  
  12.         selectStatement.setInt(1, bankId);  
  13.         ResultSet resultSet = selectStatement.executeQuery();  
  14.         resultSet.next();  
  15.         int previousAmount = resultSet.getInt(1);  
  16.         resultSet.close();  
  17.         selectStatement.close();  
  18.                                              
  19.                                              
  20.         int newAmount = previousAmount - amount;  
  21.         PreparedStatement updateStatement = connection.prepareStatement(”UPDATE BANK_ACCOUNT SET BANK_AMOUNT = ? WHERE BANK_ID = ?”);  
  22.         updateStatement.setInt(1, newAmount);  
  23.         updateStatement.setInt(2, bankId);  
  24.         updateStatement.execute();  
  25.                                              
  26.         updateStatement.close();  
  27.         connection.close();  
  28.                                              
  29.     }  
  30. }  
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类如下:

[java] view plain copy
print?在CODE上查看代码片派生到我的代码片
  1. public class FailureInsuranceDao {  
  2.     private DataSource dataSource;  
  3.                                          
  4.     public FailureInsuranceDao(DataSource dataSource){  
  5.         this.dataSource = dataSource;  
  6.     }  
  7.                                          
  8.     public void deposit(int insuranceId, int amount) throws SQLException {  
  9.         Connection connection = dataSource.getConnection();  
  10.         PreparedStatement selectStatement = connection.prepareStatement(”SELECT INSURANCE_AMOUNT FROM INSURANCE_ACCOUNT WHERE INSURANCE_ID = ?”);  
  11.         selectStatement.setInt(1, insuranceId);  
  12.         ResultSet resultSet = selectStatement.executeQuery();  
  13.         resultSet.next();  
  14.         int previousAmount = resultSet.getInt(1);  
  15.         resultSet.close();  
  16.         selectStatement.close();  
  17.                                          
  18.                                          
  19.         int newAmount = previousAmount + amount;  
  20.         PreparedStatement updateStatement = connection.prepareStatement(”UPDATE INSURANCE_ACCOUNT SET INSURANCE_AMOUNT = ? WHERE INSURANCE_ID = ?”);  
  21.         updateStatement.setInt(1, newAmount);  
  22.         updateStatement.setInt(2, insuranceId);  
  23.         updateStatement.execute();  
  24.                                          
  25.         updateStatement.close();  
  26.         connection.close();  
  27.     }  
  28. }  
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接口如下:
[java] view plain copy
print?在CODE上查看代码片派生到我的代码片
  1. public class FailureBankService implements BankService{  
  2.     private FailureBankDao failureBankDao;  
  3.     private FailureInsuranceDao failureInsuranceDao;  
  4.     private DataSource dataSource;  
  5.                                       
  6.     public FailureBankService(DataSource dataSource) {  
  7.         this.dataSource = dataSource;  
  8.     }  
  9.                                       
  10.     public void transfer(int fromId, int toId, int amount) {  
  11.         Connection connection = null;  
  12.         try {  
  13.             connection = dataSource.getConnection();  
  14.             connection.setAutoCommit(false);  
  15.                                       
  16.             failureBankDao.withdraw(fromId, amount);  
  17.             failureInsuranceDao.deposit(toId, amount);  
  18.                                       
  19.             connection.commit();  
  20.         } catch (Exception e) {  
  21.             try {  
  22.                 assert connection != null;  
  23.                 connection.rollback();  
  24.             } catch (SQLException e1) {  
  25.                 e1.printStackTrace();  
  26.             }  
  27.         } finally {  
  28.             try  
  29.             {  
  30.                 assert connection != null;  
  31.                 connection.close();  
  32.             } catch (SQLException e)  
  33.             {  
  34.                 e.printStackTrace();  
  35.             }  
  36.         }  
  37.     }  
  38.                                       
  39.     public void setFailureBankDao(FailureBankDao failureBankDao) {  
  40.         this.failureBankDao = failureBankDao;  
  41.     }  
  42.                                       
  43.     public void setFailureInsuranceDao(FailureInsuranceDao failureInsuranceDao) {  
  44.         this.failureInsuranceDao = failureInsuranceDao;  
  45.     }  
  46. }  
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类定义如下:


[java] view plain copy
print?在CODE上查看代码片派生到我的代码片
  1. public class BankFixture  
  2. {  
  3.                                     
  4.     protected final DataSource dataSource = DataSourceFactory.createDataSource();  
  5.                                     
  6.     @Before  
  7.     public void setUp() throws SQLException  
  8.     {  
  9.         Connection connection = dataSource.getConnection();  
  10.         Statement statement = connection.createStatement();  
  11.                                     
  12.         statement.execute(”DROP TABLE BANK_ACCOUNT IF EXISTS”);  
  13.         statement.execute(”DROP TABLE INSURANCE_ACCOUNT IF EXISTS”);  
  14.         statement.execute(”CREATE TABLE BANK_ACCOUNT (\n” +  
  15.                 ”BANK_ID INT,\n” +  
  16.                 ”BANK_AMOUNT INT,\n” +  
  17.                 ”PRIMARY KEY(BANK_ID)\n” +  
  18.                 ”);”);  
  19.                                     
  20.         statement.execute(”CREATE TABLE INSURANCE_ACCOUNT (\n” +  
  21.                 ”INSURANCE_ID INT,\n” +  
  22.                 ”INSURANCE_AMOUNT INT,\n” +  
  23.                 ”PRIMARY KEY(INSURANCE_ID)\n” +  
  24.                 ”);”);  
  25.                                     
  26.         statement.execute(”INSERT INTO BANK_ACCOUNT VALUES (1111, 1000);”);  
  27.         statement.execute(”INSERT INTO INSURANCE_ACCOUNT VALUES (2222, 1000);”);  
  28.                                     
  29.         statement.close();  
  30.         connection.close();  
  31.     }  
  32.                                     
  33.     protected int getBankAmount(int bankId) throws SQLException  
  34.     {  
  35.         Connection connection = dataSource.getConnection();  
  36.         PreparedStatement selectStatement = connection.prepareStatement(”SELECT BANK_AMOUNT FROM BANK_ACCOUNT WHERE BANK_ID = ?”);  
  37.         selectStatement.setInt(1, bankId);  
  38.         ResultSet resultSet = selectStatement.executeQuery();  
  39.         resultSet.next();  
  40.         int amount = resultSet.getInt(1);  
  41.         resultSet.close();  
  42.         selectStatement.close();  
  43.         connection.close();  
  44.         return amount;  
  45.     }  
  46.                                     
  47.     protected int getInsuranceAmount(int insuranceId) throws SQLException  
  48.     {  
  49.         Connection connection = dataSource.getConnection();  
  50.         PreparedStatement selectStatement = connection.prepareStatement(”SELECT INSURANCE_AMOUNT FROM INSURANCE_ACCOUNT WHERE INSURANCE_ID = ?”);  
  51.         selectStatement.setInt(1, insuranceId);  
  52.         ResultSet resultSet = selectStatement.executeQuery();  
  53.         resultSet.next();  
  54.         int amount = resultSet.getInt(1);  
  55.         resultSet.close();  
  56.         selectStatement.close();  
  57.         connection.close();  
  58.         return amount;  
  59.     }  
  60.                                     
  61. }  
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类,测试代码如下:

[java] view plain copy
print?在CODE上查看代码片派生到我的代码片
  1. public class FailureBankServiceTest extends BankFixture  
  2. {  
  3.     @Test  
  4.     public void transferSuccess() throws SQLException  
  5.     {  
  6.         FailureBankDao failureBankDao = new FailureBankDao(dataSource);  
  7.         FailureInsuranceDao failureInsuranceDao = new FailureInsuranceDao(dataSource);  
  8.                                   
  9.         FailureBankService bankService = new FailureBankService(dataSource);  
  10.         bankService.setFailureBankDao(failureBankDao);  
  11.         bankService.setFailureInsuranceDao(failureInsuranceDao);  
  12.                                   
  13.         bankService.transfer(11112222200);  
  14.                                   
  15.         assertEquals(800, getBankAmount(1111));  
  16.         assertEquals(1200, getInsuranceAmount(2222));  
  17.                                   
  18.     }  
  19.                                   
  20.     @Test  
  21.     public void transferFailure() throws SQLException  
  22.     {  
  23.         FailureBankDao failureBankDao = new FailureBankDao(dataSource);  
  24.         FailureInsuranceDao failureInsuranceDao = new FailureInsuranceDao(dataSource);  
  25.                                   
  26.         FailureBankService bankService = new FailureBankService(dataSource);  
  27.         bankService.setFailureBankDao(failureBankDao);  
  28.         bankService.setFailureInsuranceDao(failureInsuranceDao);  
  29.                                   
  30.         int toNonExistId = 3333;  
  31.         bankService.transfer(1111, toNonExistId, 200);  
  32.                                   
  33.         assertEquals(1000, getInsuranceAmount(2222));  
  34.         assertEquals(1000, getBankAmount(1111));  
  35.     }  
  36. }  
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对象的方式达到事务处理的目的。


转载地址:http://www.davenkin.me/post/2013-02-17/40049132188


0 0