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

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

阅读更多
之前的公司里,曾经在几个项目里用过 Spring + Hibernate 架构。

其中,使用了标准的 Spring 声明式事务管理(相关的文章、示例在网上随处可见)。因为当时的项目对并发访问的要求并不高,加上赶进度,所以从来没有在真正高并发的情形下,测试过系统数据库事务管理是否正确。

(唯一的确认行为,就是打开数据库本身的记录,看里面是否有事务管理的SQL代码出现)

当然了,我自己也承认这样的做法可能隐含严重的问题,所以一直在想好好做一下测试。

最近比较闲一点,就自己编了个测试用例,在 Spring + Hibernate + MySQL 环境里,使用跟 junit 集成的多线程工具 groboutils 跑了一下。


做法:
同时并发比较大数量(比如说,3000个)的测试线程;在每个线程中,从一系列(比如说,10个)共享的“银行户头”里随机挑选两个,进行转帐;


期望:
在没有配置声明式事务管理(事务方式,隔离方式等)时,转帐前、后,所有“银行户头”的总额出现误差。

而在配置声明式事务管理后,转帐前、后的总额保持一致。


结果:
当使用 MySQL InnoDB 类型表格时,出现死锁异常:(JDBCExceptionReporter.java:101) - Deadlock found when trying to get lock; try restarting transaction

当使用 MySQL MyISAM 类型表格时(死马当活马,试试看),死锁异常没了,但是无论怎么配置事务管理,都不管用,assertEquals 失败,转帐前、后的总额不一致。


补充:
当使用 MySQL InnoDB 类型表格时,死锁异常一般出现在测试线程数量较多的时候。当减小测试线程数量(减到100个)、增加共享的“银行户头”数量(加到50个)时,死锁异常不再出现。但是!!事务管理照样不管用!转帐前、后,所有“银行户头”的总额不一致,有时候变多,有时候变少......

希望做过类似测试的进来讨论讨论。

重要部分的源代码如下:

JUnit testcase: AccountTransferMultiThreadTest.java(此测试用例最新最完整的代码在这一个跟贴的末尾)

// 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 = 3000; // 测试线程总数(即转帐总次数)
	private ApplicationContext context;
	private AccountService accountService;
	private long[] accountIds;

	/* (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 {
		// 获取户头对象数组
		Account[] accounts = getAccounts();
		System.out.printf("Starting %s transfers...\n", NUM_TRANSFER);

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

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

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

	/*
	 * 测试线程类定义
	 */
	private static class TransferThread extends TestRunnable {
		private AccountService accountService;
		private Account[] accounts;
		public TransferThread(AccountService accountService, Account[] accounts) {
			super();
			this.accountService = accountService;
			this.accounts = accounts;
		}
		@Override
		public void runTest() throws Throwable {
			Random randomGenerator = new Random();

			// 随机选取转出户头
			int from = randomGenerator.nextInt(accounts.length);

			// 随机选取转入户头
			int to = randomGenerator.nextInt(accounts.length);

			// 确保转出、转入户头不是同一个
			while (to == from) {
				to = randomGenerator.nextInt(accounts.length);
			}

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

			// 转帐!
			try {
				accountService.transfer(accounts[to], accounts[from], amount);
			} catch (AppException ae) {
				// 捕捉运行时间异常“AppException”,并打印
				System.out.println(ae.getMessage());
			}
		}
	}
}


AccountService 实现类:AccountServiceImpl.java

// import 省略...

/**
 * AccountServiceImpl 是 AccountService 接口的实现类。
 *
 * AccountService 从父接口 EntityService 继承了一系列访问数据库所必需的基本方法的接口。
 *
 * AccountServiceImpl 从父类 EntityServiceDefaultImpl 继承了一系列访问数据库所必需的基本方法的实现。
 *
 * AccountService 定义了户头操作所特有的方法接口 (getTotalBalance 与 transfer。)
 *
 * AccountServiceImpl 定义了户头操作所特有的方法实现 (getTotalBalance 与 transfer。)
 *
 */
public final class AccountServiceImpl extends EntityServiceDefaultImpl<Account, Serializable>
		implements AccountService {

	/* (non-Javadoc)
	 * @see test.spring.service.AccountService#getTotalBalance(test.spring.entity.Account[])
	 */
	@Override
	public BigDecimal getTotalBalance(Account[] accounts) {
		BigDecimal total = BigDecimal.ZERO;
		if (null == accounts) {
			return total;
		}
		for (Account account : accounts) {
			if (null == account) {
				continue;
			}
			total = total.add(account.getBalance());
		}
		return total;
	}

	/* (non-Javadoc)
	 * @see test.spring.service.AccountService#transfer(test.spring.entity.Account, test.spring.entity.Account, java.math.BigDecimal)
	 */
	@Override
	public void transfer(Account to, Account from, BigDecimal amount) {
		if (null == to || null == from) {
			return;
		}
		if (null == amount || BigDecimal.ZERO.equals(amount)) {
			return;
		}
		
		// 如果转出户头的余额不足,抛出运行时间异常。
		if (from.getBalance().compareTo(amount) < 0) {
			String msg = String.format(
					"Account id [%s] has $%s left only, cannot transfer amount $%s out.",
					from.getId(), from.getBalance(), amount);
			//System.out.println(msg);
			throw new AppException(msg);
		}
		
		// 为转出户头设置新余额
		from.setBalance(from.getBalance().subtract(amount));
		
		// 为转入户头设置新余额
		to.setBalance(to.getBalance().add(amount));

		// 将转出户头写入数据库
		getDao().update(from);
		
		// 将转入户头写入数据库
		getDao().update(to);
	}

}


注1:所用的 DAO 继承了标准的 HibernateDaoSupport,就不贴出来了。
注2:AppException 是自己写的异常类,继承了 RuntimeException。

自己想了又想,觉得还是可能在 MySQL 的设置方面做得不够。。。。

希望有经验的人来聊聊。
分享到:
评论
64 楼 mislay 2009-08-25  
C_J 写道
引用
在一个事物未提交前,另一个事物不能开始,这样也能出现死锁吗?


在同一个事务为什么不会出现死锁呢?

例如:同一个事务,以下update操作:

update 1
update 2
update 1
update 2

commit;

LZ有3000个进程随机update就会出现这种状况~Oracle也是一样,应该所有数据库都是这样的,只是解决死锁问题的能力不一样。

不管LZ是否是进程还是线程,最终数据库只是自己维护自己的。

如果像你所说,一个事务都有可能发生死锁,那我是不是可以理解为一个事务是数据库中多个线程来完成的呢?因为一个线程来完成,我想多次拿锁不是问题的。

还是同一个问题:在一个事物未提交前,另一个事物不能开始,串行真的是这样吗?
63 楼 C_J 2009-08-25  
引用
在一个事物未提交前,另一个事物不能开始,这样也能出现死锁吗?


在同一个事务为什么不会出现死锁呢?

例如:同一个事务,以下update操作:

update 1
update 2
update 1
update 2

commit;

LZ有3000个进程随机update就会出现这种状况~Oracle也是一样,应该所有数据库都是这样的,只是解决死锁问题的能力不一样。
62 楼 mislay 2009-08-16  
所有的回帖看了一遍,但是也有了一些些疑问。

首先死锁问题可以肯定是出在数据库上面,但是隔离级别都用上串行了,在一个事物未提交前,另一个事物不能开始,这样也能出现死锁吗?又或者我是把这个隔离级别理解错了吗?又或者mysql自身机制问题?不知oracle是否如此。但是我感觉串行就不该有死锁发生.

另外一个问题,针对于这个典型的用例中,悲观锁下是否只有串行的隔离级别才能保证数据的完整性?异或可重复读?幻读?

        // 将转出户头写入数据库  
        getDao().update(from);  
          
        // 将转入户头写入数据库  
        getDao().update(to);

最后一个问题,采用乐观锁前提是需要保证遵循它自己的机制?如果是这样,那么hibernate的自带的悲观锁是否同样可保证?当然这里指的是单生产环境。

谢谢。
61 楼 mysaga 2009-08-15  
daquan198163 写道
mysaga 写道
现在回想起来,平常编程的时候,确实习惯了从 hibernate (无2级缓存)里面拿数据,处理,然后更新——那种情形下,很难出现这种多线程访问同一对象的错误,所以在写多线程测试用例的时候,就大意了。。。。。


不是说二级缓存没关系么?


我的意思是说,在 浏览器-服务器 编程环境下,你考虑的一般仅仅是当前用户的访问,自然也就是单线程的环境;在这种情况下,如果 hibernate 没有配置 2 级缓存,你拿出来的对象本身,基本上是不会出现并发线程访问的。

而因为习惯了这种方式,我在开始写这个测试用例的时候,就不小心忽略了对象线程安全的问题。
60 楼 daquan198163 2009-08-14  
mysaga 写道
现在回想起来,平常编程的时候,确实习惯了从 hibernate (无2级缓存)里面拿数据,处理,然后更新——那种情形下,很难出现这种多线程访问同一对象的错误,所以在写多线程测试用例的时候,就大意了。。。。。


不是说二级缓存没关系么?
nihongye 写道
引用
1.关于hibernate的二级缓存构造出来的对象是线程不安全的

这种说法是不对的,对象不是从缓存直接取出来的,缓存的是分解了的对象,每次取,对象是被重新构造出来的。
至于数据与数据库数据库一致性方面:因为二级缓存本身提供了几种策略,有不一致的也有一致的
59 楼 mysaga 2009-08-14  
C_J 写道
我想很多刚学习的人看懂这个帖子应该还是有点点难度的吧

反正我是了2遍才看明白:

原来楼主的有2个问题:

1,数据库本身的死锁,多线程操作2条记录以上时,出现相互等待update现象.
2,JVM对象的并发问题.当然是由于
int from = randomGenerator.nextInt(accounts.length);
这条代码引起的,可能上千个线程共用了一个accounts对象.

如上有错误的地方,还请大家指正.


基本上是这样的。

1,是真正困扰我的问题。

2,“JVM对象的并发”,纯粹是不小心,出的低级错误。有人指出后,我马上就改了。

现在回想起来,平常编程的时候,确实习惯了从 hibernate (无2级缓存)里面拿数据,处理,然后更新——那种情形下,很难出现这种多线程访问同一对象的错误,所以在写多线程测试用例的时候,就大意了。。。。。
58 楼 C_J 2009-08-11  
我想很多刚学习的人看懂这个帖子应该还是有点点难度的吧

反正我是了2遍才看明白:

原来楼主的有2个问题:

1,数据库本身的死锁,多线程操作2条记录以上时,出现相互等待update现象.
2,JVM对象的并发问题.当然是由于
int from = randomGenerator.nextInt(accounts.length);
这条代码引起的,可能上千个线程共用了一个accounts对象.

如上有错误的地方,还请大家指正.
57 楼 mysaga 2009-08-06  
haoxichuan 写道
非常感谢 daquan198163 mysaga 的解释
也就是说在高并发的情况下,数据库会出现一些死锁,或者其它错误,数据库能解决这些事情,但是他的解决方案不能保证数据的正确性.我们通过程序使用事务和锁去保证我们数据的正确性.使用 乐观或悲观 锁都可以解决是吧~~~


daquan198163在你楼下解释得很清楚了。。。。

haoxichuan 写道
LZ最开始的时候测试是没有加任何锁是吗?加悲观锁是不是只能在SQL语句上加,有没有其它可配置的地方?



这个问题有点复杂。

一句话答案:是的,最开始的时候没有加,是我的疏漏。所以那时候测试不能通过,数据不一致。

后来加上了,但是又被那些死锁的异常给迷惑了。

当然现在明白,死锁并不可怕,只要处理好就行了。而正因为加上了锁,测试才能通过,数据才变得一致。
56 楼 daquan198163 2009-08-06  
haoxichuan 写道
非常感谢 daquan198163 mysaga 的解释
也就是说在高并发的情况下,数据库会出现一些死锁,或者其它错误,数据库能解决这些事情,但是他的解决方案不能保证数据的正确性.我们通过程序使用事务和锁去保证我们数据的正确性.使用 乐观或悲观 锁都可以解决是吧~~~

你先看一下数据库的隔离级别的概念。
如果把数据库隔离级别设成serialize或repeatable read,也能保证数据的正确性,但性能差,也不是所有数据库都支持。所以,通常情况下,数据库都采用read commited,既保证了性能又保证了绝大多数(80%)情况下的一致性,
但时还有那20%——并发更新同一条记录——会出现更新丢失的问题,
于是就需要针对这20%的操作采用锁,如果预计很少出现并发冲突并且允许操作失败,就采用乐观锁,反之采用悲观锁。
引用
LZ最开始的时候测试是没有加任何锁是吗?加悲观锁是不是只能在SQL语句上加,有没有其它可配置的地方?

最开始没有加。 悲观锁最终都要体现到select for update,但hibernate提供了api,jdo可以配置。
参考《POJO in action》
55 楼 haoxichuan 2009-08-06  
非常感谢 daquan198163 mysaga 的解释
也就是说在高并发的情况下,数据库会出现一些死锁,或者其它错误,数据库能解决这些事情,但是他的解决方案不能保证数据的正确性.我们通过程序使用事务和锁去保证我们数据的正确性.使用 乐观或悲观 锁都可以解决是吧~~~

LZ最开始的时候测试是没有加任何锁是吗?加悲观锁是不是只能在SQL语句上加,有没有其它可配置的地方?
54 楼 mysaga 2009-08-06  
daquan198163 写道
先确认一下数据库死锁的定义:A B两个并发事务分别持有了对方需要的一行记录的更新锁,因此无论等待多久AB都无法得到需要的锁。

因此,楼主的测试可能会出现死锁,比如事务A B都要更新账户1和账户2的余额,只不过转账方向不同(对于企业账户,这种情况很正常),
如果A获得了账户1的锁,B获得了账户2的锁,于是出现互相等待。

现在的数据库可以帮我们自动解决绝大部分死锁,这里的解决指的应该是避免出现无限等待,一旦发现死锁就抛异常,强行终止一个事务。

前面说的都是数据库本身的特性,但它对于保证交易的一致性还不够,因为可能出现“更新丢失”现象(比如转账前后,帐不平),这只能靠应用(通过乐观锁或悲观锁)来解决,其中乐观锁不依赖数据库(靠version、时间戳等),而悲观锁依赖数据库的select for update特性。


赞成你的观点。特别是这一句:“现在的数据库可以帮我们自动解决绝大部分死锁,这里的解决指的应该是避免出现无限等待,一旦发现死锁就抛异常,强行终止一个事务。”这也是我现在的看法:在高并发的情形下,操作失败难以避免,系统的责任就是(1)正确处理异常(包括正确的提示用户),(2)严格保证数据一致。

关于锁,在我的实际测试中,也发现spring端的事务处理和锁(乐观或悲观),是二者缺一不可的。去掉任何一个,都会造成户头总额前后不一致。

通过发这个帖子以及后来的讨论,我确实学到许多东西。感谢各位热心的兄弟。
53 楼 mysaga 2009-08-06  
haoxichuan 写道
to mysaga
如果不用乐观锁,数据库也有自己的锁机制呀.为什么你测的这种多个线程操作同一帐户会出现死锁的情况呢?能把这种情况出现的具体说下吗?


我的看法是:数据库锁是保证数据一致的基本手段。而就是因为有了数据库锁,才会出现“死锁”——当然,没有数据库锁就没有死锁(我试过,不用任何一种锁,确实是没有异常的),但那样的话也就无法保证数据一致,那样的系统也就没什么人敢用了。

在我的测试里,如果使用乐观锁(hibernate 的 version 版本),出现的异常是

ERROR AbstractFlushingEventListener:324 - Could not synchronize database state with session
org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect)


如果使用悲观锁(LockMode.UPGRADE,其实也就是“select ... for update”),出现的异常是

ERROR JDBCExceptionReporter:101 - Deadlock found when trying to get lock; try restarting transaction



然而无论哪种锁哪种异常,在正确的捕捉异常后,我总可以看见 junit 的绿色条:因为数据完全保持了一致。我想这就是事务处理、锁、。。。。等等的意义所在。

而当然,在实际系统中,处理异常也是很重要的:至少,必须告诉相关的用户:“你刚刚的操作失败,请稍后再试。”
52 楼 daquan198163 2009-08-06  
Spring的事务处理绝对是线程安全的,否则也不要出来混了。
它把事务上下文、数据库连接、hibernate session等等这些非线程安全的东西都用threadlocal绑定到了当前线程,根本不存在线程安全的问题。
至于数据库连接池、hibernate session工厂等,本来就是线程安全的,不需要spring来保证。
51 楼 daquan198163 2009-08-06  
先确认一下数据库死锁的定义:A B两个并发事务分别持有了对方需要的一行记录的更新锁,因此无论等待多久AB都无法得到需要的锁。

因此,楼主的测试可能会出现死锁,比如事务A B都要更新账户1和账户2的余额,只不过转账方向不同(对于企业账户,这种情况很正常),
如果A获得了账户1的锁,B获得了账户2的锁,于是出现互相等待。

现在的数据库可以帮我们自动解决绝大部分死锁,这里的解决指的应该是避免出现无限等待,一旦发现死锁就抛异常,强行终止一个事务。

前面说的都是数据库本身的特性,但它对于保证交易的一致性还不够,因为可能出现“更新丢失”现象(比如转账前后,帐不平),这只能靠应用(通过乐观锁或悲观锁)来解决,其中乐观锁不依赖数据库(靠version、时间戳等),而悲观锁依赖数据库的select for update特性。
50 楼 haoxichuan 2009-08-06  
to mysaga
如果不用乐观锁,数据库也有自己的锁机制呀.为什么你测的这种多个线程操作同一帐户会出现死锁的情况呢?能把这种情况出现的具体说下吗?
49 楼 mysaga 2009-08-05  
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));
				}
			}
		}
	}
}

48 楼 mysaga 2009-08-04  
nihongye 写道
引用
1.关于hibernate的二级缓存构造出来的对象是线程不安全的

这种说法是不对的,对象不是从缓存直接取出来的,缓存的是分解了的对象,每次取,对象是被重新构造出来的。
至于数据与数据库数据库一致性方面:因为二级缓存本身提供了几种策略,有不一致的也有一致的

引用
2.锁问题

悲观锁或是乐观锁在例子中高并发的情况下,死锁都是必然的,因为都要执行数据库更新,无可避免的产生行锁,
这个时候就依赖于数据库的死锁检查机制了,悲观锁从读就产生,所以死锁的几率高些;乐观锁不是读的时候就上锁,支持长事务,泡杯茶后继续那种。
至于使用java的锁机制:简单点,同步所有对账户的操作,并发效果差些,无死锁。
使用高级点的锁机制,一开始就锁定所有lock1.tryLock(),lock2.tryLock(),lock...,lockn.tryLock(),依然无死锁。


哦,受教了。
47 楼 mysaga 2009-08-04  
rain2005 写道
按照你的说法,线程1操作帐户A,B,线程2操作帐户C,D,线程3操作帐户E,F这个是不需要事务的???


你又来了。。。。任意曲解、发挥别人的话。。。

我只是说,你的理想场景测试不了“并发的事务控制”,因为三个转帐互不干扰!!

A给B转300块,C给D转500块,E给F转600块,这三个操作同时发生跟先后发生,有什么区别?

在我的测试用例里面如果改成这样,那我告诉你,spring context xml 里面加不加事务处理配置,junit 的测试结果都是绿色条!你还真以为我没试过???

rain2005 写道

spring并发的事务正确性按照你的理解就是死锁有提示?拜托,这好像跟spring没有什么关系把,我看你的标题应该是测试数据库的并发正确性。


又一次断章取义,被抓了现行。

我的原话:

mysaga 写道

无所谓。只要系统能(1),正确的提示那些操作失败的用户“你的操作失败,请稍后再试”,并且(2),绝对保证数据的一致性:失败的操作没有任何效果,成功的操作能够被持久,银行没有损失,用户也没有损失——那么,一个健壮系统最基本的必要条件就满足了。

现在我的测试用例已经证明了这一点,所以我认为它是成功的。


这怎么又跟 spring 没有关系了??难道这第(2)点不是由 spring 的声明式事务处理 + hibernate 帮忙的锁机制实现的吗?

对你,我无话可说。
46 楼 rain2005 2009-08-04  
mysaga 写道
rain2005 写道
即使只是使用 servlet + jdbc,也不用手写SQL代码来管理什么事务了——因为你根本不需要事务嘛。

这就没有什么可以说的了,第一次听说servlet + jdbc根本不需要事务。建议你先搞清楚什么是事务。。。


少在那边断章取义了。你怎么不引用我的全文??

就因为你先在那边嚷嚷什么:
rain2005 写道
......楼主完全可以这样
线程1操作帐户A,B,线程2操作帐户C,D,线程3操作帐户E,F,这样才是真正的测试spring并发的事务正确性
......


我才说:
mysaga 写道
“线程1操作帐户A,B,线程2操作帐户C,D,线程3操作帐户E,F,” —— 嘿嘿,如果只考虑这种井水不犯河水的操作,还要spring事务处理干嘛?普通的 servlet + jdbc 就搞定了....

所以,俺的测试用例目标还是很明确的:多线程粗暴蹂躏共享的数据库记录,看你 spring + hibernate 如何反应...


然后你说:
rain2005 写道
明白你的意思了。
spring + hibernate对待数据库记录的方式和servlet + jdbc没有什么区别吗?要保证事务一致性那是数据库的职责把。不就是更新锁和乐观锁?这好像不关spring + hibernate什么事把?


接下来才是我的那段话,注意看红色的全文:
mysaga 写道

哈哈,俺的意思是说,有了spring + hibernate,你自己就不用手写SQL代码来管理事务了——不是你不需要事务管理,而是 spring + hibernate 帮你搞定绝大多数东东。

而假如只考虑“线程1操作帐户A,B,线程2操作帐户C,D,线程3操作帐户E,F,”这种井水不犯河水的操作,就不用担心任何事务隔离级别、锁之类的东西。因此,即使只是使用 servlet + jdbc,不用手写SQL代码来管理什么事务了——因为你根本不需要事务嘛。


我是在指出你最早的谬误,而且我的本意是“servlet + jdbc”
同样需要(写在SQL里面的)事务管理,
在你所假想的场景下不需要,也因此,你提出的这个场景
根本测不了什么“spring并发的事务正确性”——只不过,我的话比较委婉罢了。

你却从我的整段话里截了后一半,把我的意思完全反了过来。这算什么??


按照你的说法,线程1操作帐户A,B,线程2操作帐户C,D,线程3操作帐户E,F这个是不需要事务的???
spring并发的事务正确性按照你的理解就是死锁有提示?拜托,这好像跟spring没有什么关系把,我看你的标题应该是测试数据库的并发正确性。
45 楼 mysaga 2009-08-04  
andyyehoo 写道


关于缓存方面,太久没用Hibernate,已经不确定默认的行为了,所以我说不确定因素。但是实际上,重点不是到底默认行为如何,不是重点,有这样的可能性就是危险。系统的关键代码,强健性本来就应该是重中之重,你的代码,只要是万一打开了Hibernate的二级缓存,就是不安全的,这样的设计,对于银行系统的来说,就是不够的。你换个角度,不是这段代码的编写者,而是作为公司的Code Reviewer,在单独看到这样的代码的时候,肯定会有这样的第一直觉浮现。



嘿嘿,这就是写测试用例的目的之一啊!

假如某天某人在我不知情的情况下打开了二级缓存,出现了问题,我再重新跑一遍测试,就发现了嘛!

	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]));
		}
	}


这里的 assertNotSame 就会失败。

所以我说,在最基本的方面,我的用例到达了目的。

当然,你说的也是我的意思,接下来要做的还很多。

相关推荐

    多线程测试组件groboutils

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

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

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

    net.sourceforge.groboutils.groboutils-core:5

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

    groboutils-core-5

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

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

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

    groboutils

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

    GroboUtils-5-core

    通过使用GroboUtils-5-core,开发者可以更加高效和有信心地测试其多线程应用程序,减少因并发问题导致的软件缺陷,提高软件质量和稳定性。同时,它也降低了测试的复杂性,让测试工作更加系统化和可维护。

    GroboUtils-3

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

    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