我的同事和我有一个在 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。
经过多次令人沮丧的调查,我发现删除@Transactional
for B.execute() 可以解决问题。随着那条线的消失,我可以有很多线程,但我总是在下一个“开始”之前看到一个“开始”,然后是一个“结束”(并且我的数据结构保持不变)。不知何故,synchronized
唯一似乎在@Transactional
不存在时起作用。但我不明白为什么。任何人都可以帮忙吗?关于如何进一步调查的任何提示?
在堆栈跟踪中,我可以看到在 A.doPost() 和 B.execute() 之间以及 B.execute() 和 C.insert() 之间生成了一个 aop/cglib 代理。我想知道代理的构造是否会以某种方式破坏synchronized
行为。