MarkLogic Java API死锁检测

7

我们的一个应用程序刚刚遭受了一些麻烦的死锁。我花了很长时间来重新创建这个问题,因为在我的Java应用程序日志中,死锁(或堆栈跟踪)并没有立即显示出来。

让我感到惊讶的是,Marklogic Java API会重试失败的请求(例如由于死锁)。如果您的请求不是多语句请求,这可能是有意义的,但否则我不确定它是否有效。

所以让我们继续解决这个死锁问题。我创建了一个简单的代码片段,在其中有意地创建了一个死锁。该片段创建一个名为“test.xml”的文档,然后尝试从两个不同的事务中读取和写入,每个事务都在新线程上运行。

public static void main(String[] args) throws Exception {
        final Logger root = (Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
        final Logger ok = (Logger) LoggerFactory.getLogger(OkHttpServices.class);
        root.setLevel(Level.ALL);
        ok.setLevel(Level.ALL);

        final DatabaseClient client = DatabaseClientFactory.newClient("localhost", 8000, new DatabaseClientFactory.DigestAuthContext("username", "password"));

        final StringHandle handle = new StringHandle("<doc><name>Test</name></doc>")
            .withFormat(Format.XML);
        client.newTextDocumentManager().write("test.xml", handle);

        root.info("t1: opening");
        final Transaction t1 = client.openTransaction();
        root.info("t1: reading");
        client.newXMLDocumentManager()
            .read("test.xml", new StringHandle(), t1);

        root.info("t2: opening");
        final Transaction t2 = client.openTransaction();
        root.info("t2: reading");
        client.newXMLDocumentManager()
            .read("test.xml", new StringHandle(), t2);

        new Thread(() -> {
            root.info("t1: writing");
            client.newXMLDocumentManager().write("test.xml", new StringHandle("<doc><t>t1</t></doc>").withFormat(Format.XML), t1);
            t1.commit();
        }).start();

        new Thread(() -> {
            root.info("t2: writing");
            client.newXMLDocumentManager().write("test.xml", new StringHandle("<doc><t>t2</t></doc>").withFormat(Format.XML), t2);
            t2.commit();
        }).start();

        TimeUnit.MINUTES.sleep(5);

        client.release();
    }

这段代码将产生以下日志:

14:12:27.437 [main] DEBUG c.m.client.impl.OkHttpServices - Connecting to localhost at 8000 as admin
14:12:27.570 [main] DEBUG c.m.client.impl.OkHttpServices - Sending test.xml document in transaction null
14:12:27.608 [main] INFO  ROOT - t1: opening
14:12:27.609 [main] DEBUG c.m.client.impl.OkHttpServices - Opening transaction
14:12:27.962 [main] INFO  ROOT - t1: reading
14:12:27.963 [main] DEBUG c.m.client.impl.OkHttpServices - Getting test.xml in transaction 5298588351036278526
14:12:28.283 [main] INFO  ROOT - t2: opening
14:12:28.283 [main] DEBUG c.m.client.impl.OkHttpServices - Opening transaction
14:12:28.286 [main] INFO  ROOT - t2: reading
14:12:28.286 [main] DEBUG c.m.client.impl.OkHttpServices - Getting test.xml in transaction 8819382734425123844
14:12:28.289 [Thread-1] INFO  ROOT - t1: writing
14:12:28.289 [Thread-1] DEBUG c.m.client.impl.OkHttpServices - Sending test.xml document in transaction 5298588351036278526
14:12:28.289 [Thread-2] INFO  ROOT - t2: writing
14:12:28.290 [Thread-2] DEBUG c.m.client.impl.OkHttpServices - Sending test.xml document in transaction 8819382734425123844

t1t2都不会被提交。MarkLogic日志确认实际上存在死锁:

==> /var/opt/MarkLogic/Logs/8000_AccessLog.txt <==
127.0.0.1 - admin [24/Nov/2018:14:12:30 +0000] "PUT /v1/documents?txid=5298588351036278526&category=content&uri=test.xml HTTP/1.1" 503 1034 - "okhttp/3.9.0"

==> /var/opt/MarkLogic/Logs/ErrorLog.txt <==
2018-11-24 14:12:30.719 Info: Deadlock detected locking Documents test.xml

如果其中一个请求失败并抛出异常,这将不是问题,但事实并非如此。MarkLogic Java Api会重试每个请求,最多达到120秒,而其中一个更新在大约120秒后超时:
Exception in thread "Thread-1" com.marklogic.client.FailedRequestException: Service unavailable and maximum retry period elapsed: 121 seconds after 65 retries
    at com.marklogic.client.impl.OkHttpServices.putPostDocumentImpl(OkHttpServices.java:1422)
    at com.marklogic.client.impl.OkHttpServices.putDocument(OkHttpServices.java:1256)
    at com.marklogic.client.impl.DocumentManagerImpl.write(DocumentManagerImpl.java:920)
    at com.marklogic.client.impl.DocumentManagerImpl.write(DocumentManagerImpl.java:758)
    at com.marklogic.client.impl.DocumentManagerImpl.write(DocumentManagerImpl.java:717)
    at Scratch.lambda$main$0(scratch.java:40)
    at java.lang.Thread.run(Thread.java:748)

有哪些可能的解决方法?一种方法可能是为事务设置最大生存时间(比如5秒),但这感觉有点像临时抱佛脚,不太可靠。还有其他的想法吗?我应该检查其他的设置吗?
我使用的是MarkLogic 9.0-7.2和marklogic-client-api:4.0.3。
编辑:解决死锁的一种方法是同步调用函数,这实际上是我在我的情况下解决问题的方法(请参见注释)。但我认为根本问题仍然存在。在多语句事务中出现死锁不应该被隐藏在120秒的超时之后。我宁愿要一个立即失败的请求,而不是一个120秒的文档锁定+每个线程64次失败重试

2
死锁是一个需要在概念上克服的问题,工具只能帮助你到这个程度。在关键部分周围创建一个锁通常是一种简单的方法。 - daniu
1
在我的Java应用程序中,使用锁是我解决问题的方法,但我认为默认情况下死锁事务重试120秒有点粗鲁。难以解决的死锁不应该抛出错误吗?有人可能会将此视为marklogic-client-api的错误/功能请求。 - Wagner Michael
请参考以下链接:https://dev59.com/e3NA5IYBdhLWcg3wEpgU该文介绍如何在Java中进行编程式死锁检测。 - Shiva
@secretsuperstar 我的问题不是关于Java死锁,而是关于MarkLogic死锁。但无论如何,谢谢你的评论! - Wagner Michael
1个回答

3
死锁通常可以通过重试来解决。内部,服务器会进行内部重试循环,因为死锁通常是短暂的和偶发的,持续时间非常短。在您的情况下,您构建了一个无论何时限都不会成功的情况。
当使用REST API时,可以通过避免多语句事务来避免应用程序层面上的死锁(这就是Java API使用的方法)。
由于客户端负责管理事务ID并且服务器无法检测到客户端错误或客户端标识,因此无法完全安全地实现REST上的多语句事务。除非您积极主动地处理错误和多线程问题,否则可能会出现非常微妙的问题。如果将逻辑“推”到服务器(xquery或javascript),服务器将能够更好地管理事物。
至于Java API是否实现重试对于这种情况是否“好”,这是有争议的。(看似易于使用的接口的妥协是许多本来可以成为选项的东西被作为惯例为您决定。通常没有一种大小适合所有的答案。在这种情况下,我假设的想法是死锁更可能是由“意外”而不是同时运行的相同代码/逻辑引起的 - 在那种情况下重试是一个好选择。在您的示例中,这不是一个好的选择,但是之前的错误仍然会可预测地失败,直到您更改代码以“不这样做”为止)。
如果还不存在,请求配置超时和重试行为的功能似乎是一个合理的请求。但是,我建议尝试避免导致开放事务的任何REST调用 - 本质上这是有问题的,特别是如果您没有及时注意到问题(那么它更可能在生产中咬你)。与JDBC不同,JDBC保持连接打开,以便服务器可以检测到客户端断开连接,而HTTP和ML Rest API则不会 - 这导致与Java中传统数据库编码不同的编程模型。

感谢您提供这么详细的答案。为了更好地解释我们在应用程序中为什么要使用大量的多语句事务,我想再补充一点背景:该应用程序是一个Java Spring框架应用程序,因此采用了典型的 @Transactional 模型(即客户端事务模型)。我们从SQL切换到了MarkLogic,但显然仍有数千行客户端代码期望这种类型的模型。因此,我们在各个地方都使用了多语句事务。 - Wagner Michael
1
如果发生死锁,SQL会回滚事务,但现在不再是这种情况了。这使得从SQL转换和迁移到MarkLogic变得更加困难:(。除此之外,我真的很喜欢你的答案。它解释了当前行为背后的原因,并且确实有道理。你是对的,这个死锁代码应该被移植到MarkLogic以获得最佳的事务处理。 - Wagner Michael
你不觉得MarkLogic有一种方法可以告诉客户端其多语句事务是死锁的一部分吗?这样,客户端就可以为此事务抛出错误并相应地回滚它(而不是重试)。它已经告诉客户端它无法执行该请求,为什么不只是添加“因为死锁”呢? - Wagner Michael
根据此链接:https://help.marklogic.com/Knowledgebase/Article/View/17/0/understanding-xdmp-deadlock,我认为您正在遇到一种*可恢复的*死锁情况,在这种情况下,REST API不会向客户端报告错误,但性能会受到影响。请参见https://developer.marklogic.com/blog/resolving-unresolvable-deadlocks以获取有关修复建议。您可以按照第一个链接启用DEBUG级别日志记录来验证此假设。 - DALDEI
祝你好运。小心你所要求的内容。许多死锁和“类似死锁”的情况很难与瞬态负载区分开来——直到一段时间过去了。此时,过早终止实际上可能导致退化状态或不必要的失败。你可能会发现,在应用程序的更高层次上处理这个问题是更好的方法。考虑反应式设计模式。低级别的代码通常没有足够的上下文来在尽力满足请求和过早失败之间做出良好的选择。 - DALDEI
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接