Java SQLite:auto commit 为false 事务失败时是否需要调用rollback?

Java SQLite: Is it necessary to call rollback when auto commit is false and a transaction fails?

在 Java 中使用 SQLite 数据库时,假设我将自动提交设置为 false。发生 SQLException 时是否有必要调用 rollback() 方法?或者我可以直接忽略调用它并且事务将自动回滚(我在事务期间所做的所有更改将自动撤消)?

快速回答:您问的事实可能意味着您做错了。但是,如果你必须知道:是的,你需要显式回滚。

引擎盖下发生了什么

在 JDBC 级别(如果您使用的是 JOOQ、JDBI、Hibernate 或类似的东西,通常是在 JDBC 之上构建的库),您有一个 Connection 实例。您可能已经通过 DriverManager.getConnection(...) 获得了它 - 或者连接池为您获得了它,但有些事情确实发生了。

该连接可以在事务的中间(自动提交模式仅意味着连接假定您打算在您关心的每个 SQL 语句后写一个额外的 commit() 运行 在该连接的上下文中,这就是所有自动提交所做的,但是,显然,如果它打开,您可能处于 'clean' 状态,也就是说,该连接处理的最后一个命令是 COMMITROLLBACK).

如果它正在处理事务并且您关闭了连接,ROLLBACK 是隐式的

连接必须做出选择,它不能保持现有状态,因此,它提交或回滚。规范保证它不会只是为了取笑你而提交,因此,它会回滚。

然后问题归结为您的具体设置。具体来说,这很危险:

try (Connection con = ...) {
  con.setAutoCommit(false);
  try {
    try (var s = con.createStatement()) {
      s.execute("DROP TABLE foobar");
    }
  } catch (SQLException ignore) {
    // ignoring an exception usually bad idea. But for sake of example..
  }

  // A second statement on the same connection...
  try (var s = con.createStatement()) {
    s.execute("DROP TABLE quux");
  }
}

JDBC 驱动程序,就规范而言,可以在第二个语句中按照 'the connection is aborted; you must explicitly rollback first then you can use it again' 的方式自由抛出 SQL 异常。

但是,上面的代码很糟糕。您不能对这种代码使用事务隔离级别 SERIALIZABLE(一旦您获得的用户超过少数,应用程序将崩溃并在重试异常行列中燃烧),并且它要么在做一些无用的事情(在使用连接池时为多个事务重新使用 1 个连接),要么正在严重解决问题(问题:为每个事务使用新连接是昂贵的)。

1 个事务,1 个连接

上述危险的唯一原因是因为我们在与连接对象关联的单个 try-block 中做两件不相关的事情(即:2 个事务)。我们正在重新使用连接。这是一个坏主意:连接有与之相关的包袱:已设置的属性,是的,处于 'abort' 状态(在连接之前 需要 显式回滚愿意执行任何其他 SQL)。只需关闭连接并获得一个新连接,您就可以丢掉所有的包袱。这是导致单元测试不容易捕获的错误的包袱,a.k.a。错误一旦触发,将花费大量金钱/眼球/善意/时间来修复。客观地说,如果它避免了一个 100 倍更难捕获的错误,那么你必须更喜欢 99 个易于捕获的错误,而这是属于后一类的错误之一。

连接很贵?什么?

这有一个问题:只需为单个事务使用一个连接,然后将其交回,这样就无需回滚,因为如果您close()它,连接将自动执行此操作:获取连接非常占用资源。

因此,人们倾向于/可能应该使用连接池来避免这种成本。也不要在这里写你自己的;使用 HikariCP 或类似的东西。这些工具为您汇集连接:您无需调用 DriverManager.getConnection,而是向 HikariCP 请求一个,并在完成连接后将连接交还给 HikariCP。 Hikari 将负责为您重置它,其中包括如果连接处于事务中途时回滚,并处理任何其他每个连接设置,使其恢复到已知状态。

常见的DB交互模型本质上是这样'flow':

someDbAccessorObject.act(db -> {
  // do a single transaction here
});

就是这样。这段代码,在幕后,做了各种各样的事情:

  • 使用连接池。
  • 以正确的方式设置连接,主要涉及将自动提交设置为 false,并设置正确的事务隔离级别。
  • 将在 lambda 块的末尾提交,如果没有发生异常。在任何一种情况下都将连接交还给池。
  • 会捕获SQL异常并分析是否为重试异常。如果是,是否使用 nagle 算法或其他一些随机指数退避算法并重新运行s lambda 块(这就是重试异常的意思)。
  • 负责将 'gets' 连接的代码(例如确定 JDBC url 的正确使用)放在一个地方,以便数据库配置中的更改不需要在您的代码库中进行全局 search/replace 狂欢。

在该模型中,您 运行 很少遇到问题,因为您最终会遇到 '1 笔交易? 1 个连接!模型。通常这是昂贵的(创建连接比像往常一样滚动 back/committing 然后继续在同一个连接对象上进行新事务要昂贵得多),但一旦使用池化器,它归结为同一件事。

换句话说:正确编写的数据库代码不应该有你的问题,除非你自己编写连接池,在这种情况下答案肯定是:显式回滚。