V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
mayowwwww
V2EX  ›  Java

请教一个关于数据库事务的问题

  •  
  •   mayowwwww · 2018-10-26 18:47:47 +08:00 · 2496 次点击
    这是一个创建于 2222 天前的主题,其中的信息可能已经有所发展或是发生改变。

    两个线程同时查一行记录,然后更新记录。

    class MyRunner implements Runnable {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + ":连接数据库...");
                Connection connection = null;
                try {
                    connection = DriverManager.getConnection(RepeatableRead.DB_URL, RepeatableRead.USER, RepeatableRead.PASS);
                    connection.setAutoCommit(false);
                    connection.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ);
                    String querySql = "select * from account where id=?";
    
                    PreparedStatement queryStmt = connection.prepareStatement(querySql);
                    queryStmt.setInt(1, 1);
                    ResultSet set = queryStmt.executeQuery();
                    set.next();
                    int amount = set.getInt("amount");
                    System.out.println(Thread.currentThread().getName() + ":Amount=" + amount);
                    Thread.sleep(1000);
                    // 读取了数据,未提交事务。
                    synchronized (this) {
                        String updateSql = "update account set amount=? where id=1";
                        PreparedStatement updateStmt = connection.prepareStatement(updateSql);
                        updateStmt.setInt(1, amount + 100);
                        int count = updateStmt.executeUpdate();
                        if (count > 0) {
                            set = queryStmt.executeQuery();
                            set.next();
                            amount = set.getInt("amount");
                            System.out.println(Thread.currentThread().getName() + ":更新成功...|" + amount);
                            connection.commit();
                        }
                    }
                } catch (SQLException e) {
                    e.printStackTrace();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    
        @Test
        public void testMyRunner() throws ClassNotFoundException, SQLException, InterruptedException {
            MyRunner myRunner = new MyRunner();
            Thread t1 = new Thread(myRunner, "T1");
            Thread t2 = new Thread(myRunner, "T2");
            t1.start();
            t2.start();
            t1.join();
            t2.join();
        }
    
    /* 
    输出:
    T1:连接数据库...
    T2:连接数据库...
    T2:Amount=277
    T1:Amount=277
    T2:更新成功...|377
    T1:更新成功...|277
    
    T1 为什么还是 277 ?
    */
    

    由于隔离级别是 RR,T1 不应该也输出 377 吗?

    另外要怎么才能保证最后 amount 得到正确的结果 447 呢?

    第 1 条附言  ·  2018-10-26 23:42:39 +08:00
    这种操作好像确实会导致数据不一致的问题。查了些资料,除了 #1 的方法,还可以通过在表中添加 version 字段,在更新的时候通过判断 version 来解决。
    Chyroc
        1
    Chyroc  
       2018-10-26 19:21:04 +08:00   ❤️ 1
    select for update
    wqlin
        2
    wqlin  
       2018-10-26 21:07:18 +08:00
    RP 保证一个事务读不到另一个事务已经提交的修改。
    可以试下 Read Commit,可以读到另一个事务已经提交的修改(没有试过)
    另外,楼主的代码应该是:

    ```
    synchronized (MyRunner.class) {
    ....
    }
    ```
    如果用 this,其实两个线程并没有互斥执行
    mayowwwww
        3
    mayowwwww  
    OP
       2018-10-26 23:37:32 +08:00
    @wqlin
    ``` java
    MyRunner myRunner = new MyRunner();
    Thread t1 = new Thread(myRunner, "T1");
    Thread t2 = new Thread(myRunner, "T2");
    ```
    锁定的应该都是 myRunner 这个实例对象吧。
    REPEATABLE READ 的确是读不到其他会话的修改,select 查询并没有问题。
    问题是当前线程的 update 并没有生效(如果生效的话 T1 应该也是 377 )。
    lu5je0
        4
    lu5je0  
       2018-10-27 00:27:37 +08:00
    在我电脑上试了下,T1 和 T2 更新后都是 377 啊
    mayowwwww
        5
    mayowwwww  
    OP
       2018-10-27 00:49:21 +08:00
    @lu5je0 mysql 么?其他数据库可能事务隔离级别不一样看到的结果也不一样。
    lu5je0
        6
    lu5je0  
       2018-10-27 01:43:50 +08:00
    @mayowwwww 是 mysql
    wqlin
        7
    wqlin  
       2018-10-27 10:42:21 +08:00
    @mayowwwww #3
    抱歉看错了,用 this 也可以。
    不过为啥楼主你更新不成功,我试了一下,最后 T1 和 T2 都是 377。这应该算是 Lost Update 吧,一个 transaction 覆盖了另一个 transaction 更新的值。
    如果要输出 447,我觉得可以用线程互斥+ Read Committed,一个线程先 commit,然后另一个线程再开启一个 transaction,代码见: https://gist.github.com/wqlin/e957e01fcb40986996b71831a4c45404
    在我的电脑上输出为:
    ```
    T1:连接数据库...
    T2:连接数据库...
    T2:Amount=400
    T2:更新成功...|500
    T1:Amount=500
    T1:更新成功...|600
    ```
    wqlin
        8
    wqlin  
       2018-10-27 12:18:47 +08:00   ❤️ 1
    @wqlin #7 补一下,mysql 不能处理 lost update 的问题,所以一个办法是使用 #1 提到的 for update 显式的锁住行; PostgreSQL,Oracle 这些可以检查到 Lost update。楼主可以参考下 Designing Data Intensive Application 的 第七章 transaction
    dbolo123
        9
    dbolo123  
       2018-10-27 17:16:57 +08:00 via Android
    最简单的应该就是把要 update 字段作为 where 条件,然后看 affect_row
    dbolo123
        10
    dbolo123  
       2018-10-27 17:17:42 +08:00 via Android
    @dbolo123 为 0 就重试
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   3127 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 856ms · UTC 14:02 · PVG 22:02 · LAX 06:02 · JFK 09:02
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.