读书人

纳闷:有没有人真正用多线程工具(比如

发布时间: 2012-11-07 09:56:10 作者: rapoo

疑惑:有没有人真正用多线程工具(比如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(此测试用例最新最完整的代码在这一个跟贴的末尾)



原测试类 AccountTransferMultiThreadTest.java,已做必要修改:
    50 楼    haoxichuan    2009-08-06              to mysaga
如果不用乐观锁,数据库也有自己的锁机制呀.为什么你测的这种多个线程操作同一帐户会出现死锁的情况呢?能把这种情况出现的具体说下吗? 51 楼 daquan198163 2009-08-06 先确认一下数据库死锁的定义:A B两个并发事务分别持有了对方需要的一行记录的更新锁,因此无论等待多久AB都无法得到需要的锁。

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

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

前面说的都是数据库本身的特性,但它对于保证交易的一致性还不够,因为可能出现“更新丢失”现象(比如转账前后,帐不平),这只能靠应用(通过乐观锁或悲观锁)来解决,其中乐观锁不依赖数据库(靠version、时间戳等),而悲观锁依赖数据库的select for update特性。 52 楼 daquan198163 2009-08-06 Spring的事务处理绝对是线程安全的,否则也不要出来混了。
它把事务上下文、数据库连接、hibernate session等等这些非线程安全的东西都用threadlocal绑定到了当前线程,根本不存在线程安全的问题。
至于数据库连接池、hibernate session工厂等,本来就是线程安全的,不需要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 的绿色条:因为数据完全保持了一致。我想这就是事务处理、锁、。。。。等等的意义所在。

而当然,在实际系统中,处理异常也是很重要的:至少,必须告诉相关的用户:“你刚刚的操作失败,请稍后再试。”
54 楼 mysaga 2009-08-06 daquan198163 写道先确认一下数据库死锁的定义:A B两个并发事务分别持有了对方需要的一行记录的更新锁,因此无论等待多久AB都无法得到需要的锁。

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

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

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

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

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

通过发这个帖子以及后来的讨论,我确实学到许多东西。感谢各位热心的兄弟。 55 楼 haoxichuan 2009-08-06 非常感谢 daquan198163 mysaga 的解释
也就是说在高并发的情况下,数据库会出现一些死锁,或者其它错误,数据库能解决这些事情,但是他的解决方案不能保证数据的正确性.我们通过程序使用事务和锁去保证我们数据的正确性.使用 乐观或悲观 锁都可以解决是吧~~~

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


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

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


这个问题有点复杂。

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

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

当然现在明白,死锁并不可怕,只要处理好就行了。而正因为加上了锁,测试才能通过,数据才变得一致。 58 楼 C_J 2009-08-11 我想很多刚学习的人看懂这个帖子应该还是有点点难度的吧

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

原来楼主的有2个问题:

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

如上有错误的地方,还请大家指正. 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级缓存)里面拿数据,处理,然后更新——那种情形下,很难出现这种多线程访问同一对象的错误,所以在写多线程测试用例的时候,就大意了。。。。。 60 楼 daquan198163 2009-08-14 mysaga 写道现在回想起来,平常编程的时候,确实习惯了从 hibernate (无2级缓存)里面拿数据,处理,然后更新——那种情形下,很难出现这种多线程访问同一对象的错误,所以在写多线程测试用例的时候,就大意了。。。。。

不是说二级缓存没关系么?
nihongye 写道引用1.关于hibernate的二级缓存构造出来的对象是线程不安全的
这种说法是不对的,对象不是从缓存直接取出来的,缓存的是分解了的对象,每次取,对象是被重新构造出来的。
至于数据与数据库数据库一致性方面:因为二级缓存本身提供了几种策略,有不一致的也有一致的 61 楼 mysaga 2009-08-15 daquan198163 写道mysaga 写道现在回想起来,平常编程的时候,确实习惯了从 hibernate (无2级缓存)里面拿数据,处理,然后更新——那种情形下,很难出现这种多线程访问同一对象的错误,所以在写多线程测试用例的时候,就大意了。。。。。

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


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

而因为习惯了这种方式,我在开始写这个测试用例的时候,就不小心忽略了对象线程安全的问题。 62 楼 mislay 2009-08-16 所有的回帖看了一遍,但是也有了一些些疑问。

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

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

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

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

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

谢谢。 63 楼 C_J 2009-08-25 引用在一个事物未提交前,另一个事物不能开始,这样也能出现死锁吗?

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

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

update 1
update 2
update 1
update 2

commit;

LZ有3000个进程随机update就会出现这种状况~Oracle也是一样,应该所有数据库都是这样的,只是解决死锁问题的能力不一样。 64 楼 mislay 2009-08-25 C_J 写道引用在一个事物未提交前,另一个事物不能开始,这样也能出现死锁吗?

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

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

update 1
update 2
update 1
update 2

commit;

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

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

还是同一个问题:在一个事物未提交前,另一个事物不能开始,串行真的是这样吗?

读书人网 >软件架构设计

热点推荐