`
mysaga
  • 浏览: 18938 次
  • 性别: Icon_minigender_2
  • 来自: 成都
文章分类
社区版块
存档分类
最新评论

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

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

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

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


好了!花了点时间,完善了我对上述rain2005 所臆想场景的模拟,并证明了此提议的荒谬。

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

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

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

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

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

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

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



附代码:



子测试类: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
分享到:
评论

相关推荐

    spring培训笔记

    spring基础 知识要点: Spring的事务管理及实现,Spring操作Hibernate的...事务允许多个用户对同一数据并发访问, 各个事务互相独立, 且不破坏数据的正确性和完整性. Durability: 成功提交的事务的持久性. 可掉电保存.

    领域驱动设计与模式实战

    1.5.1 有关何时需要运行机制的一个例子 1.5.2 运行机制的一些例子 1.5.3 它不仅仅是我们的过错 1.6 小结 第2章 模式起步 2.1 模式概述 2.1.1 为什么要学习模式 2.1.2 在模式方面要注意哪些事情 2.2 设计模式 2.3 ...

    Java学习资料-SpringBoot整合Mybatis实战

    在执行 Lua 脚本之前开启一个事务,执行完毕后再提交事务。 4. 问题:Lua 脚本的安全性问题。 解决方案:对 Lua 脚本进行充分的安全校验,避免被恶意利用。可以使用白名单机制来限制可执行的 Lua 脚本。 5. 问题...

    基于SpringBoot的高并发选课系统源码+项目说明(毕设).zip

    本项目主要解决在高校选课场景下,保证选课系统在大量读写压力下不宕机,以及选课时尽可能提高选课QPS,给学生一个良好的选课体验,完成上述功能同时保证选课安全 ## 技术选型 前端:Bootstrap、JQuery、Thymeleaf ...

    java开源包1

    GWT Spring 使得在 Spring 框架下构造 GWT 应用变得很简单,提供一个易于理解的依赖注入和RPC机制。 Java扫雷游戏 JVMine JVMine用Applets开发的扫雷游戏,可在线玩。 public class JVMine extends java.applet....

    java开源包11

    GWT Spring 使得在 Spring 框架下构造 GWT 应用变得很简单,提供一个易于理解的依赖注入和RPC机制。 Java扫雷游戏 JVMine JVMine用Applets开发的扫雷游戏,可在线玩。 public class JVMine extends java.applet....

    java开源包2

    GWT Spring 使得在 Spring 框架下构造 GWT 应用变得很简单,提供一个易于理解的依赖注入和RPC机制。 Java扫雷游戏 JVMine JVMine用Applets开发的扫雷游戏,可在线玩。 public class JVMine extends java.applet....

    java开源包3

    GWT Spring 使得在 Spring 框架下构造 GWT 应用变得很简单,提供一个易于理解的依赖注入和RPC机制。 Java扫雷游戏 JVMine JVMine用Applets开发的扫雷游戏,可在线玩。 public class JVMine extends java.applet....

    java开源包6

    GWT Spring 使得在 Spring 框架下构造 GWT 应用变得很简单,提供一个易于理解的依赖注入和RPC机制。 Java扫雷游戏 JVMine JVMine用Applets开发的扫雷游戏,可在线玩。 public class JVMine extends java.applet....

    java开源包5

    GWT Spring 使得在 Spring 框架下构造 GWT 应用变得很简单,提供一个易于理解的依赖注入和RPC机制。 Java扫雷游戏 JVMine JVMine用Applets开发的扫雷游戏,可在线玩。 public class JVMine extends java.applet....

    java开源包10

    GWT Spring 使得在 Spring 框架下构造 GWT 应用变得很简单,提供一个易于理解的依赖注入和RPC机制。 Java扫雷游戏 JVMine JVMine用Applets开发的扫雷游戏,可在线玩。 public class JVMine extends java.applet....

    java开源包4

    GWT Spring 使得在 Spring 框架下构造 GWT 应用变得很简单,提供一个易于理解的依赖注入和RPC机制。 Java扫雷游戏 JVMine JVMine用Applets开发的扫雷游戏,可在线玩。 public class JVMine extends java.applet....

    java开源包8

    GWT Spring 使得在 Spring 框架下构造 GWT 应用变得很简单,提供一个易于理解的依赖注入和RPC机制。 Java扫雷游戏 JVMine JVMine用Applets开发的扫雷游戏,可在线玩。 public class JVMine extends java.applet....

    java开源包7

    GWT Spring 使得在 Spring 框架下构造 GWT 应用变得很简单,提供一个易于理解的依赖注入和RPC机制。 Java扫雷游戏 JVMine JVMine用Applets开发的扫雷游戏,可在线玩。 public class JVMine extends java.applet....

    java开源包9

    GWT Spring 使得在 Spring 框架下构造 GWT 应用变得很简单,提供一个易于理解的依赖注入和RPC机制。 Java扫雷游戏 JVMine JVMine用Applets开发的扫雷游戏,可在线玩。 public class JVMine extends java.applet....

    java开源包101

    GWT Spring 使得在 Spring 框架下构造 GWT 应用变得很简单,提供一个易于理解的依赖注入和RPC机制。 Java扫雷游戏 JVMine JVMine用Applets开发的扫雷游戏,可在线玩。 public class JVMine extends java.applet....

    Java资源包01

    GWT Spring 使得在 Spring 框架下构造 GWT 应用变得很简单,提供一个易于理解的依赖注入和RPC机制。 Java扫雷游戏 JVMine JVMine用Applets开发的扫雷游戏,可在线玩。 public class JVMine extends java.applet....

    JAVA上百实例源码以及开源项目源代码

    5个目标文件,演示Address EJB的实现,创建一个EJB测试客户端,得到名字上下文,查询jndi名,通过强制转型得到Home接口,getInitialContext()函数返回一个经过初始化的上下文,用client的getHome()函数调用Home接口...

    JAVA上百实例源码以及开源项目

    5个目标文件,演示Address EJB的实现,创建一个EJB测试客户端,得到名字上下文,查询jndi名,通过强制转型得到Home接口,getInitialContext()函数返回一个经过初始化的上下文,用client的getHome()函数调用Home接口...

Global site tag (gtag.js) - Google Analytics