I try to implement transfer between two accounts, so that it works correctly in concurrent transactions. But for some reason, these two transactions a) run sequentially b) the second transaction doesn't see changes made by the first one. As a result, balances are not updated correctly.
Is it expected behaviour?...
CREATE TABLE ACCOUNT (
ID BIGINT PRIMARY KEY AUTO_INCREMENT,
NUMBER VARCHAR(25) NOT NULL,
BALANCE DECIMAL(19,2) DEFAULT 0 CHECK (BALANCE >= 0)
);
INSERT INTO ACCOUNT(NUMBER, BALANCE) VALUES('acc1', 300);
INSERT INTO ACCOUNT(NUMBER, BALANCE) VALUES('acc2', 400);
public class ConcurrentTransfersTest {
@Test
void testForStackOverflow() throws Exception {
final String url = "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;MULTI_THREADED=1;" +
"INIT=RUNSCRIPT FROM 'classpath:/h2/schema.sql'\\;RUNSCRIPT FROM 'classpath:/h2/test-data.sql'";
JdbcConnectionPool pool = JdbcConnectionPool.create(url, "sa", "");
assertEquals(BigDecimal.valueOf(30000, 2), readData(pool, 1));
assertEquals(BigDecimal.valueOf(40000, 2), readData(pool, 2));
Thread t1 = new Thread(() -> updateData("t1", pool, 1, 2, BigDecimal.valueOf(50)));
Thread t2 = new Thread(() -> updateData("t2", pool, 2, 1, BigDecimal.valueOf(200)));
t1.start();
Thread.sleep(200);
t2.start();
t1.join();
t2.join();
assertEquals(BigDecimal.valueOf(40000, 2), readData(pool, 1));
assertEquals(BigDecimal.valueOf(30000, 2), readData(pool, 2));
try (Connection c = pool.getConnection(); Statement st = c.createStatement()) {
st.execute("SHUTDOWN IMMEDIATELY");
}
}
private BigDecimal readData(DataSource ds, long id) {
try (Connection c = ds.getConnection();
PreparedStatement select = c.prepareStatement("SELECT BALANCE FROM ACCOUNT WHERE ID = ?")) {
return readFromPreparedStatment(select, id);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
private BigDecimal readFromPreparedStatment(PreparedStatement select, long id) throws Exception {
select.setLong(1, id);
ResultSet rs = select.executeQuery();
rs.next();
BigDecimal b = rs.getBigDecimal(1);
rs.close();
return b;
}
private void updateData(String thread, DataSource ds, long from, long to, BigDecimal amount) {
try {
try (Connection c = ds.getConnection();
PreparedStatement minus = c.prepareStatement("UPDATE ACCOUNT SET BALANCE = BALANCE - ? WHERE ID = ?");
PreparedStatement plus = c.prepareStatement("UPDATE ACCOUNT SET BALANCE = BALANCE + ? WHERE ID = ?");
PreparedStatement select = c.prepareStatement("SELECT BALANCE FROM ACCOUNT WHERE ID = ?")
) {
c.setAutoCommit(false);
System.out.println(System.currentTimeMillis() + " " + thread + ": transfer " + amount + " " +
"from = " + readFromPreparedStatment(select, from) + " to = " + readFromPreparedStatment(select, to));
minus.setBigDecimal(1, amount);
minus.setLong(2, from);
System.out.println(System.currentTimeMillis() + " " + thread + ": from minus " + amount);
minus.executeUpdate();
Thread.sleep(2000);
plus.setBigDecimal(1, amount);
plus.setLong(2, to);
System.out.println(System.currentTimeMillis() + " " + thread + ": to plus " + amount);
plus.executeUpdate();
System.out.println(System.currentTimeMillis() + " " + thread + ": from = " + readFromPreparedStatment(select, from));
System.out.println(System.currentTimeMillis() + " " + thread + ": to = " + readFromPreparedStatment(select, to));
c.commit();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
1508149269895 t1: transfer 50 from = 300.00 to = 400.00
1508149269895 t1: from minus 50
1508149271899 t1: to plus 50
1508149271900 t1: before commit from = 250.00
1508149271900 t1: before commit to = 450.00
1508149271913 t2: transfer 200 from = 400.00 to = 300.00
1508149271913 t2: from minus 200
1508149273918 t2: to plus 200
1508149273918 t2: before commit from = 200.00
1508149273918 t2: before commit to = 500.00
I try to implement transfer between two accounts, so that it works correctly in concurrent transactions. But for some reason, these two transactions a) run sequentially b) the second transaction doesn't see changes made by the first one. As a result, balances are not updated correctly.
Is it expected behaviour?...
H2 database settings
MULTI_THREADED=1Test case
SQL Schema
Unit test
Output