疑惑:有没有人真正用多线程工具(比如groboutils)测试过Spring的事务处理?

之前的公司里,曾经在几个项目里用过 Spring + Hibernate 架构。

其中,使用了标准的 Spring 声明式事务管理(相关的文章、示例在网上随处可见)。因为当时的项目对并发访问的要求并不高,加上赶进度,所以从来没有在真正高并发的情形下,测试过系统数据库事务管理是否正确。

(唯一的确认行为,就是打开数据库本身的记录,看里面是否有事务管理的SQL代码出现)

[b]当然了,我自己也承认这样的做法可能隐含严重的问题,所以一直在想好好做一下测试。[/b]

最近比较闲一点,就自己编了个测试用例,在 Spring + Hibernate + MySQL 环境里,使用跟 junit 集成的多线程工具 groboutils 跑了一下。


[b]做法:[/b]
同时并发比较大数量(比如说,3000个)的测试线程;在每个线程中,从一系列(比如说,10个)共享的“银行户头”里随机挑选两个,进行转帐;


[b]期望:[/b]
在没有配置声明式事务管理(事务方式,隔离方式等)时,转帐前、后,所有“银行户头”的总额出现误差。

而在配置声明式事务管理后,转帐前、后的总额保持一致。


[b]结果:[/b]
当使用 MySQL InnoDB 类型表格时,出现死锁异常:(JDBCExceptionReporter.java:101) - Deadlock found when trying to get lock; try restarting transaction

当使用 MySQL MyISAM 类型表格时(死马当活马,试试看),死锁异常没了,但是无论怎么配置事务管理,都不管用,assertEquals 失败,转帐前、后的总额不一致。


[b]补充:[/b]
当使用 MySQL InnoDB 类型表格时,死锁异常一般出现在测试线程数量较多的时候。当减小测试线程数量(减到100个)、增加共享的“银行户头”数量(加到50个)时,死锁异常不再出现。但是!![b]事务管理照样不管用![/b]转帐前、后,所有“银行户头”的总额不一致,有时候变多,有时候变少......

希望做过类似测试的进来讨论讨论。

重要部分的源代码如下:

JUnit testcase: AccountTransferMultiThreadTest.java(此测试用例最新最完整的代码在[url=http://www.iteye.com/topic/436718?page=4#1116489]这一个跟贴[/url]的末尾)


// import 省略...

/**
* 测试类 AccountTransferMultiThreadTest,使用了 groboutils 以实现多线程测试。
*
* 每个测试线程从一定数量的测试户头中随机选取一对 转出/转入 户头,然后进行一次随机数额的转帐。
*
* 测试户头总数由常量 NUM_ACC 设定。
*
* 测试线程总数由常量 NUM_TRANSFER 设定。
*
*/
public class AccountTransferMultiThreadTest extends TestCase {
// 每个测试户头的初始余额为1000元
private static final BigDecimal INIT_BALANCE = BigDecimal.valueOf(100000L, 2);
private static final int NUM_ACC = 10; // 测试户头的总数
private static final int NUM_TRANSFER = 3000; // 测试线程总数(即转帐总次数)
private ApplicationContext context;
private AccountService accountService;
private long[] accountIds;

/* (non-Javadoc)
* @see junit.framework.TestCase#setUp()
*
* 在setUp方法中,生成测试所需的Spring Application Context, 并在数据库中创建
* 一定数量的户头(Account),供多线程测试使用。
*
*/
protected void setUp() throws Exception {
super.setUp();
context = new ClassPathXmlApplicationContext("xiao/test/spring/*Context.xml");
accountService = (AccountService) context.getBean("accountService");

Account[] accounts = new Account[NUM_ACC];
accountIds = new long[accounts.length];
for (int i = 0; i < accounts.length; i++) {
accounts[i] = new Account();
accounts[i].setBalance(INIT_BALANCE);

// 将当前生成的户头写入数据库
accountService.create(accounts[i]);

// 重要步骤!将当前生成的户头主键记录下来,以供测试线程使用
accountIds[i] = (Long)accounts[i].getId();
}
}

/* (non-Javadoc)
* @see junit.framework.TestCase#tearDown()
*/
protected void tearDown() throws Exception {
super.tearDown();
}

private Account[] getAccounts() {
Account[] accounts = new Account[accountIds.length];
for (int i = 0; i < accountIds.length; i++) {
// 从数据库获取这个户头对象
accounts[i] = accountService.findById(accountIds[i]);
}
// 返回户头数组
return accounts;
}

public void testMultiThreadTransfer() throws Throwable {
// 获取户头对象数组
Account[] accounts = getAccounts();
System.out.printf("Starting %s transfers...\n", NUM_TRANSFER);

// 记录测试前的所有户头总余额
BigDecimal total1 = accountService.getTotalBalance(accounts);

// 生成所有测试线程
TestRunnable[] tr = new TestRunnable[NUM_TRANSFER];
long start = System.currentTimeMillis();
for (int i = 0; i < tr.length; i++) {
tr[i] = new TransferThread(accountService, accounts);
}

// 生成测试线程运行器
MultiThreadedTestRunner mttr = new MultiThreadedTestRunner(tr);

// 运行测试线程
mttr.runTestRunnables();

// 显示转帐消耗时间
long used = System.currentTimeMillis() - start;
System.out.printf("%s transfers used %s milli-seconds.\n", NUM_TRANSFER, used);

// 获取测试后所有户头总余额
Account[] accounts2 = getAccounts();
BigDecimal total2 = accountService.getTotalBalance(accounts2);

// 确认测试前后,所有户头总余额还是一致的。
assertEquals(total1, total2);
}

/*
* 测试线程类定义
*/
private static class TransferThread extends TestRunnable {
private AccountService accountService;
private Account[] accounts;
public TransferThread(AccountService accountService, Account[] accounts) {
super();
this.accountService = accountService;
this.accounts = accounts;
}
@Override
public void runTest() throws Throwable {
Random randomGenerator = new Random();

// 随机选取转出户头
int from = randomGenerator.nextInt(accounts.length);

// 随机选取转入户头
int to = randomGenerator.nextInt(accounts.length);

// 确保转出、转入户头不是同一个
while (to == from) {
to = randomGenerator.nextInt(accounts.length);
}

// 随机选取转帐数额(0 ~ 149元之间)
BigDecimal amount = BigDecimal.valueOf(randomGenerator.nextInt(150));

// 转帐!
try {
accountService.transfer(accounts[to], accounts[from], amount);
} catch (AppException ae) {
// 捕捉运行时间异常“AppException”,并打印
System.out.println(ae.getMessage());
}
}
}
}


AccountService 实现类:AccountServiceImpl.java


// import 省略...

/**
* AccountServiceImpl 是 AccountService 接口的实现类。
*
* AccountService 从父接口 EntityService 继承了一系列访问数据库所必需的基本方法的接口。
*
* AccountServiceImpl 从父类 EntityServiceDefaultImpl 继承了一系列访问数据库所必需的基本方法的实现。
*
* AccountService 定义了户头操作所特有的方法接口 (getTotalBalance 与 transfer。)
*
* AccountServiceImpl 定义了户头操作所特有的方法实现 (getTotalBalance 与 transfer。)
*
*/
public final class AccountServiceImpl extends EntityServiceDefaultImpl<Account, Serializable>
implements AccountService {

/* (non-Javadoc)
* @see test.spring.service.AccountService#getTotalBalance(test.spring.entity.Account[])
*/
@Override
public BigDecimal getTotalBalance(Account[] accounts) {
BigDecimal total = BigDecimal.ZERO;
if (null == accounts) {
return total;
}
for (Account account : accounts) {
if (null == account) {
continue;
}
total = total.add(account.getBalance());
}
return total;
}

/* (non-Javadoc)
* @see test.spring.service.AccountService#transfer(test.spring.entity.Account, test.spring.entity.Account, java.math.BigDecimal)
*/
@Override
public void transfer(Account to, Account from, BigDecimal amount) {
if (null == to || null == from) {
return;
}
if (null == amount || BigDecimal.ZERO.equals(amount)) {
return;
}

// 如果转出户头的余额不足,抛出运行时间异常。
if (from.getBalance().compareTo(amount) < 0) {
String msg = String.format(
"Account id [%s] has $%s left only, cannot transfer amount $%s out.",
from.getId(), from.getBalance(), amount);
//System.out.println(msg);
throw new AppException(msg);
}

// 为转出户头设置新余额
from.setBalance(from.getBalance().subtract(amount));

// 为转入户头设置新余额
to.setBalance(to.getBalance().add(amount));

// 将转出户头写入数据库
getDao().update(from);

// 将转入户头写入数据库
getDao().update(to);
}

}


注1:所用的 DAO 继承了标准的 HibernateDaoSupport,就不贴出来了。
注2:AppException 是自己写的异常类,继承了 RuntimeException。

[b]自己想了又想,觉得还是可能在 MySQL 的设置方面做得不够。。。。[/b]

希望有经验的人来聊聊。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值