我尝试在两个帐户之间实现转移,以便它在并发事务中正常工作。但由于某种原因,第二个交易正在等待第一个交易进行所有更新,但它没有看到更改。因此,余额未正确更新。
H2数据库设置
MVCC mode is enabled by default in version 1.4.x
If MVCC is enabled, changing the lock mode (LOCK_MODE) has no effect)
MULTI_THREADED=1
测试用例
acc1
= 300
acc2
= 400
50
从acc1
到acc2
200
从acc2
到acc1
acc1
= 450
(减去50
,加上200
)acc2
= 250
(加50
,减去200
)acc1
= 500
acc2
= 200
SQL架构
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