对一个所谓 “真正的测试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:
原测试类 AccountTransferMultiThreadTest.java,已做必要修改:
线程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 访问控制 (转)