我支持爲其客戶端提供某種文件系統的真實世界的Java應用程序(Web服務)。單個文件系統樹的所有元數據都保存在數據庫中。現在,當「太多」併發更新發生在給定的樹上時,由於隱式行級寫入鎖,基礎數據庫事務會發生死鎖情況。樹節點上的JPA併發更新導致事務死鎖
我交叉閱讀Joe Celko's Trees and Hierarchies in SQL for Smarties以及給定實體模型的結果是如何在SQL中表示樹的最簡單情況,稱爲鄰接列表。儘管我喜歡Celko先生的嵌套集合模式,但我擔心這在JPA中很難實現,即使頻繁插入也會導致大量數據重組開銷。
正在使用的數據庫是MySQL,使用的庫包括Spring-Data-JPA和Hibernate 4.1.7。
由於原始代碼非常複雜,我提取了一個最小的測試用例。看看下面。
這是表示樹節點的實體:
@Entity
public class Node extends AbstractPersistable<Integer> {
private static final Logger logger = LoggerFactory.getLogger(Node.class);
@ManyToOne
private Node parent;
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true)
private Set<Node> children = new LinkedHashSet<Node>();
@Column(nullable = false)
@Type(type = "org.jadira.usertype.dateandtime.joda.PersistentInstantAsMillisLong")
private Instant timeStamp = Instant.now();
@Version
private Long version;
public Node addChild(Node child) {
child.setParent(this);
children.add(child);
touch();
return this;
}
public void touch() {
doTouch(Instant.now());
}
private void doTouch(Instant time) {
logger.info("touching {} to {}", this, time);
this.timeStamp = time;
if (parent != null) {
parent.doTouch(time);
}
}
}
這是我測試的情況下,模擬在樹上併發更新:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfig.class)
public class NodeServiceIntegrationTest {
@Inject
private NodeRepository repository;
@Inject
private NodeService service;
private Random random;
@Before
public void setUp() throws Exception {
this.random = new Random();
}
@Test
public void testRecursiveUpdate() throws Exception {
int depth = random.nextInt(10) + 1;
Node root = new Node();
final Node leaf = createHierarchy(root, depth);
root = repository.save(root);
int threadCount = 50;
Callable<Node> addChild = new Callable<Node>() {
@Override
public Node call() throws Exception {
return service.addChild(leaf.getId(), new Node());
}
};
List<Callable<Node>> tasks = Collections.nCopies(threadCount, addChild);
ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
List<Future<Node>> futures = executorService.invokeAll(tasks);
List<Node> resultList = new ArrayList<Node>(futures.size());
for (Future<Node> future : futures) {
resultList.add(future.get());
}
// todo assert something... ;)
}
private Node createHierarchy(Node root, int depth) {
int count = random.nextInt(20) + 1;
for (int i = 0; i < count; i++) {
Node child = new Node();
root.addChild(child);
if (depth > 0 && random.nextBoolean()) {
return createHierarchy(child, depth - 1);
}
}
return Iterables.get(root.getChildren(), count - 1);
}
}
這provoces同樣的錯誤我在看生產代碼太:
org.springframework.dao.CannotAcquireLockException: Deadlock found when trying to get lock; try restarting transaction; SQL [n/a]; nested exception is org.hibernate.exception.LockAcquisitionException: Deadlock found when trying to get lock; try restarting transaction
at org.springframework.orm.hibernate3.SessionFactoryUtils.convertHibernateAccessException(SessionFactoryUtils.java:639)
at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:104)
at org.springframework.orm.jpa.JpaTransactionManager.doCommit(JpaTransactionManager.java:516)
at org.springframework.transaction.support.AbstractPlatformTransactionManager.processCommit(AbstractPlatformTransactionManager.java:754)
at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:723)
at org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java:394)
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:120)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172)
at org.springframework.aop.framework.Cglib2AopProxy$DynamicAdvisedInterceptor.intercept(Cglib2AopProxy.java:622)
at a.e.treetest.service.NodeService$$EnhancerByCGLIB$$98fc01a8.addChild(<generated>)
at a.e.treetest.service.NodeServiceIntegrationTest$1.call(NodeServiceIntegrationTest.java:54)
at a.e.treetest.service.NodeServiceIntegrationTest$1.call(NodeServiceIntegrationTest.java:51)
at java.util.concurrent.FutureTask$Sync.innerRun(FutureTask.java:303)
at java.util.concurrent.FutureTask.run(FutureTask.java:138)
at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:895)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:918)
at java.lang.Thread.run(Thread.java:662)
所以我的問題是是否有更好的方法來r在SQL數據庫中存在樹,以及支持頻繁插入而不引發死鎖的好方法。還是我不得不接受在併發更新時可能會發生死鎖,我應該考慮自動重試原始操作?
我在基於您發佈的鏈路上實現嘗試中間遇到僵局的問題。由於似乎沒有更多的答案進來,我會接受你的未經考驗。謝謝。 – aeisele