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

问题描述 投票:1回答:3

我正在编写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都创建了主要用户。

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

java mysql multithreading jdbc
3个回答
2
投票

这种事情是交易的目的,它们允许您输入多个语句,并保证一致性。技术上的原子性,一致性,隔离性和耐久性,请参阅https://stackoverflow/q/974596

你可以锁定行

select for update 

锁定将一直持续到交易结束。

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

通过将select放入插入中来消除对2个语句的需要是个好主意。但总的来说,如果您使用数据库,则需要了解交易。

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

易于使用事务是spring框架的卖点。


0
投票

Question Overview

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

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

Code

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

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作为主要原因,因为它可能是。

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

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或主字段。所以我们假设你的表是这样的:

您的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  |

However...

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

If your question was an example....

因此,如果您的问题仅仅是用于说明有关事务管理的更大问题的示例,则可以使用连接上的自动提交为false并手动管理您的事务。您可以在Oracle JDBC Java文档中阅读有关autocommit的更多信息。另外,如上所述,您可以根据具体操作更改表/行级别锁定,但我认为这是解决此问题的非常不切实际的解决方案。您还可以将选择作为子查询包括在内,但您又只是在不良练习中加入了创可贴。

总而言之,除非你想彻底改变你的主要用户范例,我认为这是最有效和最有条理的方式。


0
投票

我认为最好包含你的条件来在一个插入查询中设置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)的可见性的任何疑问,如the documentation所述:

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

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

要么

unique index用于is_primary列,(我假设ynull是可能的值。请注意,所有MySql引擎都允许多个null值),因此如果出现唯一约束违规,您只需要重新运行insert查询。这种独特的索引解决方案也适用于您现有的代码。

© www.soinside.com 2019 - 2024. All rights reserved.