春季交易可以不同步一个同步方法吗?

问题描述:

我的同事和我有一个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代理。

+0

谢谢Plouh。我没有意识到ReentrantLock - 我会看看。 – John 2010-02-02 11:51:39

+0

ReentrantLock可以解决这个问题吗? – Matt 2013-04-03 05:10:50

选项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); 
    ... 
} 
+0

谢谢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来做这件事。 执行是这样的:

  1. 开始交易
  2. 电话与@Transactional注释的方法
  3. 当方法返回提交事务

步骤1.和3可以被执行由许多线程在同一时间。因此你会得到多次交易的开始。

您唯一的解决方案是将调用同步到方法本身。

+0

我相信这是正确和最有帮助的答案。春天aop的微妙往往被忽视,这导致各种意想不到的问题,就像这一个! – 2014-11-05 10:50:41

+0

正确答案 - 我们运行并发单元测试来确认这一点 – mithrandir 2014-12-18 09:33:53