树节点上的JPA并发更新会导致事务死锁

时间:2013-04-17 12:48:08

标签: java spring hibernate jpa tree

我支持一个真实的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数据库中的树,以及在不引发死锁的情况下支持频繁插入的好方法。或者我必须接受在并发更新时可能会发生死锁,我应该考虑自动重试原始操作吗?

1 个答案:

答案 0 :(得分:0)

尝试使用Nested Set Model - 我从来没有遇到过它的死锁问题

Hibernate implementation