春季交易可以不同步一个同步方法吗?
我的同事和我有一个Web应用程序,它在MyEclipse中的Tomcat上使用Spring 3.0.0和JPA(hibernate 3.5.0-Beta2)。其中一个数据结构是一棵树。为了好玩,我们尝试用JMeter对“插入节点”操作进行压力测试,发现并发问题。 Hibernate的报告发现两个实体具有相同的私钥,只是这样的警告后:春季交易可以不同步一个同步方法吗?
WARN [org.hibernate.engine.loading.LoadContexts] fail-safe cleanup (collections) : ...
这是很容易看到,如果多个线程同时调用insert()方法的问题是如何这样可能会发生。
我的servlet A调用一个服务层对象B.execute(),然后调用一个低层对象C.insert()。 (真正的代码太大张贴,所以这是有点删节。)
的Servlet答:
public void doPost(Request request, Response response) {
...
b.execute(parameters);
...
}
服务B:
@Transactional //** Delete this line to fix the problem.
public synchronized void execute(parameters) {
log("b.execute() starting. This="+this);
...
c.insert(params);
...
log("b.execute() finishing. This="+this);
}
子服务C:
@Transactional
public void insert(params) {
...
// data structure manipulation operations that should not be
// simultaneous with any other manipulation operations called by B.
...
}
我所有的状态改变呼叫都经过B,所以我决定让B.execute()。它已经是@Transactional
,但它实际上是需要同步的业务逻辑,而不仅仅是持久性,所以这似乎是合理的。
我的C.insert()方法也是@Transactional
。但是由于Spring中默认的事务传播似乎是必需的,我不认为有任何为C.insert()创建的新事务。
所有组件A,B和C都是spring-beans,因此是singleton。如果真的只有一个B对象,那么我得出结论,一次不能有多个威胁执行b.execute()。当负载较轻时,只使用一个线程,情况就是如此。但是在负载下,会有额外的线程参与进来,并且我看到有多个线程在第一个线程打印“完成”之前打印“开始”。这似乎违反了方法的性质。
我决定在日志消息中打印this
以确认是否只有一个B对象。所有日志消息都显示相同的对象ID。
经过多次令人沮丧的调查后,我发现删除用于B.execute()的@Transactional
解决了这个问题。随着这一行的消失,我可以拥有很多线程,但在下一个“开始”之前(我的数据结构保持不变),我总是看到“开始”,接着是“结束”。不知何故,似乎只有@Transactional
不存在。但我不明白为什么。谁能帮忙?有关如何进一步了解这一点的任何提示?
在栈跟踪,我可以看到,有()A.doPost之间产生一个AOP/CGLIB代理和B.execute() - 和也B.execute之间()和C.insert()。我想知道代理的构建是否会破坏行为。
如您所述,同步关键字需要所涉及的对象始终相同。我自己没有观察到上述行为,但您的嫌疑人可能是正确的。
您是否尝试从doPost方法注销b?如果每次都有所不同,那么AOP/cglib代理正在进行一些春季魔术。
无论如何,我不会依赖于syncronized关键字,而是使用类似于java.util.concurrent.locks中的ReentrantLock来确保同步行为,因为您的b对象始终是相同的,无论可能有多个cglib代理。
选项1:
Delete synchronized of ServiceB and:
public void doPost(Request request, Response response) {
...
synchronized(this)
{
b.execute(parameters);
}
...
}
选项2:
Delete synchronized of ServiceB and:
public class ProxyServiceB (extends o implements) ServiceB
{
private ServiceB serviceB;
public ProxyServiceB(ServiceB serviceB)
{
this.serviceB =serviceB;
}
public synchronized void execute(parameters)
{
this.serviceB.execute(parameters);
}
}
public void doPost(Request request, Response response)
{
...
ProxyServiceB proxyServiceB = new ProxyServiceB(b);
proxyServiceB .execute(parameters);
...
}
谢谢Springfan。用您的选项B,不能有多个ProxyServiceB实例吗?如果每个人只是在同步自己,我不明白这是如何提供任何好处。选项A(移动同步上一层)很可能工作,但似乎很遗憾必须同步所有可能调用这一服务的不同servlet。 – John 2011-04-21 12:46:09
选项2再次:
删除同步ServiceB的和:
public class ProxyServiceB (extends o implements) ServiceB
{
private ServiceB serviceB;
public ProxyServiceB(ServiceB serviceB)
{
this.serviceB =serviceB;
}
public synchronized void execute(parameters)
{
this.serviceB.execute(parameters);
}
}
public class TheServlet extends HttpServlet
{
private static ProxyServiceB proxyServiceB = null;
private static ProxyServiceB getProxyServiceBInstance()
{
if(proxyServiceB == null)
{
return proxyServiceB = new ProxyServiceB(b);
}
return proxyServiceB;
}
public void doPost(Request request, Response response)
{
...
ProxyServiceB proxyServiceB = getProxyServiceBInstance();
proxyServiceB .execute(parameters);
...
}
}
的问题是,@事务性封装了syn慢性方法。 Spring使用AOP来做这件事。 执行是这样的:
- 开始交易
- 电话与@Transactional注释的方法
- 当方法返回提交事务
步骤1.和3可以被执行由许多线程在同一时间。因此你会得到多次交易的开始。
您唯一的解决方案是将调用同步到方法本身。
我相信这是正确和最有帮助的答案。春天aop的微妙往往被忽视,这导致各种意想不到的问题,就像这一个! – 2014-11-05 10:50:41
正确答案 - 我们运行并发单元测试来确认这一点 – mithrandir 2014-12-18 09:33:53
谢谢Plouh。我没有意识到ReentrantLock - 我会看看。 – John 2010-02-02 11:51:39
ReentrantLock可以解决这个问题吗? – Matt 2013-04-03 05:10:50