H2 数据库 - 在 h2database 升级到版本 1.4.198 后,来自 select 的更新设置了过时的数据
H2 database - Update from select sets out-of-date data after h2database upgrade to version 1.4.198
我们在项目数据库中有一个简单的计数器。到目前为止,我们使用的是 1.4.197 版的 H2 数据库。使用此版本执行以下示例代码段始终意味着计数器为 5000。升级到版本 1.4.198 或更高版本会使以下代码返回不一致的结果,通常在 1500 到 2000 之间。
public static void main(String[] args) throws SQLException, InterruptedException {
String url = "jdbc:h2:mem:testdb";
Connection connection = DriverManager.getConnection(url);
connection.prepareStatement("CREATE TABLE t1 (id INT PRIMARY KEY, counter INT)").execute();
connection.prepareStatement("INSERT INTO t1 (id, counter) VALUES (1, 0)").execute();
int threads = 10;
int times = 500;
ExecutorService service = Executors.newFixedThreadPool(threads);
ExecutorCompletionService<Void> cs = new ExecutorCompletionService<>(service);
for (int i = 0; i < threads; i++) {
cs.submit(() -> {
Connection conn = DriverManager.getConnection(url);
IntStream.range(0, times).forEach($ -> {
try {
conn.prepareStatement("UPDATE t1 SET counter = (SELECT counter FROM t1 WHERE id = 1) + 1 WHERE id = 1").executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
}
});
return null;
});
}
for (int i = 0; i < threads; i++) {
cs.take();
}
ResultSet resultSet = connection.prepareStatement("SELECT counter FROM t1 WHERE id = 1").executeQuery();
resultSet.next();
System.out.println("counter: " + resultSet.getInt("counter"));
}
我的假设是 SQL 下面的 select 语句在其他事务从 table 释放锁之前执行,然后用过时的数据执行更新来自 select 声明。
"UPDATE t1 SET counter = (SELECT counter FROM t1 WHERE id = 1) + 1 WHERE id = 1"
有没有人遇到过类似的问题?
普通SELECT
语句本身不会锁定选定的行,并且可以从锁定的行中读取提交的(旧)值。您的代码中的整个命令没有障碍,这就是为什么具有多线程执行功能的较新版本的 H2 可以同时执行此类命令。多个命令可能会读取相同的旧值并尝试更新该行。 UPDATE
锁定了该行,但是子查询已经被评估,并且由于自动提交,在执行命令后立即释放锁。您需要在子查询中使用 SELECT … FOR UPDATE
。
UPDATE t1 SET counter = (SELECT counter FROM t1 WHERE id = 1 FOR UPDATE) + 1 WHERE id = 1
顺便说一句,H2 1.4.198 是一个具有许多已知问题的测试版。使用 1.4.199 或 1.4.200 更安全。
我们在项目数据库中有一个简单的计数器。到目前为止,我们使用的是 1.4.197 版的 H2 数据库。使用此版本执行以下示例代码段始终意味着计数器为 5000。升级到版本 1.4.198 或更高版本会使以下代码返回不一致的结果,通常在 1500 到 2000 之间。
public static void main(String[] args) throws SQLException, InterruptedException {
String url = "jdbc:h2:mem:testdb";
Connection connection = DriverManager.getConnection(url);
connection.prepareStatement("CREATE TABLE t1 (id INT PRIMARY KEY, counter INT)").execute();
connection.prepareStatement("INSERT INTO t1 (id, counter) VALUES (1, 0)").execute();
int threads = 10;
int times = 500;
ExecutorService service = Executors.newFixedThreadPool(threads);
ExecutorCompletionService<Void> cs = new ExecutorCompletionService<>(service);
for (int i = 0; i < threads; i++) {
cs.submit(() -> {
Connection conn = DriverManager.getConnection(url);
IntStream.range(0, times).forEach($ -> {
try {
conn.prepareStatement("UPDATE t1 SET counter = (SELECT counter FROM t1 WHERE id = 1) + 1 WHERE id = 1").executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
}
});
return null;
});
}
for (int i = 0; i < threads; i++) {
cs.take();
}
ResultSet resultSet = connection.prepareStatement("SELECT counter FROM t1 WHERE id = 1").executeQuery();
resultSet.next();
System.out.println("counter: " + resultSet.getInt("counter"));
}
我的假设是 SQL 下面的 select 语句在其他事务从 table 释放锁之前执行,然后用过时的数据执行更新来自 select 声明。
"UPDATE t1 SET counter = (SELECT counter FROM t1 WHERE id = 1) + 1 WHERE id = 1"
有没有人遇到过类似的问题?
普通SELECT
语句本身不会锁定选定的行,并且可以从锁定的行中读取提交的(旧)值。您的代码中的整个命令没有障碍,这就是为什么具有多线程执行功能的较新版本的 H2 可以同时执行此类命令。多个命令可能会读取相同的旧值并尝试更新该行。 UPDATE
锁定了该行,但是子查询已经被评估,并且由于自动提交,在执行命令后立即释放锁。您需要在子查询中使用 SELECT … FOR UPDATE
。
UPDATE t1 SET counter = (SELECT counter FROM t1 WHERE id = 1 FOR UPDATE) + 1 WHERE id = 1
顺便说一句,H2 1.4.198 是一个具有许多已知问题的测试版。使用 1.4.199 或 1.4.200 更安全。