在本系列的上一篇文章中,我们讲到了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 (\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对象的方式达到事务处理的目的。

Java事务处理全解析(二)——失败的案例的更多相关文章

  1. Java事务处理全解析(三)——丑陋的案例

    在本系列的上一篇文章中,我们看到了一个典型的事务处理失败的案例,其主要原因在于,service层和各个DAO所使用的Connection是不一样的,而JDBC中事务处理的作用对象正是Connectio ...

  2. Java事务处理全解析(一)——Java事务处理的基本问题

    Java中的事务处理有多简单?在使用EJB时,事务在我们几乎察觉不到的情况下发挥着作用:而在使用Spring时,也只需要配置一个TransactionManager,然后在需要事务的方法上加上Tran ...

  3. Java事务处理全解析(七)—— 像Spring一样使用Transactional注解(Annotation)

    在本系列的上一篇文章中,我们讲到了使用动态代理的方式完成事务处理,这种方式将service层的所有public方法都加入到事务中,这显然不是我们需要的,需要代理的只是那些需要操作数据库的方法.在本篇中 ...

  4. Java事务处理全解析(六)—— 使用动态代理(Dynamic Proxy)完成事务

    在本系列的上一篇文章中,我们讲到了使用Template模式进行事务管理,这固然是一种很好的方法,但是不那么完美的地方在于我们依然需要在service层中编写和事务处理相关的代码,即我们需要在servi ...

  5. Java事务处理全解析(四)—— 成功的案例(自己实现一个线程安全的TransactionManager)

    在本系列的上一篇文章中我们讲到,要实现在同一个事务中使用相同的Connection对象,我们可以通过传递Connection对象的方式达到共享的目的,但是这种做法是丑陋的.在本篇文章中,我们将引入另外 ...

  6. Java事务处理全解析(八)——分布式事务入门例子(Spring+JTA+Atomikos+Hibernate+JMS)

    在本系列先前的文章中,我们主要讲解了JDBC对本地事务的处理,本篇文章将讲到一个分布式事务的例子. 请通过以下方式下载github源代码: git clone https://github.com/d ...

  7. Java事务处理全解析(五)—— Template模式

    在本系列的上一篇文章中,我们讲到了使用TransactionManger和ConnectionHolder完成线程安全的事务管理,在本篇中,我们将在此基础上引入Template模式进行事务管理. Te ...

  8. java事务处理全解析

    http://blog.csdn.net/huilangeliuxin/article/details/43446177

  9. 《Java面试全解析》505道面试题详解

    <Java面试全解析>是我在 GitChat 发布的一门电子书,全书总共有 15 万字和 505 道 Java 面试题解析,目前来说应该是最实用和最全的 Java 面试题解析了. 我本人是 ...

随机推荐

  1. socket 简介

    对TCP/IP.UDP.Socket编程这些词你不会很陌生吧?随着网络技术的发展,这些词充斥着我们的耳朵.那么我想问: 1.         什么是TCP/IP.UDP?2.         Sock ...

  2. SQL注入测试平台 SQLol -5.DELETE注入测试

    访问首页的delete模块,http://127.0.0.1/sql/delete.php,开始对delete模块进行测试. delete语法: DELETE FROM [users] WHERE [ ...

  3. C#部分---"++"在前在后的区别。

    int a=5; int b=a++;//int b=a;a=a+1 int c=++a;//a=a+1;int c=a

  4. linux ssh免密码登录设置

    ssh-keygen -t rsa 全部回车 生成密钥默认放在了/root/.shh中 打开它 cd /root/.ssh cp id_rsa.pub authorized_keys 免密码登录 现在 ...

  5. jQuery - 设置内容和属性

    设置内容 - text().html() 以及 val() 我们将使用前一章中的三个相同的方法来设置内容: text() - 设置或返回所选元素的文本内容 html() - 设置或返回所选元素的内容( ...

  6. JSBinding+SharpKit / MonoBehaviour替换成JSComponent原理

    Unity 是基于组件式的开发,gameObject 身上可以绑定任意个脚本.每个脚本组成 gameObject 的一个部分. 脚本里通过添加预定义好的函数来执行自己的任务.比如Awake,用于初始化 ...

  7. glibc下的内存管理

    在解码过程中我们也遇到了类似的问题,第一次解码的音频比较大60s,耗了3G的内存,reset之后内存并没有退还给操作系统,第二次即使解一个10s的音频 几周前我曾提到,我被项目组分配去做了一些探究li ...

  8. Why we made vorlon.js and how to use it to debug your JavaScript remotely

    Vorlon.js is powered by node.JS, socket.io, and late-night coffee. I would like to share with you wh ...

  9. Mysql函数FIND_IN_SET()的使用方法

    有了FIND_IN_SET这个函数.我们可以设计一个如:一只手机即是智能机,又是Andriod系统的. 比如:有个产品表里有一个type字段,他存储的是产品(手机)类型,有 1.智能机,2.Andri ...

  10. 【转】PHP简单拦截器的实现

    最近在看Yii的源代码,收获了不少,这里就是从中得到的启发,而写的一个简单拦截器的实现下面看例子: <?phpclass A{    private $_e = array();       p ...