在H2数据库中的并行事务中,一个事务中提交的更改不可用

时间:2017-10-16 10:15:49

标签: java sql database transactions h2

我尝试在两个帐户之间实现转移,以便它在并发事务中正常工作。但由于某种原因,第二个交易正在等待第一个交易进行所有更新,但它没有看到更改。因此,余额未正确更新。

  1. 行为是否正确?
  2. 如果是对的,为什么呢? H2 docs明确表示提交事务所做的更改对其他事务可见。
  3. 如果这种转移方式基本上是错误的,哪一个是正确的?但请记住,我想依靠数据库设备,而不是Java锁等。
  4. H2数据库设置

    • 版本1.4.196
    • MVCC引擎已开启: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

    测试用例

    • 初始余额:
      1. acc1 = 300
      2. acc2 = 400
    • 进行两次并行传输(第二次传输延迟200ms):
      1. 50acc1acc2
      2. 200acc2acc1
    • 预计期末余额:
      1. acc1 = 450(减去50,加上200
      2. acc2 = 250(加50,减去200
    • 实际期末余额:
      1. acc1 = 500
      2. 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
    

0 个答案:

没有答案