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

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

阅读更多
andyyehoo 写道
LZ还是没体会rain2005的批评


哈哈,俺认为正好相反。我想了又想,觉得rain2005主要是在批评我在数据库端造成的死锁,而不是java对象这边的线程冲突:因为我姑且相信他能正确理解 hibernate 不同缓存的不同作用。

andyyehoo 写道

一个测试不是变绿了,就表示万事大吉了,真这样的话,Mock的fans就可以横行无忌了


假如在5000个并发线程蹂躏3个帐号(很极端的情形)之后(而且是重复运行这个测试用例很多次之后),数据库状态还是保持了一致(总额不变),我认为这是万事大吉了。

可能我们的基本观念有差异吧。

andyyehoo 写道

你的测试还存在很多漏洞,这些漏洞和数据库无关,


不同意。

andyyehoo 写道

目前看来,数据库的漏洞,通过乐观锁和事务,基本上已经解决了,虽然还不是最优的,但是可以说不会有bug了。


这一点我同意。

andyyehoo 写道

但是代码由于spring和hibernate,已经java多线程并发,还是有问题的。

public void transfer(Account to, Account from, BigDecimal amount)
  from.setBalance(from.getBalance().subtract(amount));  
  to.setBalance(to.getBalance().add(amount));  
  getDao().update(from);  
  getDao().update(to);  


这里首先隐藏了一个不确定因素,hibernate的findby,默认情况下,是同一个AccountId,由于缓存的原因,就会返回相同的Account对象。而Account这个对象,很明显就不是线程安全的,而且还是Rich Object,带了很多的业务逻辑。而且还是和Hibernate直接相关的domain Object,由hibernate的Find By直接返回。然后在accountService这样由spring负责管理的singleton的类进行操作,那么一个Account对象,就有可能即作为一个线程的From的同时,作为另外一个线程的To,这样的不严谨设计,在普通的系统都有问题,何况一个银行系统。单靠数据库的事务来控制,是不严谨的。


完全不同意。缺省状态下,hibernate 的二级缓存是关闭的,只有一级缓存起作用;而一级缓存的作用,仅仅是保证你从同一个 hibernate session 里面,用同样的主键拿出的是同一个对象实例。但假如 hibernate session 并不是同一个呢?

看我以下添加的代码:

	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]));
		}
		
		//.... 以下省略
	}		


上面的 assertEquals 和 assertNotSame 均顺利通过。说明什么?说明了每次用同样的主键交给 accountService.findById 方法,这个方法会打开一个独立的 hibernate session,从数据库取得一个新的 Account 实例(因为只有一级缓存,没有二级缓存嘛),然后关闭刚打开的  hibernate session,最后返回 Account 实例。

所以,你这个帖子的最主要依据,其实是不成立的。

andyyehoo 写道

rain2005的意思是这样的场景:

1)账户A被线程1作为from账户,转帐100到B
2)账户A杯线程2作为to账户,C转帐50给它

那么在transfer的非线程安全代码中,线程1中,A被先-100,在线程1执行update(from)前,线程2做了+50,结果是加了50,然后在数据库的事务竞争中,线程1成功了,于是更新到数据库,而2的这个事务,被完整的不提交而失败。但是这个时候,帐户应该是不平衡的,代码是有漏洞的。

但是实际上,setBalance的操作是非常快的,所以to.setBalance到getDAO().update(from)的时间是非常短的,以LZ的测试强度,加上出现这样情况的概率,基本上很难插入,以至于很少出现这样的情况。更多的情况是,A作为from已经被正确写入数据库,而作为to的A,想要写入数据库的时候,已经由于数据库的脏数据读写问题,失败告终,所以LZ的测试,都是通过的。如果lz在update(from)前加上个随机等待0.1秒,我相信冲突的可能性是非常大的。

简单来说,就是代码的线程不安全性,现在已经被数据库的操作所掩盖,很难重现,但是还是有可能。
......


你说的这些就都没有意义了。因为现在我能保证,每个线程访问的是不同的 Account 实例!所以在Java JVM这边,完全不存在线程安全的问题。

当然,你的论述也说明了很有用的一点:在需要高并发修改数据的场合,慎用二级缓存!

为了大家有个完整的看法,我把最新的测试用例再贴一次:


//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 = 500; // 测试线程总数(即转帐总次数)
	private static int successTransfers = 0;
	private ApplicationContext context;
	private AccountService accountService;
	private long[] accountIds;
	private Map<Long, BigDecimal> balanceTracking = new HashMap<Long, BigDecimal>();;

	/* (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 {

		// 验证在仅有一级缓存的情况下,用同样的主键交给 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", NUM_TRANSFER);

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

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

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

		// 生成测试线程运行器
		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);

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

	/*
	 * 测试线程类定义
	 */
	private static class TransferThread extends TestRunnable {
		private AccountService accountService;
		private long[] accountIds;
		private Map<Long, BigDecimal> balanceTracking;
		public TransferThread(AccountService accountService,
				long[] accountIds, Map<Long, BigDecimal> balanceTracking) {
			super();
			this.accountService = accountService;
			this.accountIds = accountIds;
			this.balanceTracking = balanceTracking;
		}
		@Override
		public void runTest() throws Throwable {
			Random randomGenerator = new Random();

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

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

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

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

			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);
					balanceTracking.put(fromId, oriFromBal.subtract(amount));
					balanceTracking.put(toId, oriToBal.add(amount));
				}
			}
		}
	}
}

分享到:
评论

相关推荐

    多线程测试组件groboutils

    《groboutils:多线程测试组件的卓越选择》 在软件开发过程中,单元测试是确保代码质量的重要环节。它允许开发者对程序中的每个模块进行独立验证,从而尽早发现和修复潜在问题。然而,随着多线程编程的普及,传统的...

    Java多线程Junit测试GroboUtils-5.zip

    在多线程环境中使用JUnit进行测试会面临一些挑战,比如如何确保测试的线程安全,避免测试结果的不确定性,以及如何有效地模拟并发情况。GroboUtils-5可能就是为了解决这些问题而设计的工具集,它可能包含了用于创建...

    groboutils-core-5

    总的来说,`groboutils-core-5`是一个强大的Java单元测试工具,尤其在处理多线程测试方面表现出色。通过熟练掌握这个框架,开发者可以提高测试的质量和效率,为软件项目的稳定性和可靠性打下坚实的基础。而`...

    groboutils

    标题 "groboutils" 指的可能是一个开源工具库,主要用于多线程编程和测试,其中包含了一个核心组件 "groboutils-core-5.jar"。这个库的版本号为5,暗示了它可能经过多次迭代和优化,以提供更好的性能和稳定性。这个...

    net.sourceforge.groboutils.groboutils-core:5

    描述中的"亲测可用"意味着有人已经实际测试过这个版本的Groboutils-core,并确认它在预期环境中能够正常工作。这给其他开发者提供了信心,他们可以相对放心地在自己的项目中使用这个库,而不用担心兼容性或功能性...

    有关Junit和多线程测试的问题

    在使用GroboUtils进行多线程测试时,开发者需要注意以下几点: 1. 设计测试:明确测试目标,确保测试覆盖到多线程交互的关键路径。 2. 使用适当的同步机制:根据需要使用GroboUtils提供的同步工具,避免死锁和竞态...

    GroboUtils-3

    GroboUtils-3 是一个专为进行多线程测试而设计的工具包,它提供了丰富的功能和接口,帮助开发者在复杂的应用场景下测试并优化多线程程序的性能和稳定性。在多线程编程中,确保并发执行的正确性和效率至关重要,因为...

    GroboUtils-5-core

    GroboUtils-5-core 是一个专门针对Java开发的测试工具包,主要目的是为了简化和加速Junit测试中的多线程并发测试。这个工具包的核心功能是帮助开发者在编写测试用例时,能够轻松地创建和管理多线程环境,从而确保在...

    GroboUtils-5-core.jar

    GroboUtils库是针对Java开发者的实用工具集,尤其在处理多线程测试方面展现出了强大的功能。这个名为"core"的模块,即groboutils-core-5.jar,是该库的核心部分,包含了各种专为多线程测试设计的类和方法。下面我们...

    groboutils-core-5.jar.7z

    这种测试通常会模拟多个用户同时访问系统,以检测潜在的线程安全问题、资源管理问题以及在高压力环境下的性能瓶颈。这一步对于确保软件在生产环境中能够可靠运行至关重要。 “自测jar包没有问题”意味着发布者已经...

    groboutils-core-5.jar

    groboutils-core-5.jar 单元测试多线程

    java并发测试

    `MutiThreadTest.java` 可能会使用这些工具来模拟并行执行的任务,测试目标接口在多线程环境下的行为。 在并发测试中,我们通常关注以下几个方面: 1. **性能测试**:衡量多线程环境下接口的响应时间和吞吐量,以...

    pay-facade简易版支付系统源码所需jar包:groboutils-core-5.jar、proton-jms-0.3.0-fuse-2.jar等

    包含:groboutils-core-5.jar、fastdfs-client、kaptcha-2.3.2.jar、pinyin4j-2.5.0.jar、proton-jms-0.3.0-fuse-2.jar等

Global site tag (gtag.js) - Google Analytics