我支持一个真实的Java应用程序(Web服务),它为它的客户端提供某种文件系统。各个文件系统树的所有元数据都保存在数据库中。现在,当给定树上发生“太多”并发更新时,由于隐式行级写锁定,底层数据库事务会进入死锁状态。
我交叉阅读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);
}
}
这也引发了我在生产代码中看到的同样的错误:
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)
所以我的问题是,是否有更好的方法来表示SQL数据库中的树,以及在不引发死锁的情况下支持频繁插入的好方法。或者我必须接受在并发更新时可能会发生死锁,我应该考虑自动重试原始操作吗?