论坛首页 Java企业应用论坛

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

浏览 27948 次
该帖已经被评为良好帖
作者 正文
   发表时间:2009-08-02  
// 随机选取转入户头   
            long toId = accountIds[   
                                 randomGenerator.nextInt(accountIds.length)   
                                 ];   
  
            // 确保转出、转入户头不是同一个   
            while (toId == fromId) {   
                toId = accountIds[   
                                randomGenerator.nextInt(accountIds.length)   
                                ];   
            }   
            [/b]   
            // 随机选取转帐数额(0 ~ 149元之间)   
            BigDecimal amount = BigDecimal.valueOf(randomGenerator.nextInt(150));  

仔细看看这段代码,不死锁才怪。要测试先考虑测试用例,在我看来你的测试用例太差劲了。


0 请登录后投票
   发表时间:2009-08-02   最后修改:2009-08-02
// 随机选取转入户头   
            long toId = accountIds[   
                                 randomGenerator.nextInt(accountIds.length)   
                                 ];   
  
            // 确保转出、转入户头不是同一个   
            while (toId == fromId) {   
                toId = accountIds[   
                                randomGenerator.nextInt(accountIds.length)   
                                ];   
            }   
            [/b]   
            // 随机选取转帐数额(0 ~ 149元之间)   
            BigDecimal amount = BigDecimal.valueOf(randomGenerator.nextInt(150));  

仔细看看这段代码,不死锁才怪。要测试先考虑测试用例,在我看来你的测试用例太差劲了。


发重复了,请求删除。


0 请登录后投票
   发表时间:2009-08-03  
ximenpiaohua 写道
对于以上问题
数据不一致应该是共享资源没有加锁,主要是你的账户数组。
死锁应该是多个线程操作同一条记录导致,可以用乐观锁验证。


是的,都已经改正了。现在,每个 thread 都有自己的账户数组拷贝;乐观锁也已经加入,并起作用了。见我楼上的回帖:http://www.iteye.com/topic/436718?page=2#1112937
0 请登录后投票
   发表时间:2009-08-03  
rain2005 写道
// 随机选取转入户头   
            long toId = accountIds[   
                                 randomGenerator.nextInt(accountIds.length)   
                                 ];   
  
            // 确保转出、转入户头不是同一个   
            while (toId == fromId) {   
                toId = accountIds[   
                                randomGenerator.nextInt(accountIds.length)   
                                ];   
            }   
            [/b]   
            // 随机选取转帐数额(0 ~ 149元之间)   
            BigDecimal amount = BigDecimal.valueOf(randomGenerator.nextInt(150));  

仔细看看这段代码,不死锁才怪。要测试先考虑测试用例,在我看来你的测试用例太差劲了。




哥们儿,说实话俺看不出这段代码有啥好死锁的,它无非就是随机的选择转入户头罢了,没有任何多线程共享资源的问题。

如果你指的是这样造成了多线程同时修改同一个数据库记录形成数据库死锁,那倒是我前几天遇到的问题,不过现在基本解决了。

我觉得测试用例不能简单的用差劲与否来形容。我这个 test case 的目的,就是想看在高并发访问(并修改)同一组数据库记录的时候,spring 的事务管理将如何应对。这个 test case 现在达到了我的目的,因为它证明了数据的一致性并未受到破坏。在我看来,既然它能够向老板充分演示我们系统的健壮,它自然也就是成功的。
0 请登录后投票
   发表时间:2009-08-03   最后修改:2009-08-03
mysql MyISAM 是不支持事务的, mysql innDB是支持事务的,所以不管你怎么配置事务,对MyISAM类型的表是不起作用的,还有如果你随机取两个来转账的设计,也是不符合业务的,比如一笔是从a到b转账,一笔从b到a转账,肯定死锁,但是在真正业务中这是不会出现的。
0 请登录后投票
   发表时间:2009-08-03  
mysaga 写道
rain2005 写道
// 随机选取转入户头   
            long toId = accountIds[   
                                 randomGenerator.nextInt(accountIds.length)   
                                 ];   
  
            // 确保转出、转入户头不是同一个   
            while (toId == fromId) {   
                toId = accountIds[   
                                randomGenerator.nextInt(accountIds.length)   
                                ];   
            }   
            [/b]   
            // 随机选取转帐数额(0 ~ 149元之间)   
            BigDecimal amount = BigDecimal.valueOf(randomGenerator.nextInt(150));  

仔细看看这段代码,不死锁才怪。要测试先考虑测试用例,在我看来你的测试用例太差劲了。




哥们儿,说实话俺看不出这段代码有啥好死锁的,它无非就是随机的选择转入户头罢了,没有任何多线程共享资源的问题。

如果你指的是这样造成了多线程同时修改同一个数据库记录形成数据库死锁,那倒是我前几天遇到的问题,不过现在基本解决了。

我觉得测试用例不能简单的用差劲与否来形容。我这个 test case 的目的,就是想看在高并发访问(并修改)同一组数据库记录的时候,spring 的事务管理将如何应对。这个 test case 现在达到了我的目的,因为它证明了数据的一致性并未受到破坏。在我看来,既然它能够向老板充分演示我们系统的健壮,它自然也就是成功的。


首先,spring事务本身就是在多线程的环境下运行的,你把tomcat并发线程设大点,就可以测试。
在则,你现在完全还没有搞明白你这里的死锁是怎么回事。。。。
我打个比方,假设线程1获得两个随机数10,20,线程2获得两个随机数20,10,然后你更新不死锁才怪。当然这种概率太低,所以当你并发一上来才发生。
在说一种情况,线程1获得随机数10,20,线程2获得两个随机数10,30,线程3获得两个随机数30,20,同样可能死锁。剩下的自己想把。。。。。
0 请登录后投票
   发表时间:2009-08-03  
fengbaoxp 写道
mysql MyISAM 是不支持事务的, mysql innDB是支持事务的,所以不管你怎么配置事务,对MyISAM类型的表是不起作用的,


这个当然我了解,所以在顶楼我就说了

mysaga 写道

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


fengbaoxp 写道
还有如果你随机取两个来转账的设计,也是不符合业务的,比如一笔是从a到b转账,一笔从b到a转账,肯定死锁,但是在真正业务中这是不会出现的。


这有点武断了。比如说,两口子相濡以沫,这边从提款机A转钱给老婆,那边从提款机B转钱给老公,真的不可能吗?

就算你能说服我这不可能,你能说服那些变态的老板、客户吗?




0 请登录后投票
   发表时间:2009-08-03   最后修改:2009-08-03
rain2005 写道


首先,spring事务本身就是在多线程的环境下运行的,你把tomcat并发线程设大点,就可以测试。


如果“把tomcat并发线程设大点,就可以测试”,那 junit 就没多大意思了嘛,反正任何功能都可以打开tomcat,然后从浏览器访问自己写的网站,一个一个页面看下去,用自己眼睛来验证系统有没有bug。。。

拜托,我这里之所以用 junit 跑多线程来测 spring 事务处理,就是要在完全自动化的条件下,看看它能否应付高并发的随机转帐操作,然后用 junit 来自动验证数据的完整性。这和你给每个模块写unit test,目的是很类似的。

rain2005 写道

在则,你现在完全还没有搞明白你这里的死锁是怎么回事。。。。
我打个比方,假设线程1获得两个随机数10,20,线程2获得两个随机数20,10,然后你更新不死锁才怪。当然这种概率太低,所以当你并发一上来才发生。
在说一种情况,线程1获得随机数10,20,线程2获得两个随机数10,30,线程3获得两个随机数30,20,同样可能死锁。剩下的自己想把。。。。。


哥们儿真逗,还知道我心里想的啥了。互相转帐的可能性楼上解释过了,不多说了。

再重复一遍,测试的目的,是要证明:如果在没有任何规律的'乱'操作后,系统能够保证数据的完整(比如说,测试前后,总额不变);假如出现任何形式的死锁,系统能够正确的提示用户“你的操作不成功”。

现在测试目的已经达到了。当然要感谢各位帮我解惑的弟兄们。
0 请登录后投票
   发表时间:2009-08-03  

引用
我打个比方,假设线程1获得两个随机数10,20,线程2获得两个随机数20,10,然后你更新不死锁才怪。当然这种概率太低,所以当你并发一上来才发生。
在说一种情况,线程1获得随机数10,20,线程2获得两个随机数10,30,线程3获得两个随机数30,20,同样可能死锁。剩下的自己想把。。。。。


哥们,我也不好意思说你什么了,你先把死锁在那里分清楚,测试的目的和现实业务的数据,必须是让你出现这种情况的,这样,才有数据库的锁机制出现。所有锁机制就是避免上述如此发生引起。

如果是最高级级别serliazable,例如你的第一个例子,如果在两个事务里,是不会出现死锁的,你可以测试看看。
0 请登录后投票
   发表时间:2009-08-04   最后修改:2009-08-04
LZ还是没体会rain2005的批评

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

你的测试还存在很多漏洞,这些漏洞和数据库无关,目前看来,数据库的漏洞,通过乐观锁和事务,基本上已经解决了,虽然还不是最优的,但是可以说不会有bug了。但是代码由于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,这样的不严谨设计,在普通的系统都有问题,何况一个银行系统。单靠数据库的事务来控制,是不严谨的。

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秒,我相信冲突的可能性是非常大的。

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

作为一个代码review的结果来看,应该在线程并发安全的基础上实现数据库事务完整。

1. 加入DTO设计,确保transfer操作的业务对象是DTO,最后在update操作中,把dto转换回domain。DTO肯定不是单实例和共享的,自己控制DTO的生成,而不是交给Hibernate。
2. 把完整的帐户加减操作都放在transfer中,不要在DTO或者domain加入任何业务方法,保证transfer可以看到完整的最简单加减过程,把业务方法集中

有了上面两步,基本上是安全了。

3. 如果一定要保留直接这样的简单设计,应该加写锁。即便是有了DTO,加这样的写锁,也不为过。
	 
         accountFromLock.write();
		  from.setBalance(from.getBalance().subtract(amount));  
		  getDao().update(from);  
	 accountFromLock.release();

	 accountToLock.write();
		  from.setBalance(from.getBalance().subtract(amount));  
		  getDao().update(from);  
	 accountToLock.release();


当然了,这样的设计,会麻烦很多,但是像银行这样的EE系统,本来就要求严谨度很高,否则LZ也不需要做这样的测试。既然有这样的要求了,就要把严谨进行到底。多一层dto或者锁又如何?像这样的操作,就应该实现,每一步操作,无论等待时间多久,在多线程并发环境下,最后都能得出正确的结果,当然,数据库报的竞争锁错是合理的,但是频率要尽可能的低。

如果是不那么需要严谨的系统,那么我现在也倾向不用Java用Ruby,该怎么方便这么写,不用什么domain object到dto,spring啊Hibernate一堆,当然,那样的系统,不是说就不用一致性了,只不过那是另外的设计理念和想法了。
1 请登录后投票
论坛首页 Java企业应用版

跳转论坛:
Global site tag (gtag.js) - Google Analytics