对一个所谓 “真正的测试spring并发的事务正确性” 的证伪

[quote="rain2005"]楼主的代码是没有问题的,其实我想表达的意思就是楼主的测试并发大时必然死锁,从楼主的标题看是想测试spring事务的并发,楼主完全可以这样
线程1操作帐户A,B,线程2操作帐户C,D,线程3操作帐户E,F,这样才是真正的测试[color=red]spring并发的事务正确性[/color]。

如果想测试程序的健壮性,如死锁可以再写测试用例。

总之保证,每个测试用例目标明确。[/quote]

好了!花了点时间,完善了我对上述[url=http://www.iteye.com/topic/436718?page=4#1116566]rain2005 所臆想场景[/url]的模拟,并证明了此提议的荒谬。

我先给原本的测试类做了些必要的修改,然后为其添加子类 AccountTransferMultiThreadTestAccountsNotConflict。

目的:让每一个转帐线程所选取的转出(from)与转入(to)户头[b][color=red]不[/color][/b]与其它线程相冲突。也就是说,模拟了场景:[b][color=red]“楼主完全可以这样 线程1操作帐户A,B,线程2操作帐户C,D,线程3操作帐户E,F,这样才是真正的测试spring并发的事务正确性。”[/color][/b],并证明其提议之荒谬:在这种“井水不犯河水”的操作下,根本测不了什么“spring并发的事务正确性”。

结果:在打开 spring 声明式事务处理的情况下,父测试类 AccountTransferMultiThreadTest(各转帐线程选取的户头出现冲突)与本测试类 AccountTransferMultiThreadTestAccountsNotConflict(各转帐线程选取的户头没有任何冲突)[b][color=red]均顺利通过[/color][/b],junit 显示绿色条;

在关闭 spring 声明式事务处理的情况下,父测试类 AccountTransferMultiThreadTest(各转帐线程选取的户头出现冲突)[b][color=red]失败[/color][/b],junit 显示红色条。从错误信息发现,转帐前后,所有账户总额不一致;

而在同样关闭 spring 声明式事务处理的情况下,子测试类 AccountTransferMultiThreadTestAccountsNotConflict(各转帐线程选取的户头没有任何冲突)[b][color=red]仍然顺利通过[/color][/b],junit 显示绿色条。从打印信息发现,转帐前后,所有账户总额保持一致,并且每个账户的最终余额和记录(balanceTracking)中完全相同;

证明:AccountTransferMultiThreadTestAccountsNotConflict 所模拟的这种(户头没有冲突的)操作[b][color=red]完全测试不了[/color][/b]“spring并发的事务正确性”,rain2005 的提案没有任何意义;而我在顶楼(当时有小错误,后来已修正)所提出的做法才能真正达到这个目的。

[color=red][b]希望大家在发贴的时候要谨慎些。不管你自己有多菜,总有比你更菜的人。你那些不负责任的论断,极有可能给他们造成误导。[/b][/color]


附代码:


子测试类:AccountTransferMultiThreadTestAccountsNotConflict.java:

//import 省略

/**
*
* 测试类 AccountTransferMultiThreadTestAccountsNotConflict 继承了
* 测试类 AccountTransferMultiThreadTest。
*
* 目的:让每一个转帐线程所选取的转出(from)与转入(to)户头不与其它线程相冲突。也就是说,
* 模拟了 iteye.com 中某某人所臆想的场景:<b>“楼主完全可以这样 线程1操作帐户A,B,
* 线程2操作帐户C,D,线程3操作帐户E,F,这样才是真正的测试spring并发的事务正确性。”</b>,
* 并证明他的提议之荒谬:在这种“井水不犯河水”的操作下,根本测不了什么“spring并发的事务正确性”。
*/
public class AccountTransferMultiThreadTestAccountsNotConflict extends
AccountTransferMultiThreadTest {

private LinkedList<Long> accountIdsNotChosen;

public AccountTransferMultiThreadTestAccountsNotConflict() {
super();

// 重新设置父类中定义的 测试户头的总数 和 测试线程总数。
numOfAccounts = 200; // 测试户头的总数。这里,它必须是偶数。
numOfTransfers = 100; // 测试线程总数(即转帐总次数。这里,它必须等于 测试户头总数 的一半。)
}

protected void setUp() throws Exception {
super.setUp();

// 利用“accountIdsNotChosen”,避免重复选取户头。
accountIdsNotChosen = new LinkedList<Long>();
for (Long id : accountIds) {
accountIdsNotChosen.add(id);
}
}

protected void tearDown() throws Exception {
super.tearDown();
}

/* (non-Javadoc)
* @see xiao.test.spring.testCases.AccountTransferMultiThreadTest#generateTransferThread()
*/
@Override
protected TransferThread generateTransferThread() {
return new TransferThreadAccountsNotConflict(accountService, accountIds,
accountIdsNotChosen, balanceTracking);
}

/* (non-Javadoc)
* @see xiao.test.spring.testCases.AccountTransferMultiThreadTest#testMultiThreadTransfer()
*/
@Override
public void testMultiThreadTransfer() throws Throwable {
super.testMultiThreadTransfer();
}

private static class TransferThreadAccountsNotConflict extends TransferThread {
private LinkedList<Long> accountIdsNotChosen;

public TransferThreadAccountsNotConflict(AccountService accountService,
long[] accountIds, LinkedList<Long> accountIdsNotChosen, Map<Long, BigDecimal> balanceTracking) {

super(accountService, accountIds, balanceTracking);

this.accountIdsNotChosen = accountIdsNotChosen;
if (accountIdsNotChosen.size() <= 1) {
throw new AppException("There are at most 1 account in 'not chosen list', cannot"
+ " choose 2 accounts to make a transfer!");
}
}

/* (non-Javadoc)
* @see xiao.test.spring.testCases.AccountTransferMultiThreadTest.TransferThread#generateTransferOptions()
*/
@Override
protected void generateTransferOptions() {
Random randomGenerator = new Random();

synchronized (accountIdsNotChosen) {
// 随机选取转出户头
int i = randomGenerator.nextInt(accountIdsNotChosen.size());
fromId = accountIdsNotChosen.remove(i);

// 随机选取转入户头
i = randomGenerator.nextInt(accountIdsNotChosen.size());
toId = accountIdsNotChosen.remove(i);
}

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

/* (non-Javadoc)
* @see xiao.test.spring.testCases.AccountTransferMultiThreadTest.TransferThread#runTest()
*/
@Override
public void runTest() throws Throwable {
super.runTest();
}

}
}



原测试类 AccountTransferMultiThreadTest.java,已做必要修改:

//import 省略

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

protected int numOfAccounts; // 测试户头的总数
protected int numOfTransfers; // 测试线程总数(即转帐总次数)
private ApplicationContext context;
protected AccountService accountService;
protected long[] accountIds;
protected Map<Long, BigDecimal> balanceTracking = new HashMap<Long, BigDecimal>();;

public AccountTransferMultiThreadTest() {
super();
numOfAccounts = 10; // 测试户头的总数
numOfTransfers = 300; // 测试线程总数(即转帐总次数)

context = new ClassPathXmlApplicationContext("xiao/test/spring/*Context.xml");
accountService = (AccountService) context.getBean("accountService");
}

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

Account[] accounts = new Account[numOfAccounts];
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();
}

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

protected TransferThread generateTransferThread() {
return new TransferThread(accountService, accountIds, balanceTracking);
}

public void testMultiThreadTransfer() throws Throwable {

// 验证在仅有一级缓存的情况下,用同样的主键交给 accountService.findById,它每次
// 返回的是相等,但并不同一的实例。
// 当然了,Account 对象的 equals 必须被正确的覆盖先。
for (int i = 0; i < accountIds.length; i++) {
assertEquals(accountService.findById(accountIds[i]),
accountService.findById(accountIds[i]));
assertNotSame(accountService.findById(accountIds[i]),
accountService.findById(accountIds[i]));
}

// 获取户头对象数组
Account[] accounts = getAccounts();
//System.out.printf("Starting %s transfers...\n", numOfTransfers);

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

// 记录测试前的所有户头的余额
for (Account account : accounts) {
balanceTracking.put(account.getId(), account.getBalance());
}

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

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

// 运行测试线程
mttr.runTestRunnables();
long used = System.currentTimeMillis() - start;
System.out.printf("Total: %s transfers used %s milli-seconds.\n", numOfTransfers, used);

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

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

// 确认测试前后,所有户头余额与转帐记录相一致。
System.out.printf("Successful transfers: %s\n", successTransfers);
System.out.println(balanceTracking);
for (Account account : accounts2) {
assertEquals(balanceTracking.get(account.getId()), account.getBalance());
}
}

/*
* 测试线程类定义
*/
protected static class TransferThread extends TestRunnable {
private AccountService accountService;
private Map<Long, BigDecimal> balanceTracking;
protected long[] accountIds;
protected long fromId;
protected long toId;
protected BigDecimal amount;

public TransferThread(AccountService accountService,
long[] accountIds, Map<Long, BigDecimal> balanceTracking) {
super();
this.accountService = accountService;
this.accountIds = accountIds;
this.balanceTracking = balanceTracking;
}

protected void generateTransferOptions() {
Random randomGenerator = new Random();

// 随机选取转出户头
fromId = accountIds[
randomGenerator.nextInt(accountIds.length)
];

// 随机选取转入户头
toId = accountIds[
randomGenerator.nextInt(accountIds.length)
];

// 确保转出、转入户头不是同一个
while (toId == fromId) {
toId = accountIds[
randomGenerator.nextInt(accountIds.length)
];
}

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

@Override
public void runTest() throws Throwable {
generateTransferOptions();

boolean success;
// 转帐!
try {
accountService.transfer(
accountService.findById(toId),
accountService.findById(fromId),
amount);
success = true;
} catch (AppException ae) {
// 捕捉运行时间异常“AppException”。在真实的系统中,这里必须通知用户:转帐失败,请稍后再试。
//System.out.println("AppException:" + ae.getMessage());
success = false;
} catch (Throwable t) {
// 捕捉所有异常。在真实的系统中,这里必须通知用户:转帐失败,请稍后再试。
success = false;
}
if (success) {
// 以下记录每一次成功的转帐后,被影响户头的余额。假如在 accountService.transfer 中有异常抛出,
// 这一记录动作将不会执行。
synchronized (balanceTracking) {
successTransfers ++;
BigDecimal oriFromBal = balanceTracking.get(fromId);
BigDecimal oriToBal = balanceTracking.get(toId);
System.out.printf("Successful transfer no.%s: account[%s] (bal: %s) -> account[%s] (bal: %s),"
+ " amount (%s)\n", successTransfers, fromId, oriFromBal, toId, oriToBal, amount);
balanceTracking.put(fromId, oriFromBal.subtract(amount));
balanceTracking.put(toId, oriToBal.add(amount));
}
}
}
}
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值