Если я запускаю транзакцию и проверяю мои проходы условий, а затем обновляю таблицу, другой процесс не может обновить между получением и обновлением, верно?
То естьправильно, но даже с SERIALIZABLE изоляцией транзакции вы все равно можете столкнуться с тупиковыми ситуациями, если несколько процессов попытаются использовать вашу стратегию «проверить, затем обновить и зафиксировать».Рассмотрим упрощенный пример, в котором код просто хочет увеличить purchasedItems
максимум до 10:
try (Connection conn = DriverManager.getConnection(connectionUrl, myUid, myPwd)) {
conn.setAutoCommit(false);
conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);
final int maxPurchasedItems = 10;
Statement st = conn.createStatement();
System.out.println("Initial SELECT ...");
Long t0 = System.nanoTime();
ResultSet rs = st.executeQuery("SELECT purchasedItems FROM items WHERE id = 1");
rs.next();
int n = rs.getInt(1);
System.out.printf("Original value: %d (%d ms)%n",
n, (System.nanoTime() - t0) / 1000000);
if (n >= maxPurchasedItems) {
System.out.printf("Increment would exceed limit of %d. Cancelled.%n", maxPurchasedItems);
conn.rollback();
} else {
Thread.sleep(5000);
t0 = System.nanoTime();
System.out.println("Attempting UPDATE ...");
st.executeUpdate("UPDATE items SET purchasedItems = purchasedItems+1 WHERE id = 1");
rs = st.executeQuery("SELECT purchasedItems FROM items WHERE id = 1");
rs.next();
n = rs.getInt(1);
System.out.printf("Updated value: %d (%d ms)%n",
n, (System.nanoTime() - t0) / 1000000);
Thread.sleep(5000);
conn.commit();
}
} catch (Throwable ex) {
ex.printStackTrace(System.err);
}
Если мы попытаемся запустить этот код одновременно в двух независимых процессах, мы увидим
Process_A:
Initial SELECT ...
Original value: 6 (142 ms)
Attempting UPDATE ...
Updated value: 7 (1910 ms)
Process_B:
Initial SELECT ...
Original value: 6 (144 ms)
Attempting UPDATE ...
com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction
Это потому, что первоначальный SELECT Process_A устанавливает блокировку записи в строке.Это только блокировка записи, поэтому начальный SELECT Process_B может продолжаться.Однако это также накладывает блокировку записи на строку, поэтому две транзакции блокируются.MySQL должен выбрать транзакцию, которую нужно убить, а Process_B неудачник.
Вместо этого вам следует использовать стратегию «обнови, затем проверь и откатись при необходимости»:
try (Connection conn = DriverManager.getConnection(connectionUrl, myUid, myPwd)) {
conn.setAutoCommit(false);
final int maxPurchasedItems = 10;
Statement st = conn.createStatement();
System.out.println("Initial UPDATE ...");
Long t0 = System.nanoTime();
st.executeUpdate("UPDATE items SET purchasedItems = purchasedItems+1 WHERE id = 1");
ResultSet rs = st.executeQuery("SELECT purchasedItems FROM items WHERE id = 1");
rs.next();
int n = rs.getInt(1);
System.out.printf("Updated value: %d (%d ms)%n",
n, (System.nanoTime() - t0) / 1000000);
Thread.sleep(5000);
if (n > maxPurchasedItems) {
System.out.printf("Increment exceeds limit of %d. Rolling back.%n", maxPurchasedItems);
conn.rollback();
} else {
conn.commit();
}
} catch (Throwable ex) {
ex.printStackTrace(System.err);
}