5

我的同事和我有一个在 MyEclipse 内的 Tomcat 上使用 Spring 3.0.0 和 JPA(hibernate 3.5.0-Beta2)的 Web 应用程序。其中一种数据结构是树。只是为了好玩,我们尝试用 JMeter 对“插入节点”操作进行压力测试,发现了一个并发问题。Hibernate 报告发现两个具有相同私钥的实体,就在这样的警告之后:

 WARN  [org.hibernate.engine.loading.LoadContexts] fail-safe cleanup (collections) : ...

如果多个线程同时调用 insert() 方法,很容易看出这些问题是如何发生的。

我的 servlet A 调用服务层对象 B.execute(),然后它调用较低层对象 C.insert()。(实际代码太大,无法发布,因此有些删节。)

小服务程序 A:

  public void doPost(Request request, Response response) {
    ...
    b.execute(parameters);
    ...
  }

服务乙:

  @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() synchronized。已经是了@Transactional,但实际上需要同步的是业务逻辑,而不仅仅是持久化,所以这似乎是合理的。

我的 C.insert() 方法也是@Transactional. 但由于 Spring 中的默认事务传播似乎是必需的,我认为没有为 C.insert() 创建任何新事务。

所有组件 A、B 和 C 都是 spring-beans,因此是单例。如果确实只有一个 B 对象,那么我得出的结论是,一次执行 b.execute() 的威胁不应该超过一个。当负载较轻时,只使用单个线程,就是这样。但是在负载下,会涉及额外的线程,我看到有几个线程在第一个打印“完成”之前打印“开始”。这似乎违反synchronized了方法的本质。

我决定this在日志消息中打印以确认是否只有一个 B 对象。所有日志消息都显示相同的对象 ID。

经过多次令人沮丧的调查,我发现删除@Transactionalfor B.execute() 可以解决问题。随着那条线的消失,我可以有很多线程,但我总是在下一个“开始”之前看到一个“开始”,然后是一个“结束”(并且我的数据结构保持不变)。不知何故,synchronized唯一似乎在@Transactional不存在时起作用。但我不明白为什么。任何人都可以帮忙吗?关于如何进一步调查的任何提示?

在堆栈跟踪中,我可以看到在 A.doPost() 和 B.execute() 之间以及 B.execute() 和 C.insert() 之间生成了一个 aop/cglib 代理。我想知道代理的构造是否会以某种方式破坏synchronized行为。

4

4 回答 4

5

问题是@Transactional 封装了同步方法。Spring 使用 AOP 执行此操作。执行是这样的:

  1. 开始交易
  2. 调用带有@Transactional 注释的方法
  3. 当方法返回时提交事务

步骤 1. 和 3. 可以由多个线程同时执行。因此,您可以多次开始交易。

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

于 2014-07-15T14:37:02.287 回答
2

正如您所说,同步关键字要求所涉及的对象始终相同。我自己没有观察到上述行为,但你的嫌疑人可能是正确的。

您是否尝试过从 doPost 方法中注销 b ?如果每次都不同,那么 AOP/cglib 代理就会有一些春天的魔力。

无论如何,我不会依赖 syncronized 关键字,而是使用java.util.concurrent.locks 中的ReentrantLock之类的东西来确保同步行为,因为无论可能有多个 cglib 代理,你的 b 对象总是相同的。

于 2010-02-02T11:29:03.643 回答
0

选项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);
    ...
}
于 2011-04-20T12:29:15.563 回答
0

再次选择选项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);
    ...
   }    
}
于 2011-05-04T23:48:00.543 回答