欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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

程序员文章站 2024-03-19 13:57:28
...
[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));
}
}
}
}
}

上一篇: Nginx 访问控制 (转)

下一篇: