简体   繁体   English

有条件地更新交易中的值

[英]Conditional update of a value within a transaction

I have a totalAmount and purchasedItems of items column in my table items .我的表items中有一个totalAmountpurchasedItems of items 列。 I want to atomically update purchasedItems if: totalAmount >= purchasedItems + 1 , else I want to throw an error.我想自动更新purchasedItems如果: totalAmount >= purchasedItems + 1 ,否则我想抛出错误。 I tried doing something like this but it fails.我试过做这样的事情但它失败了。 How can I achieve this atomically (I am doing this with java jdbc)?我怎样才能以原子方式实现这一点(我正在用 java jdbc 这样做)?

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
BEGIN TRANSACTION
    DECLARE @purchased AS INT
    DECLARE @total AS INT
    SELECT @purchased = SELECT (purchasedItems + 1) FROM events WHERE id=1
    SELECT @total = SELECT totalAmount FROM items WHERE id=1
    IF @purchased > @total
    BEGIN
        SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Max value passed'
    END 
    # ELSE DO UPDATE 
    SELECT @remaining
COMMIT TRANSACTION

The error message is never thrown永远不会抛出错误消息

If I start a transaction and do a get to check my condition passes and then update the table, another process cant update in between the get and update, right?如果我开始一个事务并执行 get 检查我的条件是否通过,然后更新表,另一个进程不能在 get 和更新之间更新,对吗?

That is correct, but even with SERIALIZABLE transaction isolation you can still encounter deadlocks if multiple processes try to use your "check then update and commit" strategy.这是正确的,但即使使用 SERIALIZABLE 事务隔离,如果多个进程尝试使用您的“检查然后更新并提交”策略,您仍然会遇到死锁。 Consider a simplified example where the code simply wants to increment purchasedItems to a maximum of 10:考虑一个简化的示例,其中代码只是想将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);
}

If we try to run that code simultaneously under two independent processes we see如果我们尝试在两个独立的进程下同时运行该代码,我们会看到

Process_A:进程_A:

Initial SELECT ...
Original value: 6 (142 ms)
Attempting UPDATE ...
Updated value: 7 (1910 ms)

Process_B:流程_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

That's because Process_A's initial SELECT places a read lock (aka shared lock) on the row.这是因为 Process_A 的初始 SELECT 在该行上放置了一个读锁(也称为共享锁)。 It's only a read lock, so Process_B's initial SELECT is allowed to proceed.它只是一个读锁,因此允许 Process_B 的初始 SELECT 继续进行。 However, it also places a read lock on that same row so the two transactions are deadlocked when it comes to writes.但是,它还在同一行上放置了一个读锁,因此这两个事务在写入时会发生死锁。 MySQL has to pick a transaction to kill, and Process_B is the unlucky one. MySQL 必须选择一个事务来杀死,而 Process_B 是不幸的。

Instead, you should use an "update then check and rollback if necessary" strategy:相反,您应该使用“更新然后检查并在必要时回滚”策略:

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);
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM