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

时间:2017-12-27 19:42:59

标签: java mysql multithreading jdbc

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

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

3 个答案:

答案 0 :(得分:2)

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

您可以使用

锁定行
select for update 

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

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

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

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

易于使用交易是Spring框架的卖点。

答案 1 :(得分:0)

问题概述

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

如果我正确读取此信息,您尝试将第一个用户设置为主要用户。我可能根本不会采用这种方法(也就是说,为第一个用户设置了一个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作为主要原因,因为它可能是。

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

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  |

然而...

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

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

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

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

答案 2 :(得分: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的结果可能不会立即显示。

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

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