0

我支持一个真实世界的 Java 应用程序(Web 服务),它为它的客户提供某种文件系统。单个文件系统树的所有元数据都保存在数据库中。现在,当给定树上发生“太多”并发更新时,由于隐式行级写锁,底层数据库事务会陷入死锁情况。

我交叉阅读了Joe Celko 的 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 数据库中的树,以及在不引发死锁的情况下支持频繁插入的好方法。还是我必须接受并发更新时可能会发生死锁,我应该考虑自动重试原始操作?

4

1 回答 1

0

尝试使用嵌套集模型- 我从来没有遇到过死锁问题

休眠实现

于 2013-04-17T14:27:38.640 回答