繁体   English   中英

如何线程安全地读取和写入数据库?

[英]How to thread-safely read from and write to the database?

我正在编写Java API,并且正在使用MySQL。 线程安全读取和写入数据库的最佳实践是什么?

例如,采用以下createUser方法:

// Create a user
// If the account does not have any users, make this
// user the primary account user
public void createUser(int accountId) {
    String sql =
            "INSERT INTO users " +
            "(user_id, is_primary) " +
            "VALUES " +
            "(0, ?) ";
    try (
            Connection conn = JDBCUtility.getConnection();
            PreparedStatement ps = conn.prepareStatement(sql)) {

        int numberOfAccountsUsers = getNumberOfAccountsUsers(conn, accountId);
        if (numberOfAccountsUsers > 0) {
            ps.setString(1, null);
        } else {
            ps.setString(1, "y");
        }

        ps.executeUpdate();
    } catch (SQLException e) {
        e.printStackTrace();
    }
}

// Get the number of users in the specified account
public int getNumberOfAccountsUsers(Connection conn, int accountId) throws SQLException {
    String sql =
            "SELECT COUNT(*) AS count " +
            "FROM users ";
    try (
            PreparedStatement ps = conn.prepareStatement(sql);
            ResultSet rs = ps.executeQuery()) {

        return rs.getInt("count");
    }
}

说源A调用createUser ,并且刚刚读取到帐户ID 100有0个用户。 然后,源B调用createUser ,并且在源A执行其更新之前,源B还读取到该帐户ID有0个用户。 结果,源A和源B都创建了主要用户。

如何安全地实施这种情况?

这种事情就是交易的目的,它们使您可以输入多个具有一致性保证的语句。 从技术上讲原子性,一致性,隔离性和持久性,请参阅https:// stackoverflow / q / 974596

您可以使用

select for update 

并且锁将保持到交易结束。

对于这种情况,您可以锁定整个表,运行select来对行进行计数,然后执行插入。 锁定表后,您知道在选择和插入之间行数不会改变。

通过将select插入插入来消除对2条语句的需要是一个好主意。 但是通常,如果您使用的是数据库,则需要了解事务。

对于本地jdbc事务(与xa事务相反),您需要对参与同一事务的所有语句使用相同的jdbc连接。 所有语句运行后,在连接上调用commit。

轻松使用事务是spring框架的卖点。

问题概述

首先,您的问题与线程安全无关,它与可以更好地优化的事务和代码有关。

如果我正确阅读了这篇文章,则尝试将第一个用户设置为主用户。 我可能根本不会采用这种方法(也就是说,第一个用户有一个is_primary行),但是如果我不这样做,则根本不会在您的Java应用程序中包含该条件。 每次创建用户时,不仅要评估条件用户,而且还不必要地调用数据库。 相反,我会像这样重构。

首先,确保您的表是这样的。

CREATE TABLE `users` (
     user_id INT NOT NULL AUTO_INCREMENT,
     is_primary CHAR(1),
     name VARCHAR(30),
     PRIMARY KEY (user_id)
);

换句话说,您的user_id应该是auto_incrementnot null 我包括了name列,因为它有助于说明我的观点(但是您可以用user_idis_primary用户表中的其他列)。 我也将user_id primary,因为它可能是。

如果您只想修改当前表,它将是这样。

ALTER TABLE `users` MODIFY COLUMN `user_id` INT not null auto_increment;

然后创建一个触发器,以便每当您插入一行时,它都会检查它是否是第一行,如果是,则会相应地进行更新。

delimiter |
    CREATE TRIGGER primary_user_trigger 
    AFTER INSERT ON users
      FOR EACH ROW BEGIN

        IF NEW.user_id = 1 THEN
           UPDATE users SET is_primary = 'y' where user_id = 1;
        END IF;
      END;
| delimiter ;

那时,您只需要一个createUser方法即可将新记录插入数据库中,而根本不需要指定user_id或primary字段 因此,假设您的表格是这样的:

您的createUser方法基本上如下所示

// Create a user
// If the account does not have any users, make this
// user the primary account user
public void createUser(String name) {
    String sql =
            "INSERT INTO users (name) VALUES(?)"
    try (
            Connection conn = JDBCUtility.getConnection();
            PreparedStatement ps = conn.prepareStatement(sql));

            ps.setString(1, name);
            ps.executeUpdate();
    } catch (SQLException e) {
        e.printStackTrace();
    }
}

两次运行用户表看起来像

|    user_id   |   is_primary   |   name   |
+--------------+----------------+----------+
|      1       |       y        |   Jimmy  |
---------------+----------------+----------+
|      2       |       null     |   Cindy  |

然而...

但是,即使我认为上述解决方案比原始问题中提出的解决方案要好,但我仍然认为这不是处理此问题的最佳方法。 在提出任何建议之前,我需要了解有关该项目的更多信息。 第一个用户主要是什么? 什么是主要用户? 如果只有一个主用户,为什么不能仅对其进行手动设置? 有管理控制台吗? 等等。

如果您的问题是一个例子...

因此,如果您的问题只是一个用于说明有关事务管理的较大问题的示例,则可以在连接为false时使用自动提交,并手动管理事务。 您可以在Oracle JDBC Java文档中阅读有关自动提交的更多信息。 此外,如上所述,您可以根据您的特定操作来更改表/行级别的锁定,但是我认为这对于此问题是非常不现实的解决方案。 您还可以将select包含为子查询,但同样,您只是在临时作弊上使用创可贴。

总而言之,除非您想完全改变您的主要用户范例,否则我认为这是最有效且组织最完善的方式。

我认为最好在一个插入查询中包含设置is_primary值的条件:

public void createUser(int accountId) {
    String sql = "INSERT INTO users (user_id, is_primary) " +
                 "SELECT 0, if(COUNT(*)=0, 'y', null) " +
                 "FROM users";
   //....
}

因此,在多线程环境中运行代码将是安全的,并且可以摆脱getNumberOfAccountsUsers方法。

并且为了消除对并发insert的可见性的任何怀疑(感谢@tsolakp comment ),如文档所述

并发INSERT的结果可能不会立即可见。

您可以对插入语句使用相同的Connection对象,以便MySql服务器将按顺序运行它们,

要么

is_primary列使用唯一索引 (我假设ynull是可能的值。 请注意 ,所有MySql引擎都允许使用多个null值),因此,在违反唯一约束的情况下,您只需要重新运行insert查询。 这种独特的索引解决方案还将与您现有的代码一起使用。

暂无
暂无

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

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